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 \
|
--type block \
|
||||||
--overwrite \
|
--overwrite \
|
||||||
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
|
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
|
||||||
- job: function_build
|
- job: function_test
|
||||||
dependsOn: frontend_build_production
|
dependsOn: frontend_build_production
|
||||||
condition: succeeded('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
|
displayName: Build And Push Function Image
|
||||||
pool:
|
pool:
|
||||||
name: odms-deploy-pipeline
|
name: odms-deploy-pipeline
|
||||||
@ -186,32 +206,6 @@ jobs:
|
|||||||
command: ci
|
command: ci
|
||||||
workingDir: dictation_function
|
workingDir: dictation_function
|
||||||
verbose: false
|
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
|
- task: Docker@0
|
||||||
displayName: build
|
displayName: build
|
||||||
inputs:
|
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',
|
||||||
|
};
|
||||||
6032
dictation_client/package-lock.json
generated
6032
dictation_client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,8 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"codegen": "sh codegen.sh",
|
"codegen": "sh codegen.sh",
|
||||||
"lint": "eslint --cache . --ext .js,.ts,.tsx",
|
"lint": "eslint --cache . --ext .js,.ts,.tsx",
|
||||||
"lint:fix": "npm run lint -- --fix"
|
"lint:fix": "npm run lint -- --fix",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-browser": "^2.33.0",
|
"@azure/msal-browser": "^2.33.0",
|
||||||
@ -25,7 +26,6 @@
|
|||||||
"@testing-library/jest-dom": "^5.16.4",
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
"@testing-library/react": "^13.3.0",
|
"@testing-library/react": "^13.3.0",
|
||||||
"@testing-library/user-event": "^14.2.1",
|
"@testing-library/user-event": "^14.2.1",
|
||||||
"@types/jest": "^27.5.2",
|
|
||||||
"@types/node": "^17.0.45",
|
"@types/node": "^17.0.45",
|
||||||
"@types/react": "^18.0.14",
|
"@types/react": "^18.0.14",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
@ -38,6 +38,7 @@
|
|||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-google-recaptcha-v3": "^1.10.0",
|
"react-google-recaptcha-v3": "^1.10.0",
|
||||||
@ -56,8 +57,10 @@
|
|||||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||||
"@mdx-js/react": "^2.1.2",
|
"@mdx-js/react": "^2.1.2",
|
||||||
"@openapitools/openapi-generator-cli": "^2.5.2",
|
"@openapitools/openapi-generator-cli": "^2.5.2",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
"@types/lodash": "^4.14.191",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/luxon": "^3.2.0",
|
"@types/luxon": "^3.2.0",
|
||||||
|
"@types/papaparse": "^5.3.14",
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@types/redux-mock-store": "^1.0.3",
|
"@types/redux-mock-store": "^1.0.3",
|
||||||
@ -67,16 +70,18 @@
|
|||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"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-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.0",
|
"eslint-plugin-jsx-a11y": "^6.6.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.30.1",
|
"eslint-plugin-react": "^7.30.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.8",
|
||||||
"redux-mock-store": "^1.5.4",
|
"redux-mock-store": "^1.5.4",
|
||||||
"sass": "^1.58.3",
|
"sass": "^1.58.3",
|
||||||
|
"ts-jest": "^29.1.2",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"vite": "^4.1.4",
|
"vite": "^4.1.4",
|
||||||
"vite-plugin-env-compatible": "^1.1.1",
|
"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", // ワークタイプ使用中エラー
|
"E011004", // ワークタイプ使用中エラー
|
||||||
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
|
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
|
||||||
"E013002", // ワークフロー不在エラー
|
"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;
|
] as const;
|
||||||
|
|||||||
@ -6,4 +6,4 @@ export type ErrorObject = {
|
|||||||
statusCode?: number;
|
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,
|
updateAccountInfoAsync,
|
||||||
getAccountRelationsAsync,
|
getAccountRelationsAsync,
|
||||||
deleteAccountAsync,
|
deleteAccountAsync,
|
||||||
|
updateFileDeleteSettingAsync,
|
||||||
} from "./operations";
|
} from "./operations";
|
||||||
|
|
||||||
const initialState: AccountState = {
|
const initialState: AccountState = {
|
||||||
@ -15,6 +16,8 @@ const initialState: AccountState = {
|
|||||||
tier: 0,
|
tier: 0,
|
||||||
country: "",
|
country: "",
|
||||||
delegationPermission: false,
|
delegationPermission: false,
|
||||||
|
autoFileDelete: false,
|
||||||
|
fileRetentionDays: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dealers: [],
|
dealers: [],
|
||||||
@ -29,6 +32,8 @@ const initialState: AccountState = {
|
|||||||
secondryAdminUserId: undefined,
|
secondryAdminUserId: undefined,
|
||||||
},
|
},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
autoFileDelete: false,
|
||||||
|
fileRetentionDays: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,6 +69,20 @@ export const accountSlice = createSlice({
|
|||||||
const { secondryAdminUserId } = action.payload;
|
const { secondryAdminUserId } = action.payload;
|
||||||
state.apps.updateAccountInfo.secondryAdminUserId = secondryAdminUserId;
|
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) => {
|
cleanupApps: (state) => {
|
||||||
state.domain = initialState.domain;
|
state.domain = initialState.domain;
|
||||||
},
|
},
|
||||||
@ -85,6 +104,10 @@ export const accountSlice = createSlice({
|
|||||||
action.payload.accountInfo.account.primaryAdminUserId;
|
action.payload.accountInfo.account.primaryAdminUserId;
|
||||||
state.apps.updateAccountInfo.secondryAdminUserId =
|
state.apps.updateAccountInfo.secondryAdminUserId =
|
||||||
action.payload.accountInfo.account.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;
|
state.apps.isLoading = false;
|
||||||
});
|
});
|
||||||
builder.addCase(getAccountRelationsAsync.rejected, (state) => {
|
builder.addCase(getAccountRelationsAsync.rejected, (state) => {
|
||||||
@ -99,6 +122,15 @@ export const accountSlice = createSlice({
|
|||||||
builder.addCase(updateAccountInfoAsync.rejected, (state) => {
|
builder.addCase(updateAccountInfoAsync.rejected, (state) => {
|
||||||
state.apps.isLoading = false;
|
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) => {
|
builder.addCase(deleteAccountAsync.pending, (state) => {
|
||||||
state.apps.isLoading = true;
|
state.apps.isLoading = true;
|
||||||
});
|
});
|
||||||
@ -115,6 +147,8 @@ export const {
|
|||||||
changeDealerPermission,
|
changeDealerPermission,
|
||||||
changePrimaryAdministrator,
|
changePrimaryAdministrator,
|
||||||
changeSecondryAdministrator,
|
changeSecondryAdministrator,
|
||||||
|
changeAutoFileDelete,
|
||||||
|
changeFileRetentionDays,
|
||||||
cleanupApps,
|
cleanupApps,
|
||||||
} = accountSlice.actions;
|
} = accountSlice.actions;
|
||||||
export default accountSlice.reducer;
|
export default accountSlice.reducer;
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
UpdateAccountInfoRequest,
|
UpdateAccountInfoRequest,
|
||||||
UsersApi,
|
UsersApi,
|
||||||
DeleteAccountRequest,
|
DeleteAccountRequest,
|
||||||
|
UpdateFileDeleteSettingRequest,
|
||||||
} from "../../api/api";
|
} from "../../api/api";
|
||||||
import { Configuration } from "../../api/configuration";
|
import { Configuration } from "../../api/configuration";
|
||||||
import { ViewAccountRelationsInfo } from "./types";
|
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<
|
export const deleteAccountAsync = createAsyncThunk<
|
||||||
{
|
{
|
||||||
/* Empty Object */
|
/* Empty Object */
|
||||||
|
|||||||
@ -16,3 +16,18 @@ export const selectIsLoading = (state: RootState) =>
|
|||||||
state.account.apps.isLoading;
|
state.account.apps.isLoading;
|
||||||
export const selectUpdateAccountInfo = (state: RootState) =>
|
export const selectUpdateAccountInfo = (state: RootState) =>
|
||||||
state.account.apps.updateAccountInfo;
|
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 {
|
export interface Apps {
|
||||||
updateAccountInfo: UpdateAccountInfoRequest;
|
updateAccountInfo: UpdateAccountInfoRequest;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
autoFileDelete: boolean;
|
||||||
|
fileRetentionDays: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ export const STATUS = {
|
|||||||
BACKUP: "Backup",
|
BACKUP: "Backup",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type StatusType = typeof STATUS[keyof typeof STATUS];
|
export type StatusType = (typeof STATUS)[keyof typeof STATUS];
|
||||||
|
|
||||||
export const LIMIT_TASK_NUM = 100;
|
export const LIMIT_TASK_NUM = 100;
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export const SORTABLE_COLUMN = {
|
|||||||
TranscriptionFinishedDate: "TRANSCRIPTION_FINISHED_DATE",
|
TranscriptionFinishedDate: "TRANSCRIPTION_FINISHED_DATE",
|
||||||
} as const;
|
} as const;
|
||||||
export type SortableColumnType =
|
export type SortableColumnType =
|
||||||
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
|
(typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN];
|
||||||
|
|
||||||
export const isSortableColumnType = (
|
export const isSortableColumnType = (
|
||||||
value: string
|
value: string
|
||||||
@ -36,14 +36,14 @@ export const isSortableColumnType = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SortableColumnList =
|
export type SortableColumnList =
|
||||||
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
|
(typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN];
|
||||||
|
|
||||||
export const DIRECTION = {
|
export const DIRECTION = {
|
||||||
ASC: "ASC",
|
ASC: "ASC",
|
||||||
DESC: "DESC",
|
DESC: "DESC",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION];
|
export type DirectionType = (typeof DIRECTION)[keyof typeof DIRECTION];
|
||||||
|
|
||||||
// DirectionTypeの型チェック関数
|
// DirectionTypeの型チェック関数
|
||||||
export const isDirectionType = (arg: string): arg is DirectionType =>
|
export const isDirectionType = (arg: string): arg is DirectionType =>
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import {
|
|||||||
playbackAsync,
|
playbackAsync,
|
||||||
updateAssigneeAsync,
|
updateAssigneeAsync,
|
||||||
cancelAsync,
|
cancelAsync,
|
||||||
|
deleteTaskAsync,
|
||||||
|
renameFileAsync,
|
||||||
} from "./operations";
|
} from "./operations";
|
||||||
import {
|
import {
|
||||||
SORTABLE_COLUMN,
|
SORTABLE_COLUMN,
|
||||||
@ -218,6 +220,25 @@ export const dictationSlice = createSlice({
|
|||||||
builder.addCase(backupTasksAsync.rejected, (state) => {
|
builder.addCase(backupTasksAsync.rejected, (state) => {
|
||||||
state.apps.isDownloading = false;
|
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.click();
|
||||||
a.parentNode?.removeChild(a);
|
a.parentNode?.removeChild(a);
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// バックアップ済みに更新
|
||||||
await tasksApi.backup(task.audioFileId, {
|
try {
|
||||||
headers: { authorization: `Bearer ${accessToken}` },
|
// 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 {};
|
return {};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// e ⇒ errorObjectに変換"
|
// e ⇒ errorObjectに変換
|
||||||
const error = createErrorObject(e);
|
const error = createErrorObject(e);
|
||||||
|
if (error.code === "E010603") {
|
||||||
|
// 存在しない音声ファイルをダウンロードしようとした場合
|
||||||
|
thunkApi.dispatch(
|
||||||
|
openSnackbar({
|
||||||
|
level: "error",
|
||||||
|
message: getTranslationID(
|
||||||
|
"dictationPage.message.fileAlreadyDeletedError"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return thunkApi.rejectWithValue({ error });
|
||||||
|
}
|
||||||
|
|
||||||
thunkApi.dispatch(
|
thunkApi.dispatch(
|
||||||
openSnackbar({
|
openSnackbar({
|
||||||
level: "error",
|
level: "error",
|
||||||
@ -592,3 +617,143 @@ export const backupTasksAsync = createAsyncThunk<
|
|||||||
return thunkApi.rejectWithValue({ error });
|
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 { createSlice } from "@reduxjs/toolkit";
|
||||||
import { LicenseCardActivateState } from "./state";
|
import { LicenseCardActivateState } from "./state";
|
||||||
|
import { activateCardLicenseAsync } from "./operations";
|
||||||
|
|
||||||
const initialState: LicenseCardActivateState = {
|
const initialState: LicenseCardActivateState = {
|
||||||
apps: {
|
apps: {
|
||||||
@ -14,6 +15,17 @@ export const licenseCardActivateSlice = createSlice({
|
|||||||
state.apps = initialState.apps;
|
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;
|
export const { cleanupApps } = licenseCardActivateSlice.actions;
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import { LicenseSummaryState } from "./state";
|
import { LicenseSummaryState } from "./state";
|
||||||
import { getCompanyNameAsync, getLicenseSummaryAsync } from "./operations";
|
import {
|
||||||
|
getCompanyNameAsync,
|
||||||
|
getLicenseSummaryAsync,
|
||||||
|
updateRestrictionStatusAsync,
|
||||||
|
} from "./operations";
|
||||||
|
|
||||||
const initialState: LicenseSummaryState = {
|
const initialState: LicenseSummaryState = {
|
||||||
domain: {
|
domain: {
|
||||||
@ -35,12 +39,30 @@ export const licenseSummarySlice = createSlice({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
|
builder.addCase(getLicenseSummaryAsync.pending, (state) => {
|
||||||
|
state.apps.isLoading = true;
|
||||||
|
});
|
||||||
builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => {
|
builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => {
|
||||||
state.domain.licenseSummaryInfo = action.payload;
|
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) => {
|
builder.addCase(getCompanyNameAsync.fulfilled, (state, action) => {
|
||||||
state.domain.accountInfo.companyName = action.payload.companyName;
|
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,
|
GetCompanyNameResponse,
|
||||||
GetLicenseSummaryResponse,
|
GetLicenseSummaryResponse,
|
||||||
PartnerLicenseInfo,
|
PartnerLicenseInfo,
|
||||||
|
UpdateRestrictionStatusRequest,
|
||||||
} from "../../../api/api";
|
} from "../../../api/api";
|
||||||
import { Configuration } from "../../../api/configuration";
|
import { Configuration } from "../../../api/configuration";
|
||||||
import { ErrorObject, createErrorObject } from "../../../common/errors";
|
import { ErrorObject, createErrorObject } from "../../../common/errors";
|
||||||
@ -123,3 +124,58 @@ export const getCompanyNameAsync = createAsyncThunk<
|
|||||||
return thunkApi.rejectWithValue({ error });
|
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";
|
import { RootState } from "app/store";
|
||||||
|
|
||||||
// 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する
|
// 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する
|
||||||
export const selecLicenseSummaryInfo = (state: RootState) =>
|
export const selectLicenseSummaryInfo = (state: RootState) =>
|
||||||
state.licenseSummary.domain.licenseSummaryInfo;
|
state.licenseSummary.domain.licenseSummaryInfo;
|
||||||
|
|
||||||
export const selectCompanyName = (state: RootState) =>
|
export const selectCompanyName = (state: RootState) =>
|
||||||
state.licenseSummary.domain.accountInfo.companyName;
|
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 });
|
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 { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
import { PartnerLicenseInfo } from "api";
|
import { PartnerLicenseInfo } from "api";
|
||||||
import { PartnerLicensesState, HierarchicalElement } from "./state";
|
import { PartnerLicensesState, HierarchicalElement } from "./state";
|
||||||
import { getMyAccountAsync, getPartnerLicenseAsync } from "./operations";
|
import {
|
||||||
|
getMyAccountAsync,
|
||||||
|
getPartnerLicenseAsync,
|
||||||
|
switchParentAsync,
|
||||||
|
} from "./operations";
|
||||||
import { ACCOUNTS_VIEW_LIMIT } from "./constants";
|
import { ACCOUNTS_VIEW_LIMIT } from "./constants";
|
||||||
|
|
||||||
const initialState: PartnerLicensesState = {
|
const initialState: PartnerLicensesState = {
|
||||||
@ -12,6 +16,8 @@ const initialState: PartnerLicensesState = {
|
|||||||
tier: 0,
|
tier: 0,
|
||||||
country: "",
|
country: "",
|
||||||
delegationPermission: false,
|
delegationPermission: false,
|
||||||
|
autoFileDelete: false,
|
||||||
|
fileRetentionDays: 0,
|
||||||
},
|
},
|
||||||
total: 0,
|
total: 0,
|
||||||
ownPartnerLicense: {
|
ownPartnerLicense: {
|
||||||
@ -107,6 +113,15 @@ export const partnerLicenseSlice = createSlice({
|
|||||||
builder.addCase(getPartnerLicenseAsync.rejected, (state) => {
|
builder.addCase(getPartnerLicenseAsync.rejected, (state) => {
|
||||||
state.apps.isLoading = false;
|
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 {
|
export const {
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
AccountsApi,
|
AccountsApi,
|
||||||
CreatePartnerAccountRequest,
|
CreatePartnerAccountRequest,
|
||||||
GetPartnersResponse,
|
GetPartnersResponse,
|
||||||
|
DeletePartnerAccountRequest,
|
||||||
|
GetPartnerUsersResponse,
|
||||||
} from "../../api/api";
|
} from "../../api/api";
|
||||||
import { Configuration } from "../../api/configuration";
|
import { Configuration } from "../../api/configuration";
|
||||||
|
|
||||||
@ -116,3 +118,170 @@ export const getPartnerInfoAsync = createAsyncThunk<
|
|||||||
return thunkApi.rejectWithValue({ error });
|
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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { PartnerState } from "./state";
|
import { PartnerState } from "./state";
|
||||||
import { createPartnerAccountAsync, getPartnerInfoAsync } from "./operations";
|
import {
|
||||||
|
createPartnerAccountAsync,
|
||||||
|
getPartnerInfoAsync,
|
||||||
|
deletePartnerAccountAsync,
|
||||||
|
getPartnerUsersAsync,
|
||||||
|
editPartnerInfoAsync,
|
||||||
|
} from "./operations";
|
||||||
import { LIMIT_PARTNER_VIEW_NUM } from "./constants";
|
import { LIMIT_PARTNER_VIEW_NUM } from "./constants";
|
||||||
|
|
||||||
const initialState: PartnerState = {
|
const initialState: PartnerState = {
|
||||||
@ -17,6 +23,13 @@ const initialState: PartnerState = {
|
|||||||
adminName: "",
|
adminName: "",
|
||||||
email: "",
|
email: "",
|
||||||
},
|
},
|
||||||
|
editPartner: {
|
||||||
|
users: [],
|
||||||
|
id: 0,
|
||||||
|
companyName: "",
|
||||||
|
country: "",
|
||||||
|
selectedAdminId: 0,
|
||||||
|
},
|
||||||
limit: LIMIT_PARTNER_VIEW_NUM,
|
limit: LIMIT_PARTNER_VIEW_NUM,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -75,6 +88,37 @@ export const partnerSlice = createSlice({
|
|||||||
state.apps.delegatedAccountId = undefined;
|
state.apps.delegatedAccountId = undefined;
|
||||||
state.apps.delegatedCompanyName = 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) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(createPartnerAccountAsync.pending, (state) => {
|
builder.addCase(createPartnerAccountAsync.pending, (state) => {
|
||||||
@ -97,6 +141,37 @@ export const partnerSlice = createSlice({
|
|||||||
builder.addCase(getPartnerInfoAsync.rejected, (state) => {
|
builder.addCase(getPartnerInfoAsync.rejected, (state) => {
|
||||||
state.apps.isLoading = false;
|
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 {
|
export const {
|
||||||
@ -108,5 +183,9 @@ export const {
|
|||||||
savePageInfo,
|
savePageInfo,
|
||||||
changeDelegateAccount,
|
changeDelegateAccount,
|
||||||
cleanupDelegateAccount,
|
cleanupDelegateAccount,
|
||||||
|
changeEditPartner,
|
||||||
|
changeEditCompanyName,
|
||||||
|
changeSelectedAdminId,
|
||||||
|
cleanupPartnerAccount,
|
||||||
} = partnerSlice.actions;
|
} = partnerSlice.actions;
|
||||||
export default partnerSlice.reducer;
|
export default partnerSlice.reducer;
|
||||||
|
|||||||
@ -62,3 +62,17 @@ export const selectDelegatedAccountId = (state: RootState) =>
|
|||||||
state.partner.apps.delegatedAccountId;
|
state.partner.apps.delegatedAccountId;
|
||||||
export const selectDelegatedCompanyName = (state: RootState) =>
|
export const selectDelegatedCompanyName = (state: RootState) =>
|
||||||
state.partner.apps.delegatedCompanyName;
|
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 {
|
import {
|
||||||
CreatePartnerAccountRequest,
|
CreatePartnerAccountRequest,
|
||||||
GetPartnersResponse,
|
GetPartnersResponse,
|
||||||
|
PartnerUser,
|
||||||
} from "../../api/api";
|
} from "../../api/api";
|
||||||
|
|
||||||
export interface PartnerState {
|
export interface PartnerState {
|
||||||
@ -19,4 +20,11 @@ export interface Apps {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
delegatedAccountId?: number;
|
delegatedAccountId?: number;
|
||||||
delegatedCompanyName?: string;
|
delegatedCompanyName?: string;
|
||||||
|
editPartner: {
|
||||||
|
users: PartnerUser[];
|
||||||
|
id: number;
|
||||||
|
companyName: string;
|
||||||
|
country: string;
|
||||||
|
selectedAdminId: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
UsersApi,
|
UsersApi,
|
||||||
LicensesApi,
|
LicensesApi,
|
||||||
GetAllocatableLicensesResponse,
|
GetAllocatableLicensesResponse,
|
||||||
|
MultipleImportUser,
|
||||||
} from "../../api/api";
|
} from "../../api/api";
|
||||||
import { Configuration } from "../../api/configuration";
|
import { Configuration } from "../../api/configuration";
|
||||||
import { ErrorObject, createErrorObject } from "../../common/errors";
|
import { ErrorObject, createErrorObject } from "../../common/errors";
|
||||||
@ -383,3 +384,189 @@ export const deallocateLicenseAsync = createAsyncThunk<
|
|||||||
return thunkApi.rejectWithValue({ error });
|
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,
|
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 { User, AllocatableLicenseInfo } from "../../api/api";
|
||||||
import { AddUser, UpdateUser, LicenseAllocateUser } from "./types";
|
import { AddUser, UpdateUser, LicenseAllocateUser } from "./types";
|
||||||
|
|
||||||
@ -19,4 +20,6 @@ export interface Apps {
|
|||||||
selectedlicenseId: number;
|
selectedlicenseId: number;
|
||||||
hasPasswordMask: boolean;
|
hasPasswordMask: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
importFileName: string | undefined;
|
||||||
|
importUsers: CSVType[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,14 +54,14 @@ export interface LicenseAllocateUser {
|
|||||||
remaining?: number;
|
remaining?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RoleType = typeof USER_ROLES[keyof typeof USER_ROLES];
|
export type RoleType = (typeof USER_ROLES)[keyof typeof USER_ROLES];
|
||||||
|
|
||||||
// 受け取った値がUSER_ROLESの型であるかどうかを判定する
|
// 受け取った値がUSER_ROLESの型であるかどうかを判定する
|
||||||
export const isRoleType = (role: string): role is RoleType =>
|
export const isRoleType = (role: string): role is RoleType =>
|
||||||
Object.values(USER_ROLES).includes(role as RoleType);
|
Object.values(USER_ROLES).includes(role as RoleType);
|
||||||
|
|
||||||
export type LicenseStatusType =
|
export type LicenseStatusType =
|
||||||
typeof LICENSE_STATUS[keyof typeof LICENSE_STATUS];
|
(typeof LICENSE_STATUS)[keyof typeof LICENSE_STATUS];
|
||||||
|
|
||||||
// 受け取った値がLicenseStatusTypeの型であるかどうかを判定する
|
// 受け取った値がLicenseStatusTypeの型であるかどうかを判定する
|
||||||
export const isLicenseStatusType = (
|
export const isLicenseStatusType = (
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
import { USER_ROLES } from "components/auth/constants";
|
import { USER_ROLES } from "components/auth/constants";
|
||||||
|
import { CSVType } from "common/parser";
|
||||||
import { UsersState } from "./state";
|
import { UsersState } from "./state";
|
||||||
import {
|
import {
|
||||||
addUserAsync,
|
addUserAsync,
|
||||||
@ -7,6 +8,8 @@ import {
|
|||||||
updateUserAsync,
|
updateUserAsync,
|
||||||
getAllocatableLicensesAsync,
|
getAllocatableLicensesAsync,
|
||||||
deallocateLicenseAsync,
|
deallocateLicenseAsync,
|
||||||
|
deleteUserAsync,
|
||||||
|
importUsersAsync,
|
||||||
} from "./operations";
|
} from "./operations";
|
||||||
import { RoleType, UserView } from "./types";
|
import { RoleType, UserView } from "./types";
|
||||||
|
|
||||||
@ -60,6 +63,8 @@ const initialState: UsersState = {
|
|||||||
selectedlicenseId: 0,
|
selectedlicenseId: 0,
|
||||||
hasPasswordMask: false,
|
hasPasswordMask: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
importFileName: undefined,
|
||||||
|
importUsers: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -241,6 +246,21 @@ export const userSlice = createSlice({
|
|||||||
state.apps.licenseAllocateUser = initialState.apps.licenseAllocateUser;
|
state.apps.licenseAllocateUser = initialState.apps.licenseAllocateUser;
|
||||||
state.apps.selectedlicenseId = initialState.apps.selectedlicenseId;
|
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) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(listUsersAsync.pending, (state) => {
|
builder.addCase(listUsersAsync.pending, (state) => {
|
||||||
@ -290,6 +310,24 @@ export const userSlice = createSlice({
|
|||||||
builder.addCase(deallocateLicenseAsync.rejected, (state) => {
|
builder.addCase(deallocateLicenseAsync.rejected, (state) => {
|
||||||
state.apps.isLoading = false;
|
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,
|
changeLicenseAllocateUser,
|
||||||
changeSelectedlicenseId,
|
changeSelectedlicenseId,
|
||||||
cleanupLicenseAllocateInfo,
|
cleanupLicenseAllocateInfo,
|
||||||
|
changeImportFileName,
|
||||||
|
changeImportCsv,
|
||||||
|
cleanupImportUsers,
|
||||||
} = userSlice.actions;
|
} = userSlice.actions;
|
||||||
|
|
||||||
export default userSlice.reducer;
|
export default userSlice.reducer;
|
||||||
|
|||||||
@ -115,3 +115,78 @@ export const uploadTemplateAsync = createAsyncThunk<
|
|||||||
return thunkApi.rejectWithValue({ error });
|
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 { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
import { TemplateState } from "./state";
|
import { TemplateState } from "./state";
|
||||||
import { listTemplateAsync, uploadTemplateAsync } from "./operations";
|
import {
|
||||||
|
deleteTemplateAsync,
|
||||||
|
listTemplateAsync,
|
||||||
|
uploadTemplateAsync,
|
||||||
|
} from "./operations";
|
||||||
|
|
||||||
const initialState: TemplateState = {
|
const initialState: TemplateState = {
|
||||||
apps: {
|
apps: {
|
||||||
@ -45,6 +49,15 @@ export const templateSlice = createSlice({
|
|||||||
builder.addCase(uploadTemplateAsync.rejected, (state) => {
|
builder.addCase(uploadTemplateAsync.rejected, (state) => {
|
||||||
state.apps.isUploading = false;
|
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 });
|
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,
|
listTypistGroupsAsync,
|
||||||
listTypistsAsync,
|
listTypistsAsync,
|
||||||
updateTypistGroupAsync,
|
updateTypistGroupAsync,
|
||||||
|
deleteTypistGroupAsync,
|
||||||
} from "./operations";
|
} from "./operations";
|
||||||
|
|
||||||
const initialState: TypistGroupState = {
|
const initialState: TypistGroupState = {
|
||||||
@ -106,6 +107,15 @@ export const typistGroupSlice = createSlice({
|
|||||||
builder.addCase(updateTypistGroupAsync.rejected, (state) => {
|
builder.addCase(updateTypistGroupAsync.rejected, (state) => {
|
||||||
state.apps.isLoading = false;
|
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を作成する
|
// OPTION_ITEMS_DEFAULT_VALUE_TYPEからOptionItemDefaultValueTypeを作成する
|
||||||
export type OptionItemsDefaultValueType =
|
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型かどうかを判定する
|
// 受け取った値がOptionItemDefaultValueType型かどうかを判定する
|
||||||
export const isOptionItemDefaultValueType = (
|
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 { TIERS } from "components/auth/constants";
|
||||||
import { isApproveTier } from "features/auth";
|
import { isApproveTier } from "features/auth";
|
||||||
import { DeleteAccountPopup } from "./deleteAccountPopup";
|
import { DeleteAccountPopup } from "./deleteAccountPopup";
|
||||||
|
import { FileDeleteSettingPopup } from "./fileDeleteSettingPopup";
|
||||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||||
|
|
||||||
const AccountPage: React.FC = (): JSX.Element => {
|
const AccountPage: React.FC = (): JSX.Element => {
|
||||||
@ -40,10 +41,17 @@ const AccountPage: React.FC = (): JSX.Element => {
|
|||||||
const [isDeleteAccountPopupOpen, setIsDeleteAccountPopupOpen] =
|
const [isDeleteAccountPopupOpen, setIsDeleteAccountPopupOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
const [isFileDeleteSettingPopupOpen, setIsFileDeleteSettingPopupOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const onDeleteAccountOpen = useCallback(() => {
|
const onDeleteAccountOpen = useCallback(() => {
|
||||||
setIsDeleteAccountPopupOpen(true);
|
setIsDeleteAccountPopupOpen(true);
|
||||||
}, [setIsDeleteAccountPopupOpen]);
|
}, [setIsDeleteAccountPopupOpen]);
|
||||||
|
|
||||||
|
const onDeleteFileDeleteSettingOpen = useCallback(() => {
|
||||||
|
setIsFileDeleteSettingPopupOpen(true);
|
||||||
|
}, [setIsFileDeleteSettingPopupOpen]);
|
||||||
|
|
||||||
// 階層表示用
|
// 階層表示用
|
||||||
const tierNames: { [key: number]: string } = {
|
const tierNames: { [key: number]: string } = {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// 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}>
|
<div className={styles.wrap}>
|
||||||
<Header />
|
<Header />
|
||||||
<UpdateTokenTimer />
|
<UpdateTokenTimer />
|
||||||
@ -102,12 +117,13 @@ const AccountPage: React.FC = (): JSX.Element => {
|
|||||||
|
|
||||||
<section className={styles.account}>
|
<section className={styles.account}>
|
||||||
<div className={styles.boxFlex}>
|
<div className={styles.boxFlex}>
|
||||||
{/* File Delete Setting は現状不要のため非表示
|
|
||||||
<ul className={`${styles.menuAction} ${styles.box100}`}>
|
<ul className={`${styles.menuAction} ${styles.box100}`}>
|
||||||
<li>
|
<li>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||||
<a
|
<a
|
||||||
href="account_setting.html"
|
|
||||||
className={`${styles.menuLink} ${styles.isActive}`}
|
className={`${styles.menuLink} ${styles.isActive}`}
|
||||||
|
onClick={onDeleteFileDeleteSettingOpen}
|
||||||
|
data-tag="open-file-delete-setting-popup"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="images/file_delete.svg"
|
src="images/file_delete.svg"
|
||||||
@ -120,7 +136,6 @@ const AccountPage: React.FC = (): JSX.Element => {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
*/}
|
|
||||||
|
|
||||||
<div className={styles.marginRgt3}>
|
<div className={styles.marginRgt3}>
|
||||||
<dl className={styles.listVertical}>
|
<dl className={styles.listVertical}>
|
||||||
@ -216,9 +231,23 @@ const AccountPage: React.FC = (): JSX.Element => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
|
<dd
|
||||||
|
style={{ paddingBottom: 0 }}
|
||||||
|
className={`${styles.full}`}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isTier5 && <dd>-</dd>}
|
{!isTier5 && <dd>-</dd>}
|
||||||
|
<dt>
|
||||||
|
{t(
|
||||||
|
getTranslationID("accountPage.label.fileRetentionDays")
|
||||||
|
)}
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{viewInfo.account.autoFileDelete
|
||||||
|
? viewInfo.account.fileRetentionDays
|
||||||
|
: "-"}
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</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 styles from "styles/app.module.scss";
|
||||||
import { useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
selectSelectedFileTask,
|
selectSelectedFileTask,
|
||||||
selectIsLoading,
|
selectIsLoading,
|
||||||
PRIORITY,
|
PRIORITY,
|
||||||
|
renameFileAsync,
|
||||||
} from "features/dictation";
|
} from "features/dictation";
|
||||||
import { getTranslationID } from "translation";
|
import { getTranslationID } from "translation";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AppDispatch } from "app/store";
|
||||||
import close from "../../assets/images/close.svg";
|
import close from "../../assets/images/close.svg";
|
||||||
import lock from "../../assets/images/lock.svg";
|
import lock from "../../assets/images/lock.svg";
|
||||||
|
|
||||||
@ -19,14 +21,55 @@ interface FilePropertyPopupProps {
|
|||||||
export const FilePropertyPopup: React.FC<FilePropertyPopupProps> = (props) => {
|
export const FilePropertyPopup: React.FC<FilePropertyPopupProps> = (props) => {
|
||||||
const { onClose, isOpen } = props;
|
const { onClose, isOpen } = props;
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
const dispatch: AppDispatch = useDispatch();
|
||||||
const isLoading = useSelector(selectIsLoading);
|
const isLoading = useSelector(selectIsLoading);
|
||||||
|
|
||||||
|
const [isPushSaveButton, setIsPushSaveButton] = useState<boolean>(false);
|
||||||
|
|
||||||
// ポップアップを閉じる処理
|
// ポップアップを閉じる処理
|
||||||
const closePopup = useCallback(() => {
|
const closePopup = useCallback(() => {
|
||||||
|
setIsPushSaveButton(false);
|
||||||
onClose(false);
|
onClose(false);
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
const selectedFileTask = useSelector(selectSelectedFileTask);
|
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 (
|
return (
|
||||||
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
|
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
|
||||||
<div className={styles.modalBox}>
|
<div className={styles.modalBox}>
|
||||||
@ -45,7 +88,41 @@ export const FilePropertyPopup: React.FC<FilePropertyPopupProps> = (props) => {
|
|||||||
{t(getTranslationID("filePropertyPopup.label.general"))}
|
{t(getTranslationID("filePropertyPopup.label.general"))}
|
||||||
</dt>
|
</dt>
|
||||||
<dt>{t(getTranslationID("dictationPage.label.fileName"))}</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>
|
<dt>{t(getTranslationID("dictationPage.label.fileSize"))}</dt>
|
||||||
<dd>{selectedFileTask?.fileSize ?? ""}</dd>
|
<dd>{selectedFileTask?.fileSize ?? ""}</dd>
|
||||||
<dt>{t(getTranslationID("dictationPage.label.fileLength"))}</dt>
|
<dt>{t(getTranslationID("dictationPage.label.fileLength"))}</dt>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import {
|
|||||||
playbackAsync,
|
playbackAsync,
|
||||||
cancelAsync,
|
cancelAsync,
|
||||||
PRIORITY,
|
PRIORITY,
|
||||||
|
deleteTaskAsync,
|
||||||
isSortableColumnType,
|
isSortableColumnType,
|
||||||
isDirectionType,
|
isDirectionType,
|
||||||
} from "features/dictation";
|
} from "features/dictation";
|
||||||
@ -63,6 +64,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
|||||||
const isTypist = isTypistUser();
|
const isTypist = isTypistUser();
|
||||||
const isNone = !isAuthor && !isTypist;
|
const isNone = !isAuthor && !isTypist;
|
||||||
|
|
||||||
|
const isDeletableRole = isAdmin || isAuthor;
|
||||||
|
|
||||||
// popup制御関係
|
// popup制御関係
|
||||||
const [
|
const [
|
||||||
isChangeTranscriptionistPopupOpen,
|
isChangeTranscriptionistPopupOpen,
|
||||||
@ -496,9 +499,39 @@ const DictationPage: React.FC = (): JSX.Element => {
|
|||||||
setIsBackupPopupOpen(true);
|
setIsBackupPopupOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onCloseFilePropertyPopup = useCallback(() => {
|
const onCloseFilePropertyPopup = useCallback(
|
||||||
setIsFilePropertyPopupOpen(false);
|
(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 = (
|
const sortIconClass = (
|
||||||
currentParam: SortableColumnType,
|
currentParam: SortableColumnType,
|
||||||
@ -514,6 +547,53 @@ const DictationPage: React.FC = (): JSX.Element => {
|
|||||||
return styles.isActiveAz;
|
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(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -1183,17 +1263,26 @@ const DictationPage: React.FC = (): JSX.Element => {
|
|||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{/* タスク削除はCCB後回し分なので今は非表示
|
|
||||||
<li>
|
<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(
|
{t(
|
||||||
getTranslationID(
|
getTranslationID(
|
||||||
"dictationPage.label.deleteDictation"
|
"dictationPage.label.deleteDictation"
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
*/}
|
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
{displayColumn.JobNumber && (
|
{displayColumn.JobNumber && (
|
||||||
@ -1252,9 +1341,7 @@ const DictationPage: React.FC = (): JSX.Element => {
|
|||||||
<td className={styles.clm6}>{x.workType}</td>
|
<td className={styles.clm6}>{x.workType}</td>
|
||||||
)}
|
)}
|
||||||
{displayColumn.FileName && (
|
{displayColumn.FileName && (
|
||||||
<td className={styles.clm7}>
|
<td className={styles.clm7}>{x.fileName}</td>
|
||||||
{x.fileName.replace(".zip", "")}
|
|
||||||
</td>
|
|
||||||
)}
|
)}
|
||||||
{displayColumn.FileLength && (
|
{displayColumn.FileLength && (
|
||||||
<td className={styles.clm8}>{x.audioDuration}</td>
|
<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(() => {
|
const closePopup = useCallback(() => {
|
||||||
|
if (isLoading) return;
|
||||||
setIsPushOrderButton(false);
|
setIsPushOrderButton(false);
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose]);
|
}, [isLoading, onClose]);
|
||||||
|
|
||||||
// 画面からのパラメータ
|
// 画面からのパラメータ
|
||||||
const poNumber = useSelector(selectPoNumber);
|
const poNumber = useSelector(selectPoNumber);
|
||||||
|
|||||||
@ -10,12 +10,16 @@ import { useDispatch, useSelector } from "react-redux";
|
|||||||
import {
|
import {
|
||||||
getCompanyNameAsync,
|
getCompanyNameAsync,
|
||||||
getLicenseSummaryAsync,
|
getLicenseSummaryAsync,
|
||||||
selecLicenseSummaryInfo,
|
selectLicenseSummaryInfo,
|
||||||
selectCompanyName,
|
selectCompanyName,
|
||||||
|
selectIsLoading,
|
||||||
|
updateRestrictionStatusAsync,
|
||||||
} from "features/license/licenseSummary";
|
} from "features/license/licenseSummary";
|
||||||
import { selectSelectedRow } from "features/license/partnerLicense";
|
import { selectSelectedRow } from "features/license/partnerLicense";
|
||||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||||
import { DelegationBar } from "components/delegate";
|
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 postAdd from "../../assets/images/post_add.svg";
|
||||||
import history from "../../assets/images/history.svg";
|
import history from "../../assets/images/history.svg";
|
||||||
import key from "../../assets/images/key.svg";
|
import key from "../../assets/images/key.svg";
|
||||||
@ -40,6 +44,8 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
|||||||
// 代行操作用のトークンを取得する
|
// 代行操作用のトークンを取得する
|
||||||
const delegationAccessToken = useSelector(selectDelegationAccessToken);
|
const delegationAccessToken = useSelector(selectDelegationAccessToken);
|
||||||
|
|
||||||
|
const isLoading = useSelector(selectIsLoading);
|
||||||
|
|
||||||
// popup制御関係
|
// popup制御関係
|
||||||
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
|
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
|
||||||
const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] =
|
const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] =
|
||||||
@ -62,9 +68,12 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
|||||||
}, [setIsLicenseOrderHistoryOpen]);
|
}, [setIsLicenseOrderHistoryOpen]);
|
||||||
|
|
||||||
// apiからの値取得関係
|
// apiからの値取得関係
|
||||||
const licenseSummaryInfo = useSelector(selecLicenseSummaryInfo);
|
const licenseSummaryInfo = useSelector(selectLicenseSummaryInfo);
|
||||||
const companyName = useSelector(selectCompanyName);
|
const companyName = useSelector(selectCompanyName);
|
||||||
|
|
||||||
|
const isTier1 = isApproveTier([TIERS.TIER1]);
|
||||||
|
const isAdmin = isAdminUser();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getLicenseSummaryAsync({ selectedRow }));
|
dispatch(getLicenseSummaryAsync({ selectedRow }));
|
||||||
dispatch(getCompanyNameAsync({ selectedRow }));
|
dispatch(getCompanyNameAsync({ selectedRow }));
|
||||||
@ -78,6 +87,35 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
|||||||
}
|
}
|
||||||
}, [onReturn]);
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* isPopupOpenがfalseの場合はポップアップのhtmlを生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */}
|
{/* isPopupOpenがfalseの場合はポップアップのhtmlを生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */}
|
||||||
@ -272,6 +310,27 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<dl
|
||||||
className={`${styles.listVertical} ${styles.marginBtm3}`}
|
className={`${styles.listVertical} ${styles.marginBtm3}`}
|
||||||
>
|
>
|
||||||
@ -289,17 +348,31 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</dt>
|
</dt>
|
||||||
{/* Storage Usedの値表示をハイフンに置き換え */}
|
<dd>
|
||||||
{/* <dd>{licenseSummaryInfo.storageSize}GB</dd> */}
|
{/** Byte単位で受け取った値をGB単位で表示するため1000^3で割っている(小数点以下第三位まで表示で第四位で四捨五入) */}
|
||||||
<dd>-</dd>
|
{(
|
||||||
|
licenseSummaryInfo.storageSize /
|
||||||
|
1000 /
|
||||||
|
1000 /
|
||||||
|
1000
|
||||||
|
).toFixed(3)}
|
||||||
|
GB
|
||||||
|
</dd>
|
||||||
<dt>
|
<dt>
|
||||||
{t(
|
{t(
|
||||||
getTranslationID("LicenseSummaryPage.label.usedSize")
|
getTranslationID("LicenseSummaryPage.label.usedSize")
|
||||||
)}
|
)}
|
||||||
</dt>
|
</dt>
|
||||||
{/* Storage Usedの値表示をハイフンに置き換え */}
|
<dd>
|
||||||
{/* <dd>{licenseSummaryInfo.usedSize}GB</dd> */}
|
{/** Byte単位で受け取った値をGB単位で表示するため1000^3で割っている(小数点以下第三位まで表示で第四位で四捨五入) */}
|
||||||
<dd>-</dd>
|
{(
|
||||||
|
licenseSummaryInfo.usedSize /
|
||||||
|
1000 /
|
||||||
|
1000 /
|
||||||
|
1000
|
||||||
|
).toFixed(3)}
|
||||||
|
GB
|
||||||
|
</dd>
|
||||||
<dt className={styles.overLine}>
|
<dt className={styles.overLine}>
|
||||||
{t(
|
{t(
|
||||||
getTranslationID(
|
getTranslationID(
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup";
|
|||||||
import postAdd from "../../assets/images/post_add.svg";
|
import postAdd from "../../assets/images/post_add.svg";
|
||||||
import history from "../../assets/images/history.svg";
|
import history from "../../assets/images/history.svg";
|
||||||
import returnLabel from "../../assets/images/undo.svg";
|
import returnLabel from "../../assets/images/undo.svg";
|
||||||
|
import changeOwnerIcon from "../../assets/images/change_circle.svg";
|
||||||
import { isApproveTier } from "../../features/auth/utils";
|
import { isApproveTier } from "../../features/auth/utils";
|
||||||
import { TIERS } from "../../components/auth/constants";
|
import { TIERS } from "../../components/auth/constants";
|
||||||
import {
|
import {
|
||||||
@ -37,6 +38,7 @@ import { LicenseOrderPopup } from "./licenseOrderPopup";
|
|||||||
import { LicenseOrderHistory } from "./licenseOrderHistory";
|
import { LicenseOrderHistory } from "./licenseOrderHistory";
|
||||||
import { LicenseSummary } from "./licenseSummary";
|
import { LicenseSummary } from "./licenseSummary";
|
||||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||||
|
import ChangeOwnerPopup from "./changeOwnerPopup";
|
||||||
|
|
||||||
const PartnerLicense: React.FC = (): JSX.Element => {
|
const PartnerLicense: React.FC = (): JSX.Element => {
|
||||||
const dispatch: AppDispatch = useDispatch();
|
const dispatch: AppDispatch = useDispatch();
|
||||||
@ -49,6 +51,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
|||||||
const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] =
|
const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false);
|
const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false);
|
||||||
|
const [isChangeOwnerPopupOpen, setIsChangeOwnerPopupOpen] = useState(false);
|
||||||
|
|
||||||
// 階層表示用
|
// 階層表示用
|
||||||
const tierNames: { [key: number]: string } = {
|
const tierNames: { [key: number]: string } = {
|
||||||
@ -148,6 +151,11 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
|||||||
[dispatch, setIslicenseOrderHistoryOpen]
|
[dispatch, setIslicenseOrderHistoryOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// changeOwnerボタン押下時
|
||||||
|
const onClickChangeOwner = useCallback(() => {
|
||||||
|
setIsChangeOwnerPopupOpen(true);
|
||||||
|
}, [setIsChangeOwnerPopupOpen]);
|
||||||
|
|
||||||
// マウント時のみ実行
|
// マウント時のみ実行
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getMyAccountAsync());
|
dispatch(getMyAccountAsync());
|
||||||
@ -245,6 +253,13 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isChangeOwnerPopupOpen && (
|
||||||
|
<ChangeOwnerPopup
|
||||||
|
onClose={() => {
|
||||||
|
setIsChangeOwnerPopupOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
|
{!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<Header />
|
<Header />
|
||||||
@ -329,6 +344,26 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
|||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</li>
|
</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>
|
||||||
<ul className={styles.brCrumbLicense}>
|
<ul className={styles.brCrumbLicense}>
|
||||||
{hierarchicalElements.map((value) => (
|
{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;
|
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,
|
selectTotalPage,
|
||||||
getPartnerInfoAsync,
|
getPartnerInfoAsync,
|
||||||
selectPartnersInfo,
|
selectPartnersInfo,
|
||||||
|
deletePartnerAccountAsync,
|
||||||
} from "features/partner/index";
|
} from "features/partner/index";
|
||||||
import {
|
import {
|
||||||
changeDelegateAccount,
|
changeDelegateAccount,
|
||||||
|
changeEditPartner,
|
||||||
savePageInfo,
|
savePageInfo,
|
||||||
} from "features/partner/partnerSlice";
|
} from "features/partner/partnerSlice";
|
||||||
import { getTranslationID } from "translation";
|
import { getTranslationID } from "translation";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getDelegationTokenAsync } from "features/auth/operations";
|
import { getDelegationTokenAsync } from "features/auth/operations";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Partner } from "api";
|
||||||
import personAdd from "../../assets/images/person_add.svg";
|
import personAdd from "../../assets/images/person_add.svg";
|
||||||
import { TIERS } from "../../components/auth/constants";
|
import { TIERS } from "../../components/auth/constants";
|
||||||
import { AddPartnerAccountPopup } from "./addPartnerAccountPopup";
|
import { AddPartnerAccountPopup } from "./addPartnerAccountPopup";
|
||||||
|
import { EditPartnerAccountPopup } from "./editPartnerAccountPopup";
|
||||||
import checkFill from "../../assets/images/check_fill.svg";
|
import checkFill from "../../assets/images/check_fill.svg";
|
||||||
|
|
||||||
const PartnerPage: React.FC = (): JSX.Element => {
|
const PartnerPage: React.FC = (): JSX.Element => {
|
||||||
const dispatch: AppDispatch = useDispatch();
|
const dispatch: AppDispatch = useDispatch();
|
||||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||||
|
const [isEditPopupOpen, setIsEditPopupOpen] = useState(false);
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const total = useSelector(selectTotal);
|
const total = useSelector(selectTotal);
|
||||||
@ -71,6 +76,19 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
|||||||
const onOpen = useCallback(() => {
|
const onOpen = useCallback(() => {
|
||||||
setIsPopupOpen(true);
|
setIsPopupOpen(true);
|
||||||
}, [setIsPopupOpen]);
|
}, [setIsPopupOpen]);
|
||||||
|
const onOpenEditPopup = useCallback(
|
||||||
|
(editPartner: Partner) => {
|
||||||
|
dispatch(
|
||||||
|
changeEditPartner({
|
||||||
|
id: editPartner.accountId,
|
||||||
|
companyName: editPartner.name,
|
||||||
|
country: editPartner.country,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setIsEditPopupOpen(true);
|
||||||
|
},
|
||||||
|
[setIsEditPopupOpen, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
// パートナー取得APIを呼び出す
|
// パートナー取得APIを呼び出す
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -109,6 +127,31 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
|||||||
[dispatch, navigate, t]
|
[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
|
// HTML
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -118,6 +161,12 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
|||||||
setIsPopupOpen(false);
|
setIsPopupOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<EditPartnerAccountPopup
|
||||||
|
isOpen={isEditPopupOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsEditPopupOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<Header />
|
<Header />
|
||||||
<UpdateTokenTimer />
|
<UpdateTokenTimer />
|
||||||
@ -185,10 +234,30 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td className={styles.clm0}>
|
<td className={styles.clm0}>
|
||||||
<ul className={styles.menuInTable}>
|
<ul className={styles.menuInTable}>
|
||||||
{/* パートナーアカウント削除はCCB後回し分なので非表示
|
|
||||||
{isVisibleButton && (
|
{isVisibleButton && (
|
||||||
<li>
|
<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(
|
{t(
|
||||||
getTranslationID(
|
getTranslationID(
|
||||||
"partnerPage.label.deleteAccount"
|
"partnerPage.label.deleteAccount"
|
||||||
@ -197,7 +266,6 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
*/}
|
|
||||||
{isVisibleDealerManagement && (
|
{isVisibleDealerManagement && (
|
||||||
<li>
|
<li>
|
||||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
{/* 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 { useDispatch, useSelector } from "react-redux";
|
||||||
import { AppDispatch } from "app/store";
|
import { AppDispatch } from "app/store";
|
||||||
import Header from "components/header";
|
import Header from "components/header";
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
selectTemplates,
|
selectTemplates,
|
||||||
listTemplateAsync,
|
listTemplateAsync,
|
||||||
selectIsLoading,
|
selectIsLoading,
|
||||||
|
deleteTemplateAsync,
|
||||||
} from "features/workflow/template";
|
} from "features/workflow/template";
|
||||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||||
import { DelegationBar } from "components/delegate";
|
import { DelegationBar } from "components/delegate";
|
||||||
@ -35,6 +36,23 @@ export const TemplateFilePage: React.FC = () => {
|
|||||||
dispatch(listTemplateAsync());
|
dispatch(listTemplateAsync());
|
||||||
}, [dispatch]);
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{isShowAddPopup && (
|
{isShowAddPopup && (
|
||||||
@ -101,16 +119,17 @@ export const TemplateFilePage: React.FC = () => {
|
|||||||
<td>{template.name}</td>
|
<td>{template.name}</td>
|
||||||
<td>
|
<td>
|
||||||
<ul className={`${styles.menuAction} ${styles.inTable}`}>
|
<ul className={`${styles.menuAction} ${styles.inTable}`}>
|
||||||
{/* テンプレートファイル削除はCCB後回し分なので非表示
|
|
||||||
<li>
|
<li>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||||
<a
|
<a
|
||||||
href=""
|
|
||||||
className={`${styles.menuLink} ${styles.isActive}`}
|
className={`${styles.menuLink} ${styles.isActive}`}
|
||||||
|
onClick={() => {
|
||||||
|
onDeleteTemplate(template.id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t(getTranslationID("common.label.delete"))}
|
{t(getTranslationID("common.label.delete"))}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
*/}
|
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
selectTypistGroups,
|
selectTypistGroups,
|
||||||
selectIsLoading,
|
selectIsLoading,
|
||||||
listTypistGroupsAsync,
|
listTypistGroupsAsync,
|
||||||
|
deleteTypistGroupAsync,
|
||||||
} from "features/workflow/typistGroup";
|
} from "features/workflow/typistGroup";
|
||||||
import { AppDispatch } from "app/store";
|
import { AppDispatch } from "app/store";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -47,6 +48,25 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
|
|||||||
[setIsEditPopupOpen]
|
[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(() => {
|
useEffect(() => {
|
||||||
dispatch(listTypistGroupsAsync());
|
dispatch(listTypistGroupsAsync());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@ -142,6 +162,17 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
|
|||||||
{t(getTranslationID("common.label.edit"))}
|
{t(getTranslationID("common.label.edit"))}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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,
|
selectUserViews,
|
||||||
selectIsLoading,
|
selectIsLoading,
|
||||||
deallocateLicenseAsync,
|
deallocateLicenseAsync,
|
||||||
|
deleteUserAsync,
|
||||||
} from "features/user";
|
} from "features/user";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getTranslationID } from "translation";
|
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 checkFill from "../../assets/images/check_fill.svg";
|
||||||
import checkOutline from "../../assets/images/check_outline.svg";
|
import checkOutline from "../../assets/images/check_outline.svg";
|
||||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||||
|
import upload from "../../assets/images/upload.svg";
|
||||||
import { UserAddPopup } from "./popup";
|
import { UserAddPopup } from "./popup";
|
||||||
import { UserUpdatePopup } from "./updatePopup";
|
import { UserUpdatePopup } from "./updatePopup";
|
||||||
import { AllocateLicensePopup } from "./allocateLicensePopup";
|
import { AllocateLicensePopup } from "./allocateLicensePopup";
|
||||||
|
import { ImportPopup } from "./importPopup";
|
||||||
|
|
||||||
const UserListPage: React.FC = (): JSX.Element => {
|
const UserListPage: React.FC = (): JSX.Element => {
|
||||||
const dispatch: AppDispatch = useDispatch();
|
const dispatch: AppDispatch = useDispatch();
|
||||||
@ -45,6 +48,7 @@ const UserListPage: React.FC = (): JSX.Element => {
|
|||||||
const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false);
|
const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false);
|
||||||
const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] =
|
const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [isImportPopupOpen, setIsImportPopupOpen] = useState(false);
|
||||||
|
|
||||||
const onOpen = useCallback(() => {
|
const onOpen = useCallback(() => {
|
||||||
setIsPopupOpen(true);
|
setIsPopupOpen(true);
|
||||||
@ -65,6 +69,9 @@ const UserListPage: React.FC = (): JSX.Element => {
|
|||||||
},
|
},
|
||||||
[setIsAllocateLicensePopupOpen, dispatch]
|
[setIsAllocateLicensePopupOpen, dispatch]
|
||||||
);
|
);
|
||||||
|
const onImportPopupOpen = useCallback(() => {
|
||||||
|
setIsImportPopupOpen(true);
|
||||||
|
}, [setIsImportPopupOpen]);
|
||||||
|
|
||||||
const onLicenseDeallocation = useCallback(
|
const onLicenseDeallocation = useCallback(
|
||||||
async (userId: number) => {
|
async (userId: number) => {
|
||||||
@ -84,6 +91,24 @@ const UserListPage: React.FC = (): JSX.Element => {
|
|||||||
[dispatch, t]
|
[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(() => {
|
useEffect(() => {
|
||||||
// ユーザ一覧取得処理を呼び出す
|
// ユーザ一覧取得処理を呼び出す
|
||||||
dispatch(listUsersAsync());
|
dispatch(listUsersAsync());
|
||||||
@ -115,6 +140,12 @@ const UserListPage: React.FC = (): JSX.Element => {
|
|||||||
setIsAllocateLicensePopupOpen(false);
|
setIsAllocateLicensePopupOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ImportPopup
|
||||||
|
isOpen={isImportPopupOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsImportPopupOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className={`${styles.wrap} ${
|
className={`${styles.wrap} ${
|
||||||
delegationAccessToken ? styles.manage : ""
|
delegationAccessToken ? styles.manage : ""
|
||||||
@ -146,6 +177,16 @@ const UserListPage: React.FC = (): JSX.Element => {
|
|||||||
{t(getTranslationID("userListPage.label.addUser"))}
|
{t(getTranslationID("userListPage.label.addUser"))}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
<div className={styles.tableWrap}>
|
<div className={styles.tableWrap}>
|
||||||
<table className={`${styles.table} ${styles.user}`}>
|
<table className={`${styles.table} ${styles.user}`}>
|
||||||
@ -255,9 +296,13 @@ const UserListPage: React.FC = (): JSX.Element => {
|
|||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* ユーザー削除 CCB後回し分なので今は非表示
|
|
||||||
<li>
|
<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(
|
{t(
|
||||||
getTranslationID(
|
getTranslationID(
|
||||||
"userListPage.label.deleteUser"
|
"userListPage.label.deleteUser"
|
||||||
@ -265,7 +310,6 @@ const UserListPage: React.FC = (): JSX.Element => {
|
|||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
*/}
|
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
<td> {user.name}</td>
|
<td> {user.name}</td>
|
||||||
|
|||||||
@ -15,11 +15,8 @@ const UserVerifyPage: React.FC = (): JSX.Element => {
|
|||||||
const jwt = query.get("verify") ?? "";
|
const jwt = query.get("verify") ?? "";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!jwt) {
|
|
||||||
navigate("/mail-confirm/failed");
|
|
||||||
}
|
|
||||||
dispatch(userVerifyAsync({ jwt }));
|
dispatch(userVerifyAsync({ jwt }));
|
||||||
}, [navigate, dispatch, jwt]);
|
}, [dispatch, jwt]);
|
||||||
|
|
||||||
const verifyState = useSelector(VerifyStateSelector);
|
const verifyState = useSelector(VerifyStateSelector);
|
||||||
|
|
||||||
|
|||||||
@ -1630,6 +1630,43 @@ _:-ms-lang(x)::-ms-backdrop,
|
|||||||
margin-bottom: 5rem;
|
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 {
|
.account .listVertical {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
@ -1857,6 +1894,18 @@ tr.isSelected .menuInTable li a.isDisable {
|
|||||||
cursor: pointer;
|
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"] {
|
.license .listVertical dd img[src*="circle"] {
|
||||||
filter: brightness(0) saturate(100%) invert(58%) sepia(41%) saturate(5814%)
|
filter: brightness(0) saturate(100%) invert(58%) sepia(41%) saturate(5814%)
|
||||||
hue-rotate(143deg) brightness(96%) contrast(101%);
|
hue-rotate(143deg) brightness(96%) contrast(101%);
|
||||||
@ -1950,6 +1999,61 @@ tr.isSelected .menuInTable li a.isDisable {
|
|||||||
text-align: right;
|
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 {
|
.dictation .menuAction {
|
||||||
margin-top: -1rem;
|
margin-top: -1rem;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
@ -2272,6 +2376,9 @@ tr.isSelected .menuInTable li a.isDisable {
|
|||||||
.formList.property dt:not(.formTitle):nth-of-type(odd) + dd {
|
.formList.property dt:not(.formTitle):nth-of-type(odd) + dd {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
.formList.property dt:has(+ dd.hasInput) {
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
}
|
||||||
.formList.property dd {
|
.formList.property dd {
|
||||||
width: 58%;
|
width: 58%;
|
||||||
padding: 0.2rem 4% 0.2rem 0;
|
padding: 0.2rem 4% 0.2rem 0;
|
||||||
@ -2283,6 +2390,16 @@ tr.isSelected .menuInTable li a.isDisable {
|
|||||||
.formList.property dd img {
|
.formList.property dd img {
|
||||||
height: 1.1rem;
|
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 {
|
.formList.property dd.full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.2rem 4% 0.2rem 4%;
|
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 clm0: "clm0";
|
||||||
readonly menuInTable: "menuInTable";
|
readonly menuInTable: "menuInTable";
|
||||||
readonly isSelected: "isSelected";
|
readonly isSelected: "isSelected";
|
||||||
|
readonly userImport: "userImport";
|
||||||
|
readonly menuLink: "menuLink";
|
||||||
readonly odd: "odd";
|
readonly odd: "odd";
|
||||||
readonly alignRight: "alignRight";
|
readonly alignRight: "alignRight";
|
||||||
readonly menuAction: "menuAction";
|
readonly menuAction: "menuAction";
|
||||||
readonly inTable: "inTable";
|
readonly inTable: "inTable";
|
||||||
readonly menuLink: "menuLink";
|
|
||||||
readonly menuIcon: "menuIcon";
|
readonly menuIcon: "menuIcon";
|
||||||
readonly colorLink: "colorLink";
|
readonly colorLink: "colorLink";
|
||||||
readonly isDisable: "isDisable";
|
readonly isDisable: "isDisable";
|
||||||
@ -123,10 +124,18 @@ declare const classNames: {
|
|||||||
readonly txNormal: "txNormal";
|
readonly txNormal: "txNormal";
|
||||||
readonly manageIcon: "manageIcon";
|
readonly manageIcon: "manageIcon";
|
||||||
readonly manageIconClose: "manageIconClose";
|
readonly manageIconClose: "manageIconClose";
|
||||||
|
readonly checkAvail: "checkAvail";
|
||||||
readonly history: "history";
|
readonly history: "history";
|
||||||
readonly cardHistory: "cardHistory";
|
readonly cardHistory: "cardHistory";
|
||||||
readonly partner: "partner";
|
readonly partner: "partner";
|
||||||
readonly isOpen: "isOpen";
|
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 alignLeft: "alignLeft";
|
||||||
readonly displayOptions: "displayOptions";
|
readonly displayOptions: "displayOptions";
|
||||||
readonly tableFilter: "tableFilter";
|
readonly tableFilter: "tableFilter";
|
||||||
@ -192,6 +201,7 @@ declare const classNames: {
|
|||||||
readonly hideO10: "hideO10";
|
readonly hideO10: "hideO10";
|
||||||
readonly op10: "op10";
|
readonly op10: "op10";
|
||||||
readonly property: "property";
|
readonly property: "property";
|
||||||
|
readonly hasInput: "hasInput";
|
||||||
readonly formChange: "formChange";
|
readonly formChange: "formChange";
|
||||||
readonly chooseMember: "chooseMember";
|
readonly chooseMember: "chooseMember";
|
||||||
readonly holdMember: "holdMember";
|
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 install default-jre -y \
|
||||||
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
|
&& 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
|
# Update NPM
|
||||||
RUN npm install -g npm
|
RUN npm install -g npm
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,15 @@ services:
|
|||||||
- CHOKIDAR_USEPOLLING=true
|
- CHOKIDAR_USEPOLLING=true
|
||||||
networks:
|
networks:
|
||||||
- external
|
- 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:
|
networks:
|
||||||
external:
|
external:
|
||||||
name: omds_network
|
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_HOST=omds-mysql
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_NAME=omds
|
DB_NAME=omds
|
||||||
|
DB_NAME_CCB=omds_ccb
|
||||||
DB_USERNAME=omdsdbuser
|
DB_USERNAME=omdsdbuser
|
||||||
DB_PASSWORD=omdsdbpass
|
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
|
*.Cache
|
||||||
project.lock.json
|
project.lock.json
|
||||||
|
|
||||||
|
.test/
|
||||||
|
|
||||||
/packages
|
/packages
|
||||||
/TestResults
|
/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",
|
"clean": "rimraf dist",
|
||||||
"prestart": "npm run clean && npm run build",
|
"prestart": "npm run clean && npm run build",
|
||||||
"start": "func start",
|
"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": {
|
"dependencies": {
|
||||||
"@azure/functions": "^4.0.0",
|
"@azure/functions": "^4.0.0",
|
||||||
"@azure/identity": "^3.1.3",
|
"@azure/identity": "^3.1.3",
|
||||||
|
"@azure/storage-blob": "^12.17.0",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.5",
|
"@microsoft/microsoft-graph-client": "^3.0.5",
|
||||||
"@sendgrid/mail": "^7.7.0",
|
"@sendgrid/mail": "^7.7.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
@ -22,11 +25,13 @@
|
|||||||
"typeorm": "^0.3.10"
|
"typeorm": "^0.3.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@openapitools/openapi-generator-cli": "^2.9.0",
|
||||||
"@types/jest": "^27.5.0",
|
"@types/jest": "^27.5.0",
|
||||||
"@types/node": "18.x",
|
"@types/node": "18.x",
|
||||||
"@types/redis": "^2.8.13",
|
"@types/redis": "^2.8.13",
|
||||||
"@types/redis-mock": "^0.17.3",
|
"@types/redis-mock": "^0.17.3",
|
||||||
"azure-functions-core-tools": "^4.x",
|
"azure-functions-core-tools": "^4.x",
|
||||||
|
"base64url": "^3.0.1",
|
||||||
"jest": "^28.0.3",
|
"jest": "^28.0.3",
|
||||||
"redis-mock": "^0.56.3",
|
"redis-mock": "^0.56.3",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export type AdB2cResponse = {
|
export type AdB2cResponse = {
|
||||||
'@odata.context': string;
|
"@odata.context": string;
|
||||||
value: AdB2cUser[];
|
value: AdB2cUser[];
|
||||||
};
|
};
|
||||||
export type 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