Merge branch 'develop' into main

This commit is contained in:
oura.a 2023-10-19 16:50:27 +09:00
commit 279fecec88
148 changed files with 7378 additions and 1703 deletions

View File

@ -20,4 +20,6 @@ RUN mkdir build \
COPY --from=build-container app/dictation_server/dist/ dist/
COPY --from=build-container app/dictation_server/.env ./
COPY --from=build-container app/dictation_server/node_modules/ node_modules/
ARG BUILD_VERSION
ENV BUILD_VERSION=${BUILD_VERSION}
CMD ["node", "./dist/main.js" ]

View File

@ -1,5 +1,4 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成し、
# 環境変数 AZURE_SERVICE_CONNECTION の値としてServiceConenction名を設定しておくこと
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger:
tags:
@ -26,43 +25,40 @@ jobs:
fi
displayName: 'タグが付けられたCommitがmainブランチに存在するか確認'
- job: backend_deploy
dependsOn: initialize
condition: succeeded('initialize')
displayName: Backend Deploy
pool:
name: odms-deploy-pipeline
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureRmWebAppDeployment@4
- task: AzureWebAppContainer@1
inputs:
ConnectionType: 'AzureRM'
azureSubscription: $(AZURE_SERVICE_CONNECTION)
appType: 'webAppContainer'
WebAppName: 'app-odms-dictation-prod'
ResourceGroupName: 'prod-application-rg'
DockerNamespace: 'crodmsregistrymaintenance.azurecr.io'
DockerRepository: '$(Build.Repository.Name)/staging/dictation'
DockerImageTag: '$(Build.SourceVersion)'
azureSubscription: 'omds-service-connection-prod'
appName: 'app-odms-dictation-prod'
deployToSlotOrASE: true
resourceGroupName: 'odms-prod-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)'
# TODO: stagingパイプライン実装時、staging用のイメージに変更する
- job: frontend_deploy
dependsOn: backend_deploy
condition: succeeded('backend_deploy')
displayName: Deploy Frontend Files
variables:
storageAccountName: saomdspipeline
containerName: staging
pool:
name: odms-deploy-pipeline
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-prod'
inputs:
ConnectedServiceName: $(AZURE_SERVICE_CONNECTION)
KeyVaultName: kv-odms-secret-prod
SecretsFilter: '*'
- task: AzureCLI@2
inputs:
azureSubscription: $(AZURE_SERVICE_CONNECTION)
azureSubscription: 'omds-service-connection-prod'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
@ -88,15 +84,42 @@ jobs:
is_static_export: false
verbose: false
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
- job: migration
condition: succeeded('initialize')
displayName: DB migration
dependsOn:
- initialize
- backend_deploy
- frontend_deploy
- job: smoke_test
dependsOn: frontend_deploy
condition: succeeded('frontend_deploy')
displayName: 'smoke test'
pool:
name: db-migrate-pipelines
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-prod'
inputs:
azureSubscription: 'omds-service-connection-prod'
action: 'Swap Slots'
WebAppName: 'app-odms-dictation-prod'
ResourceGroupName: 'odms-prod-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
@ -104,7 +127,7 @@ jobs:
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-prod'
inputs:
ConnectedServiceName: $(AZURE_SERVICE_CONNECTION)
ConnectedServiceName: 'omds-service-connection-prod'
KeyVaultName: kv-odms-secret-prod
- task: CmdLine@2
displayName: migration

View File

@ -1,5 +1,4 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成し、
# 環境変数 AZURE_SERVICE_CONNECTION の値としてServiceConenction名を設定しておくこと
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger:
branches:
@ -47,7 +46,7 @@ jobs:
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: $(AZURE_SERVICE_CONNECTION)
ConnectedServiceName: 'omds-service-connection-stg'
KeyVaultName: kv-odms-secret-stg
SecretsFilter: '*'
- task: Bash@3
@ -59,6 +58,7 @@ jobs:
npm run test
env:
JWT_PUBLIC_KEY: $(token-public-key)
JWT_PRIVATE_KEY: $(token-private-key)
SENDGRID_API_KEY: $(sendgrid-api-key)
NOTIFICATION_HUB_NAME: $(notification-hub-name)
NOTIFICATION_HUB_CONNECT_STRING: $(notification-hub-connect-string)
@ -74,43 +74,31 @@ jobs:
ADB2C_TENANT_ID: $(adb2c-tenant-id)
ADB2C_CLIENT_ID: $(adb2c-client-id)
ADB2C_CLIENT_SECRET: $(adb2c-client-secret)
MAIL_FROM: xxxxxx
APP_DOMAIN: xxxxxxxxx
EMAIL_CONFIRM_LIFETIME : 0
TENANT_NAME : xxxxxxxxxxxx
SIGNIN_FLOW_NAME : xxxxxxxxxxxx
STORAGE_TOKEN_EXPIRE_TIME : 0
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: $(AZURE_SERVICE_CONNECTION)
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: $(Build.Repository.Name)/staging/dictation:$(Build.SourceVersion)
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: $(AZURE_SERVICE_CONNECTION)
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: $(Build.Repository.Name)/staging/dictation:$(Build.SourceVersion)
- job: backend_deploy
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
- job: frontend_build
dependsOn: backend_build
condition: succeeded('backend_build')
displayName: Backend Deploy
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureRmWebAppDeployment@4
inputs:
ConnectionType: 'AzureRM'
azureSubscription: $(AZURE_SERVICE_CONNECTION)
appType: 'webAppContainer'
WebAppName: 'app-odms-dictation-stg'
ResourceGroupName: 'stg-application-rg'
DockerNamespace: 'crodmsregistrymaintenance.azurecr.io'
DockerRepository: '$(Build.Repository.Name)/staging/dictation'
DockerImageTag: '$(Build.SourceVersion)'
- job: frontend_build
dependsOn: initialize
condition: succeeded('initialize')
displayName: Build Frontend Files
variables:
storageAccountName: saomdspipeline
@ -141,7 +129,7 @@ jobs:
replaceExistingArchive: true
- task: AzureCLI@2
inputs:
azureSubscription: $(AZURE_SERVICE_CONNECTION)
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
@ -151,10 +139,22 @@ jobs:
--container-name $(containerName) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: frontend_deploy
- job: backend_deploy
dependsOn: frontend_build
condition: succeeded('frontend_build')
displayName: Backend Deploy
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
# TODO: Productionと同様にデプロイを行う
- job: frontend_deploy
dependsOn: backend_deploy
condition: succeeded('backend_deploy')
displayName: Deploy Frontend Files
variables:
storageAccountName: saomdspipeline
@ -165,69 +165,15 @@ jobs:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: $(AZURE_SERVICE_CONNECTION)
KeyVaultName: kv-odms-secret-stg
SecretsFilter: '*'
- task: AzureCLI@2
inputs:
azureSubscription: $(AZURE_SERVICE_CONNECTION)
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob download \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(containerName) \
--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)
# TODO: Productionと同様にデプロイを行う
- job: migration
condition: succeeded('initialize')
dependsOn: frontend_deploy
condition: succeeded('frontend_deploy')
displayName: DB migration
dependsOn:
- initialize
- backend_deploy
- frontend_deploy
pool:
name: db-migrate-pipelines
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: $(AZURE_SERVICE_CONNECTION)
KeyVaultName: kv-odms-secret-stg
- task: CmdLine@2
displayName: migration
inputs:
script: >2
# DB接続情報書き換え
sed -i -e "s/DB_NAME/$(db-name)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PASS/$(db-pass)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_USERNAME/$(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
# TODO: Productionと同様にマイグレーションを行う

View File

@ -11,6 +11,7 @@ import { useTranslation } from "react-i18next";
import Snackbar from "components/snackbar";
import { selectSnackber } from "features/ui/selectors";
import { closeSnackbar } from "features/ui/uiSlice";
import { UNAUTHORIZED_TO_CONTINUE_ERROR_CODES } from "components/auth/constants";
const App = (): JSX.Element => {
const dispatch = useDispatch();
@ -21,8 +22,12 @@ const App = (): JSX.Element => {
useEffect(() => {
const id = globalAxios.interceptors.response.use(
(response: AxiosResponse) => response,
(e: AxiosError) => {
if (e?.response?.status === 401) {
(e: AxiosError<{ code?: string }>) => {
if (
e?.response?.status === 401 &&
e?.response?.data?.code &&
!UNAUTHORIZED_TO_CONTINUE_ERROR_CODES.includes(e.response.data.code)
) {
dispatch(clearToken());
instance.logoutRedirect({
postLogoutRedirectUri: "/?logout=true",

View File

@ -1,5 +1,6 @@
import { Route, Routes } from "react-router-dom";
import TopPage from "pages/TopPage";
import AuthPage from "pages/AuthPage";
import LoginPage from "pages/LoginPage";
import SamplePage from "pages/SamplePage";
import { AuthErrorPage } from "pages/ErrorPage";
@ -20,18 +21,21 @@ import WorkflowPage from "pages/WorkflowPage";
import TypistGroupSettingPage from "pages/TypistGroupSettingPage";
import WorktypeIdSettingPage from "pages/WorkTypeIdSettingPage";
import AccountPage from "pages/AccountPage";
import AcceptToUsePage from "pages/TermsPage";
import { TemplateFilePage } from "pages/TemplateFilePage";
import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess";
const AppRouter: React.FC = () => (
<Routes>
<Route path="/" element={<TopPage />} />
<Route path="/auth" element={<AuthPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/authError" element={<AuthErrorPage />} />
<Route
path="/signup"
element={<SignupPage completeTo="/signup/complete" />}
/>
<Route path="/terms" element={<AcceptToUsePage />} />
<Route path="/signup/complete" element={<SignupCompletePage />} />
<Route path="/mail-confirm/" element={<VerifyPage />} />
<Route path="/mail-confirm/user" element={<UserVerifyPage />} />

View File

@ -127,7 +127,7 @@ export interface AllocatableLicenseInfo {
* @type {string}
* @memberof AllocatableLicenseInfo
*/
'expiryDate': string;
'expiryDate'?: string;
}
/**
*
@ -442,11 +442,17 @@ export interface CreateAccountRequest {
*/
'adminPassword': string;
/**
*
* (EULA)
* @type {string}
* @memberof CreateAccountRequest
*/
'acceptedTermsVersion': string;
'acceptedEulaVersion': string;
/**
* (DPA)
* @type {string}
* @memberof CreateAccountRequest
*/
'acceptedDpaVersion': string;
/**
* reCAPTCHA Token
* @type {string}
@ -530,7 +536,7 @@ export interface CreateTypistGroupRequest {
*/
export interface CreateWorkflowsRequest {
/**
* Authornの内部ID
* Authorの内部ID
* @type {number}
* @memberof CreateWorkflowsRequest
*/
@ -643,6 +649,32 @@ export interface ErrorResponse {
*/
'code': string;
}
/**
*
* @export
* @interface GetAccountInfoMinimalAccessRequest
*/
export interface GetAccountInfoMinimalAccessRequest {
/**
* idトークン
* @type {string}
* @memberof GetAccountInfoMinimalAccessRequest
*/
'idToken': string;
}
/**
*
* @export
* @interface GetAccountInfoMinimalAccessResponse
*/
export interface GetAccountInfoMinimalAccessResponse {
/**
*
* @type {number}
* @memberof GetAccountInfoMinimalAccessResponse
*/
'tier': number;
}
/**
*
* @export
@ -994,6 +1026,19 @@ export interface GetTemplatesResponse {
*/
'templates': Array<TemplateFile>;
}
/**
*
* @export
* @interface GetTermsInfoResponse
*/
export interface GetTermsInfoResponse {
/**
*
* @type {Array<TermInfo>}
* @memberof GetTermsInfoResponse
*/
'termsInfo': Array<TermInfo>;
}
/**
*
* @export
@ -1784,6 +1829,25 @@ export interface TemplateUploadLocationResponse {
*/
'url': string;
}
/**
*
* @export
* @interface TermInfo
*/
export interface TermInfo {
/**
*
* @type {string}
* @memberof TermInfo
*/
'documentType': string;
/**
*
* @type {string}
* @memberof TermInfo
*/
'version': string;
}
/**
*
* @export
@ -1860,6 +1924,31 @@ export interface TypistGroup {
*/
'name': string;
}
/**
*
* @export
* @interface UpdateAcceptedVersionRequest
*/
export interface UpdateAcceptedVersionRequest {
/**
* IDトークン
* @type {string}
* @memberof UpdateAcceptedVersionRequest
*/
'idToken': string;
/**
* EULA
* @type {string}
* @memberof UpdateAcceptedVersionRequest
*/
'acceptedEULAVersion': string;
/**
* DPA
* @type {string}
* @memberof UpdateAcceptedVersionRequest
*/
'acceptedDPAVersion'?: string;
}
/**
*
* @export
@ -1923,6 +2012,37 @@ export interface UpdateTypistGroupRequest {
*/
'typistIds': Array<number>;
}
/**
*
* @export
* @interface UpdateWorkflowRequest
*/
export interface UpdateWorkflowRequest {
/**
* Authorの内部ID
* @type {number}
* @memberof UpdateWorkflowRequest
*/
'authorId': number;
/**
* Worktypeの内部ID
* @type {number}
* @memberof UpdateWorkflowRequest
*/
'worktypeId'?: number;
/**
* ID
* @type {number}
* @memberof UpdateWorkflowRequest
*/
'templateId'?: number;
/**
* /
* @type {Array<WorkflowTypist>}
* @memberof UpdateWorkflowRequest
*/
'typists': Array<WorkflowTypist>;
}
/**
*
* @export
@ -2408,9 +2528,9 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteAccount: async (deleteAccountRequest: DeleteAccountRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
deleteAccountAndData: async (deleteAccountRequest: DeleteAccountRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'deleteAccountRequest' is not null or undefined
assertParamExists('deleteAccount', 'deleteAccountRequest', deleteAccountRequest)
assertParamExists('deleteAccountAndData', 'deleteAccountRequest', deleteAccountRequest)
const localVarPath = `/accounts/delete`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -2441,6 +2561,80 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {number} id Worktypeの内部ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteWorktype: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('deleteWorktype', 'id', id)
const localVarPath = `/accounts/worktypes/{id}/delete`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAccountInfoMinimalAccess: async (getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'getAccountInfoMinimalAccessRequest' is not null or undefined
assertParamExists('getAccountInfoMinimalAccess', 'getAccountInfoMinimalAccessRequest', getAccountInfoMinimalAccessRequest)
const localVarPath = `/accounts/minimal-access`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(getAccountInfoMinimalAccessRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Author一覧を取得します
* @summary
@ -3180,8 +3374,30 @@ export const AccountsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteAccount(deleteAccountRequest: DeleteAccountRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAccount(deleteAccountRequest, options);
async deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAccountAndData(deleteAccountRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
* @param {number} id Worktypeの内部ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteWorktype(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteWorktype(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
* @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetAccountInfoMinimalAccessResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -3446,8 +3662,28 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteAccount(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise<object> {
return localVarFp.deleteAccount(deleteAccountRequest, options).then((request) => request(axios, basePath));
deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise<object> {
return localVarFp.deleteAccountAndData(deleteAccountRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {number} id Worktypeの内部ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteWorktype(id: number, options?: any): AxiosPromise<object> {
return localVarFp.deleteWorktype(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options?: any): AxiosPromise<GetAccountInfoMinimalAccessResponse> {
return localVarFp.getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest, options).then((request) => request(axios, basePath));
},
/**
* Author一覧を取得します
@ -3707,8 +3943,32 @@ export class AccountsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof AccountsApi
*/
public deleteAccount(deleteAccountRequest: DeleteAccountRequest, options?: AxiosRequestConfig) {
return AccountsApiFp(this.configuration).deleteAccount(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath));
public deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: AxiosRequestConfig) {
return AccountsApiFp(this.configuration).deleteAccountAndData(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {number} id Worktypeの内部ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AccountsApi
*/
public deleteWorktype(id: number, options?: AxiosRequestConfig) {
return AccountsApiFp(this.configuration).deleteWorktype(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AccountsApi
*/
public getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options?: AxiosRequestConfig) {
return AccountsApiFp(this.configuration).getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -5981,6 +6241,105 @@ export class TemplatesApi extends BaseAPI {
/**
* TermsApi - axios parameter creator
* @export
*/
export const TermsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getTermsInfo: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/terms`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* TermsApi - functional programming interface
* @export
*/
export const TermsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = TermsApiAxiosParamCreator(configuration)
return {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getTermsInfo(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetTermsInfoResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTermsInfo(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* TermsApi - factory interface
* @export
*/
export const TermsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = TermsApiFp(configuration)
return {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getTermsInfo(options?: any): AxiosPromise<GetTermsInfoResponse> {
return localVarFp.getTermsInfo(options).then((request) => request(axios, basePath));
},
};
};
/**
* TermsApi - object-oriented interface
* @export
* @class TermsApi
* @extends {BaseAPI}
*/
export class TermsApi extends BaseAPI {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof TermsApi
*/
public getTermsInfo(options?: AxiosRequestConfig) {
return TermsApiFp(this.configuration).getTermsInfo(options).then((request) => request(this.axios, this.basePath));
}
}
/**
* UsersApi - axios parameter creator
* @export
@ -6281,6 +6640,42 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateAcceptedVersion: async (updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'updateAcceptedVersionRequest' is not null or undefined
assertParamExists('updateAcceptedVersion', 'updateAcceptedVersionRequest', updateAcceptedVersionRequest)
const localVarPath = `/users/accepted-version`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(updateAcceptedVersionRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
@ -6456,6 +6851,17 @@ export const UsersApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.signup(signupRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
* @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateAcceptedVersion(updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAcceptedVersion(updateAcceptedVersionRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
@ -6565,6 +6971,16 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath
signup(signupRequest: SignupRequest, options?: any): AxiosPromise<object> {
return localVarFp.signup(signupRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateAcceptedVersion(updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options?: any): AxiosPromise<object> {
return localVarFp.updateAcceptedVersion(updateAcceptedVersionRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
@ -6688,6 +7104,18 @@ export class UsersApi extends BaseAPI {
return UsersApiFp(this.configuration).signup(signupRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof UsersApi
*/
public updateAcceptedVersion(updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options?: AxiosRequestConfig) {
return UsersApiFp(this.configuration).updateAcceptedVersion(updateAcceptedVersionRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
@ -6761,6 +7189,44 @@ export const WorkflowsApiAxiosParamCreator = function (configuration?: Configura
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {number} workflowId ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteWorkflow: async (workflowId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'workflowId' is not null or undefined
assertParamExists('deleteWorkflow', 'workflowId', workflowId)
const localVarPath = `/workflows/{workflowId}/delete`
.replace(`{${"workflowId"}}`, encodeURIComponent(String(workflowId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
@ -6790,6 +7256,50 @@ export const WorkflowsApiAxiosParamCreator = function (configuration?: Configura
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {number} workflowId ID
* @param {UpdateWorkflowRequest} updateWorkflowRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateWorkflow: async (workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'workflowId' is not null or undefined
assertParamExists('updateWorkflow', 'workflowId', workflowId)
// verify required parameter 'updateWorkflowRequest' is not null or undefined
assertParamExists('updateWorkflow', 'updateWorkflowRequest', updateWorkflowRequest)
const localVarPath = `/workflows/{workflowId}`
.replace(`{${"workflowId"}}`, encodeURIComponent(String(workflowId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(updateWorkflowRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@ -6816,6 +7326,17 @@ export const WorkflowsApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.createWorkflows(createWorkflowsRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
* @param {number} workflowId ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteWorkflow(workflowId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteWorkflow(workflowId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
@ -6826,6 +7347,18 @@ export const WorkflowsApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getWorkflows(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
* @param {number} workflowId ID
* @param {UpdateWorkflowRequest} updateWorkflowRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateWorkflow(workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateWorkflow(workflowId, updateWorkflowRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
@ -6846,6 +7379,16 @@ export const WorkflowsApiFactory = function (configuration?: Configuration, base
createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: any): AxiosPromise<object> {
return localVarFp.createWorkflows(createWorkflowsRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {number} workflowId ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteWorkflow(workflowId: number, options?: any): AxiosPromise<object> {
return localVarFp.deleteWorkflow(workflowId, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
@ -6855,6 +7398,17 @@ export const WorkflowsApiFactory = function (configuration?: Configuration, base
getWorkflows(options?: any): AxiosPromise<GetWorkflowsResponse> {
return localVarFp.getWorkflows(options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {number} workflowId ID
* @param {UpdateWorkflowRequest} updateWorkflowRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateWorkflow(workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options?: any): AxiosPromise<object> {
return localVarFp.updateWorkflow(workflowId, updateWorkflowRequest, options).then((request) => request(axios, basePath));
},
};
};
@ -6877,6 +7431,18 @@ export class WorkflowsApi extends BaseAPI {
return WorkflowsApiFp(this.configuration).createWorkflows(createWorkflowsRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {number} workflowId ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof WorkflowsApi
*/
public deleteWorkflow(workflowId: number, options?: AxiosRequestConfig) {
return WorkflowsApiFp(this.configuration).deleteWorkflow(workflowId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
@ -6887,6 +7453,19 @@ export class WorkflowsApi extends BaseAPI {
public getWorkflows(options?: AxiosRequestConfig) {
return WorkflowsApiFp(this.configuration).getWorkflows(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {number} workflowId ID
* @param {UpdateWorkflowRequest} updateWorkflowRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof WorkflowsApi
*/
public updateWorkflow(workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options?: AxiosRequestConfig) {
return WorkflowsApiFp(this.configuration).updateWorkflow(workflowId, updateWorkflowRequest, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -18,6 +18,7 @@ import worktype from "features/workflow/worktype/worktypeSlice";
import account from "features/account/accountSlice";
import template from "features/workflow/template/templateSlice";
import workflow from "features/workflow/workflowSlice";
import terms from "features/terms/termsSlice";
export const store = configureStore({
reducer: {
@ -40,6 +41,7 @@ export const store = configureStore({
account,
template,
workflow,
terms,
},
});

View File

@ -32,6 +32,7 @@ export const errorCodes = [
"E010206", // DBのTierが想定外の値エラー
"E010207", // ユーザーのRole変更不可エラー
"E010208", // ユーザーの暗号化パスワード不足エラー
"E010209", // ユーザーの同意済み利用規約バージョンが最新でないエラー
"E010301", // メールアドレス登録済みエラー
"E010302", // authorId重複エラー
"E010401", // PONumber重複エラー
@ -55,5 +56,7 @@ export const errorCodes = [
"E011001", // ワークタイプ重複エラー
"E011002", // ワークタイプ登録上限超過エラー
"E011003", // ワークタイプ不在エラー
"E011004", // ワークタイプ使用中エラー
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
"E013002", // ワークフロー不在エラー
] as const;

View File

@ -81,3 +81,21 @@ const isErrorResponse = (error: unknown): error is ErrorResponse => {
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;
};

View File

@ -5,7 +5,7 @@ export const msalConfig: Configuration = {
clientId: import.meta.env.VITE_B2C_CLIENTID,
authority: import.meta.env.VITE_B2C_AUTHORITY,
knownAuthorities: [import.meta.env.VITE_B2C_KNOWNAUTHORITIES],
redirectUri: `${globalThis.location.origin}/login`,
redirectUri: `${globalThis.location.origin}/auth`,
navigateToLoginRequestUrl: false,
},
cache: {

View File

@ -62,3 +62,16 @@ export const isIdToken = (arg: any): arg is IdToken => {
return true;
};
export const getIdTokenFromLocalStorage = (
localStorageKeyforIdToken: string
): string | null => {
const idTokenString = localStorage.getItem(localStorageKeyforIdToken);
if (idTokenString) {
const idTokenObject = JSON.parse(idTokenString);
if (isIdToken(idTokenObject)) {
return idTokenObject.secret;
}
}
return null;
};

View File

@ -28,3 +28,9 @@ export const TIERS = {
TIER4: "4",
TIER5: "5",
} as const;
/**
* 401
* @const {string[]}
*/
export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = ["E010209"];

View File

@ -130,7 +130,7 @@ export const deleteAccountAsync = createAsyncThunk<
const accountApi = new AccountsApi(config);
try {
await accountApi.deleteAccount(deleteAccounRequest, {
await accountApi.deleteAccountAndData(deleteAccounRequest, {
headers: { authorization: `Bearer ${accessToken}` },
});

View File

@ -1,17 +1,26 @@
import { createSlice } from "@reduxjs/toolkit";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { LoginState } from "./state";
import { loginAsync } from "./operations";
const initialState: LoginState = {
apps: {
LoginApiCallStatus: "none",
localStorageKeyforIdToken: null,
},
};
export const loginSlice = createSlice({
name: "login",
initialState,
reducers: {},
reducers: {
changeLocalStorageKeyforIdToken: (
state,
action: PayloadAction<{ localStorageKeyforIdToken: string }>
) => {
const { localStorageKeyforIdToken } = action.payload;
state.apps.localStorageKeyforIdToken = localStorageKeyforIdToken;
},
},
extraReducers: (builder) => {
builder.addCase(loginAsync.pending, (state) => {
state.apps.LoginApiCallStatus = "pending";
@ -25,4 +34,5 @@ export const loginSlice = createSlice({
},
});
export const { changeLocalStorageKeyforIdToken } = loginSlice.actions;
export default loginSlice.reducer;

View File

@ -3,6 +3,7 @@ import type { RootState } from "app/store";
import { setToken } from "features/auth/authSlice";
import { AuthApi } from "../../api/api";
import { Configuration } from "../../api/configuration";
import { ErrorObject, createErrorObject } from "../../common/errors";
export const loginAsync = createAsyncThunk<
{
@ -14,7 +15,7 @@ export const loginAsync = createAsyncThunk<
{
// rejectした時の返却値の型
rejectValue: {
/* Empty Object */
error: ErrorObject;
};
}
>("login/loginAsync", async (args, thunkApi) => {
@ -41,6 +42,8 @@ export const loginAsync = createAsyncThunk<
return {};
} catch (e) {
return thunkApi.rejectWithValue({});
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -4,3 +4,7 @@ export const selectLoginApiCallStatus = (
state: RootState
): "fulfilled" | "rejected" | "none" | "pending" =>
state.login.apps.LoginApiCallStatus;
export const selectLocalStorageKeyforIdToken = (
state: RootState
): string | null => state.login.apps.localStorageKeyforIdToken;

View File

@ -4,4 +4,5 @@ export interface LoginState {
export interface Apps {
LoginApiCallStatus: "fulfilled" | "rejected" | "none" | "pending";
localStorageKeyforIdToken: string | null;
}

View File

@ -3,10 +3,12 @@ import type { RootState } from "app/store";
import { ErrorObject, createErrorObject } from "common/errors";
import { getTranslationID } from "translation";
import { closeSnackbar, openSnackbar } from "features/ui/uiSlice";
import { TERMS_DOCUMENT_TYPE } from "features/terms/constants";
import {
AccountsApi,
CreateAccountRequest,
GetDealersResponse,
TermsApi,
} from "../../api/api";
import { Configuration } from "../../api/configuration";
@ -93,3 +95,42 @@ export const getDealersAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const getLatestEulaVersionAsync = createAsyncThunk<
string,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("login/getLatestEulaVersionAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const config = new Configuration(configuration);
const termsApi = new TermsApi(config);
try {
const termsInfo = await termsApi.getTermsInfo();
const latestEulaVersion = termsInfo.data.termsInfo.find(
(val) => val.documentType === TERMS_DOCUMENT_TYPE.EULA
);
if (!latestEulaVersion) {
throw new Error("EULA info is not found");
}
return latestEulaVersion.version;
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -72,3 +72,6 @@ export const selectSelectedDealer = (state: RootState) => {
const { dealer } = state.signup.apps;
return dealers.find((x: Dealer) => x.id === dealer);
};
export const selectEulaVersion = (state: RootState) =>
state.signup.domain.eulaVersion;

View File

@ -1,6 +1,10 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SignupState } from "./state";
import { getDealersAsync, signupAsync } from "./operations";
import {
getDealersAsync,
getLatestEulaVersionAsync,
signupAsync,
} from "./operations";
const initialState: SignupState = {
apps: {
@ -15,6 +19,7 @@ const initialState: SignupState = {
},
domain: {
dealers: [],
eulaVersion: "",
},
};
@ -74,6 +79,15 @@ export const signupSlice = createSlice({
builder.addCase(getDealersAsync.rejected, () => {
//
});
builder.addCase(getLatestEulaVersionAsync.pending, () => {
//
});
builder.addCase(getLatestEulaVersionAsync.fulfilled, (state, action) => {
state.domain.eulaVersion = action.payload;
});
builder.addCase(getLatestEulaVersionAsync.rejected, () => {
//
});
},
});
export const {

View File

@ -18,4 +18,5 @@ export interface Apps {
export interface Domain {
dealers: Dealer[];
eulaVersion: string;
}

View File

@ -0,0 +1,8 @@
/**
*
* @const {string[]}
*/
export const TERMS_DOCUMENT_TYPE = {
DPA: "DPA",
EULA: "EULA",
} as const;

View File

@ -0,0 +1,4 @@
export * from "./termsSlice";
export * from "./state";
export * from "./operations";
export * from "./selectors";

View File

@ -0,0 +1,158 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { ErrorObject, createErrorObject } from "common/errors";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
import { getIdTokenFromLocalStorage } from "common/token";
import { TIERS } from "components/auth/constants";
import {
UsersApi,
GetAccountInfoMinimalAccessResponse,
AccountsApi,
TermsApi,
GetTermsInfoResponse,
} from "../../api/api";
import { Configuration } from "../../api/configuration";
export const getAccountInfoMinimalAccessAsync = createAsyncThunk<
GetAccountInfoMinimalAccessResponse,
{
localStorageKeyforIdToken: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accept/getAccountInfoMinimalAccessAsync", async (args, thunkApi) => {
const { localStorageKeyforIdToken } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
try {
// IDトークンの取得
const idToken = getIdTokenFromLocalStorage(localStorageKeyforIdToken);
// IDトークンが取得できない場合エラーとする
if (!idToken) {
throw new Error("Unable to retrieve the ID token.");
}
const res = await accountApi.getAccountInfoMinimalAccess(
{ idToken },
{
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 getTermsInfoAsync = createAsyncThunk<
GetTermsInfoResponse,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accept/getTermsInfoAsync", async (_args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const termsApi = new TermsApi(config);
try {
const termsInfo = await termsApi.getTermsInfo({
headers: { authorization: `Bearer ${accessToken}` },
});
return termsInfo.data;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateAcceptedVersionAsync = createAsyncThunk<
{
/* Empty Object */
},
{
tier: number;
localStorageKeyforIdToken: string;
updateAccceptVersions: {
acceptedVerDPA: string;
acceptedVerEULA: string;
};
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accept/UpdateAcceptedVersionAsync", async (args, thunkApi) => {
const { tier, localStorageKeyforIdToken, updateAccceptVersions } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const userApi = new UsersApi(config);
try {
// IDトークンの取得
const idToken = getIdTokenFromLocalStorage(localStorageKeyforIdToken);
// IDトークンが取得できない場合エラーとする
if (!idToken) {
throw new Error("Unable to retrieve the ID token.");
}
await userApi.updateAcceptedVersion(
{
idToken,
acceptedEULAVersion: updateAccceptVersions.acceptedVerEULA,
acceptedDPAVersion: !(TIERS.TIER5 === tier.toString())
? updateAccceptVersions.acceptedVerDPA
: undefined,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return {};
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,20 @@
import { RootState } from "app/store";
import { TERMS_DOCUMENT_TYPE } from "features/terms/constants";
export const selectTermVersions = (state: RootState) => {
const { termsInfo } = state.terms.domain;
const acceptedVerDPA =
termsInfo.find(
(termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.DPA
)?.version || "";
const acceptedVerEULA =
termsInfo.find(
(termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.EULA
)?.version || "";
return { acceptedVerDPA, acceptedVerEULA };
};
export const selectTier = (state: RootState) => state.terms.domain.tier;

View File

@ -0,0 +1,15 @@
import { TermInfo } from "../../api/api";
export interface AcceptState {
domain: Domain;
apps: Apps;
}
export interface Domain {
tier: number;
termsInfo: TermInfo[];
}
export interface Apps {
isLoading: boolean;
}

View File

@ -0,0 +1,64 @@
import { createSlice } from "@reduxjs/toolkit";
import { AcceptState } from "./state";
import {
getAccountInfoMinimalAccessAsync,
getTermsInfoAsync,
updateAcceptedVersionAsync,
} from "./operations";
const initialState: AcceptState = {
domain: {
tier: 0,
termsInfo: [
{
documentType: "",
version: "",
},
],
},
apps: {
isLoading: false,
},
};
export const termsSlice = createSlice({
name: "terms",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getAccountInfoMinimalAccessAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(
getAccountInfoMinimalAccessAsync.fulfilled,
(state, actions) => {
state.apps.isLoading = false;
state.domain.tier = actions.payload.tier;
}
);
builder.addCase(getAccountInfoMinimalAccessAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getTermsInfoAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getTermsInfoAsync.fulfilled, (state, actions) => {
state.apps.isLoading = false;
state.domain.termsInfo = actions.payload.termsInfo;
});
builder.addCase(getTermsInfoAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateAcceptedVersionAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(updateAcceptedVersionAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateAcceptedVersionAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export default termsSlice.reducer;

View File

@ -87,7 +87,7 @@ export const userSlice = createSlice({
action: PayloadAction<{ authorId: string | undefined }>
) => {
const { authorId } = action.payload;
state.apps.addUser.authorId = authorId;
state.apps.addUser.authorId = authorId?.toUpperCase();
},
changeAutoRenew: (state, action: PayloadAction<{ autoRenew: boolean }>) => {
const { autoRenew } = action.payload;
@ -144,7 +144,7 @@ export const userSlice = createSlice({
state.apps.updateUser.name = user.name;
state.apps.updateUser.email = user.email;
state.apps.updateUser.role = user.role as RoleType;
state.apps.updateUser.authorId = user.authorId;
state.apps.updateUser.authorId = user.authorId?.toUpperCase();
state.apps.updateUser.encryption = user.encryption;
state.apps.updateUser.encryptionPassword = undefined;
state.apps.updateUser.prompt = user.prompt;
@ -156,7 +156,7 @@ export const userSlice = createSlice({
state.apps.selectedUser.name = user.name;
state.apps.selectedUser.email = user.email;
state.apps.selectedUser.role = user.role as RoleType;
state.apps.selectedUser.authorId = user.authorId;
state.apps.selectedUser.authorId = user.authorId?.toUpperCase();
state.apps.selectedUser.encryption = user.encryption;
state.apps.selectedUser.encryptionPassword = undefined;
state.apps.selectedUser.prompt = user.prompt;
@ -175,7 +175,7 @@ export const userSlice = createSlice({
action: PayloadAction<{ authorId: string }>
) => {
const { authorId } = action.payload;
state.apps.updateUser.authorId = authorId;
state.apps.updateUser.authorId = authorId.toUpperCase();
},
changeUpdateEncryption: (
state,
@ -243,7 +243,8 @@ export const userSlice = createSlice({
state.apps.licenseAllocateUser.id = selectedUser.id;
state.apps.licenseAllocateUser.name = selectedUser.name;
state.apps.licenseAllocateUser.email = selectedUser.email;
state.apps.licenseAllocateUser.authorId = selectedUser.authorId;
state.apps.licenseAllocateUser.authorId =
selectedUser.authorId.toUpperCase();
state.apps.licenseAllocateUser.licenseStatus = selectedUser.licenseStatus;
state.apps.licenseAllocateUser.expiration = selectedUser.expiration;
state.apps.licenseAllocateUser.remaining = selectedUser.remaining;

View File

@ -16,7 +16,6 @@ import type { RootState } from "app/store";
import { ErrorObject, createErrorObject } from "common/errors";
import { openSnackbar } from "features/ui/uiSlice";
import { getTranslationID } from "translation";
import { WorkflowRelations } from "./state";
export const listWorkflowAsync = createAsyncThunk<
GetWorkflowsResponse,
@ -141,6 +140,105 @@ export const createWorkflowAsync = createAsyncThunk<
}
});
export const updateWorkflowAsync = createAsyncThunk<
{
/* Empty Object */
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/updateWorkflowAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const workflowsApi = new WorkflowsApi(config);
const {
selectedWorkflow,
selectedAssignees,
authorId,
templateId,
worktypeId,
} = state.workflow.apps;
try {
if (selectedWorkflow === undefined) {
throw new Error("selectedWorkflow is not found");
}
if (authorId === undefined) {
throw new Error("authorId is not found");
}
// 選択されたタイピストを取得し、リクエスト用の型に変換する
const typists = selectedAssignees.map(
(item): WorkflowTypist => ({
typistId: item.typistUserId,
typistGroupId: item.typistGroupId,
})
);
await workflowsApi.updateWorkflow(
selectedWorkflow.id,
{
authorId,
typists,
templateId,
worktypeId,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const { code, statusCode } = error;
if (statusCode === 400) {
// AuthorIDとWorktypeIDが一致するものが既に存在する場合
if (code === "E013001") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"workflowPage.message.workflowConflictError"
),
})
);
return thunkApi.rejectWithValue({ error });
}
// パラメータが存在しない場合
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("workflowPage.message.saveFailedError"),
})
);
return thunkApi.rejectWithValue({ error });
}
// その他のエラー
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getworkflowRelationsAsync = createAsyncThunk<
{
authors: Author[];
@ -211,3 +309,60 @@ export const getworkflowRelationsAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const deleteWorkflowAsync = createAsyncThunk<
{
/* Empty Object */
},
{ workflowId: number },
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/deleteWorkflowAsync", async (args, thunkApi) => {
const { workflowId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const workflowsApi = new WorkflowsApi(config);
try {
await workflowsApi.deleteWorkflow(workflowId, {
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 === "E013002") {
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -36,6 +36,15 @@ export const selectWorkflowAssinee = (state: RootState) => {
),
};
};
export const selectSelectedWorkflow = (state: RootState) =>
state.workflow.apps.selectedWorkflow;
export const selectAuthorId = (state: RootState) =>
state.workflow.apps.authorId;
export const selectWorktypeId = (state: RootState) =>
state.workflow.apps.worktypeId;
export const selectTemplateId = (state: RootState) =>
state.workflow.apps.templateId;
export const selectIsAddLoading = (state: RootState) =>
state.workflow.apps.isAddLoading;

View File

@ -12,6 +12,7 @@ export interface Apps {
authorId?: number;
worktypeId?: number;
templateId?: number;
selectedWorkflow?: Workflow;
}
export interface Domain {

View File

@ -3,7 +3,9 @@ import { Assignee } from "api";
import {
createWorkflowAsync,
getworkflowRelationsAsync,
deleteWorkflowAsync,
listWorkflowAsync,
updateWorkflowAsync,
} from "./operations";
import { WorkflowState } from "./state";
@ -21,12 +23,17 @@ export const workflowSlice = createSlice({
initialState,
reducers: {
clearWorkflow: (state) => {
state.apps.selectedWorkflow = undefined;
state.apps.selectedAssignees = [];
state.apps.authorId = undefined;
state.apps.worktypeId = undefined;
state.apps.templateId = undefined;
state.domain.workflowRelations = undefined;
},
setAssignees: (state, action: PayloadAction<{ assignees: Assignee[] }>) => {
const { assignees } = action.payload;
state.apps.selectedAssignees = assignees;
},
addAssignee: (state, action: PayloadAction<{ assignee: Assignee }>) => {
const { assignee } = action.payload;
const { selectedAssignees } = state.apps;
@ -55,18 +62,35 @@ export const workflowSlice = createSlice({
x.typistGroupId !== assignee.typistGroupId
);
},
changeAuthor: (state, action: PayloadAction<{ authorId: number }>) => {
changeAuthor: (
state,
action: PayloadAction<{ authorId?: number | undefined }>
) => {
const { authorId } = action.payload;
state.apps.authorId = authorId;
},
changeWorktype: (state, action: PayloadAction<{ worktypeId?: number }>) => {
changeWorktype: (
state,
action: PayloadAction<{ worktypeId?: number | undefined }>
) => {
const { worktypeId } = action.payload;
state.apps.worktypeId = worktypeId;
},
changeTemplate: (state, action: PayloadAction<{ templateId?: number }>) => {
changeTemplate: (
state,
action: PayloadAction<{ templateId?: number | undefined }>
) => {
const { templateId } = action.payload;
state.apps.templateId = templateId;
},
changeSelectedWorkflow: (
state,
action: PayloadAction<{ workflowId: number }>
) => {
const { workflowId } = action.payload;
const workflow = state.domain.workflows?.find((x) => x.id === workflowId);
state.apps.selectedWorkflow = workflow;
},
},
extraReducers: (builder) => {
builder.addCase(listWorkflowAsync.pending, (state) => {
@ -127,15 +151,35 @@ export const workflowSlice = createSlice({
builder.addCase(createWorkflowAsync.rejected, (state) => {
state.apps.isAddLoading = false;
});
builder.addCase(updateWorkflowAsync.pending, (state) => {
state.apps.isAddLoading = true;
});
builder.addCase(updateWorkflowAsync.fulfilled, (state) => {
state.apps.isAddLoading = false;
});
builder.addCase(updateWorkflowAsync.rejected, (state) => {
state.apps.isAddLoading = false;
});
builder.addCase(deleteWorkflowAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(deleteWorkflowAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteWorkflowAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const {
setAssignees,
addAssignee,
removeAssignee,
changeAuthor,
changeWorktype,
changeTemplate,
changeSelectedWorkflow,
clearWorkflow,
} = workflowSlice.actions;

View File

@ -342,3 +342,75 @@ export const updateActiveWorktypeAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const deleteWorktypeAsync = createAsyncThunk<
{
/* Empty Object */
},
{ worktypeId: number },
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/deleteWorktypeAsync", async (args, thunkApi) => {
const { worktypeId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
await accountsApi.deleteWorktype(worktypeId, {
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.statusCode === 400) {
if (error.code === "E011003") {
// ワークタイプが削除済みの場合は成功扱いとする
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
}
if (error.code === "E011004") {
// ワークタイプがワークフローで使用中の場合は削除できない
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"worktypeIdSetting.message.worktypeInUseError"
),
})
);
return {};
}
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -149,7 +149,8 @@ const AccountPage: React.FC = (): JSX.Element => {
{isTier5 && !viewInfo.account.parentAccountName && (
<dd className={styles.form}>
<select
className={`${styles.formInput} ${styles.required}`}
className={styles.formInput}
required
onChange={(event) => {
dispatch(
changeDealer({
@ -238,8 +239,8 @@ const AccountPage: React.FC = (): JSX.Element => {
</dt>
<dd className={styles.form}>
<select
name=""
className={`${styles.formInput} ${styles.required}`}
className={styles.formInput}
required
onChange={(event) => {
dispatch(
changePrimaryAdministrator({
@ -303,8 +304,8 @@ const AccountPage: React.FC = (): JSX.Element => {
</dt>
<dd className={styles.form}>
<select
name=""
className={`${styles.formInput} ${styles.required}`}
className={styles.formInput}
required
onChange={(event) => {
dispatch(
changeSecondryAdministrator({

View File

@ -0,0 +1,78 @@
import { useMsal } from "@azure/msal-react";
import { AuthError } from "@azure/msal-browser";
import { AppDispatch } from "app/store";
import Footer from "components/footer";
import Header from "components/header";
import {
selectLoginApiCallStatus,
changeLocalStorageKeyforIdToken,
} from "features/login";
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
const AuthPage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
const dispatch: AppDispatch = useDispatch();
const navigate = useNavigate();
const status = useSelector(selectLoginApiCallStatus);
// TODO 将来的にトークンの取得処理をoperations.ts側に移動させたい。useEffect内で非同期処理を行いたくない。
useEffect(() => {
if (status !== "none") {
// ログイン処理で、何回か本画面が描画される契機があるが、認証処理は一度だけ実施すればよいため認証処理実行済みであれば何もしない
return;
}
(async () => {
try {
const loginResult = await instance.handleRedirectPromise();
// eslint-disable-next-line
console.log({ loginResult }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除eslint-disable含めてする。
if (loginResult && loginResult.account) {
const { homeAccountId, idTokenClaims } = loginResult.account;
if (idTokenClaims && idTokenClaims.aud) {
const localStorageKeyforIdToken = `${homeAccountId}-${
import.meta.env.VITE_B2C_KNOWNAUTHORITIES
}-idtoken-${idTokenClaims.aud}----`;
// AADB2Cログイン画面以外から本画面に遷移した場合用にIDトークン取得用キーをstateに保存
dispatch(
changeLocalStorageKeyforIdToken({
localStorageKeyforIdToken,
})
);
// トークン取得と設定を行う
navigate("/login");
}
}
} catch (e) {
// eslint-disable-next-line
console.log({ e }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除eslint-disable含めてする。
// AAD B2Cの多要素認証画面やパスワードリセット画面で「cancel」をクリックすると、handleRedirectPromise()にてエラーが発生するため、
// それをハンドリングして適切な画面遷移処理を行う。
if (e instanceof AuthError) {
// エラーコードはerrorMessageの中の一部として埋め込まれており完全一致で取得するのは筋が悪いため、部分一致で取得する。
// TODO 他にもAADB2Cのエラーコードを使用する箇所が出てきた場合、定数化すること
if (e.errorMessage.startsWith("AADB2C90091")) {
navigate("/");
}
}
}
})();
}, [instance, navigate, status, dispatch]);
return (
<>
<Header />
<h3>loading ...</h3>
<Footer />
</>
);
};
export default AuthPage;

View File

@ -1,30 +1,39 @@
import { useMsal } from "@azure/msal-react";
import { AuthError } from "@azure/msal-browser";
import { AppDispatch } from "app/store";
import { isIdToken } from "common/token";
import { loadAccessToken, loadRefreshToken } from "features/auth/utils";
import { loginAsync, selectLocalStorageKeyforIdToken } from "features/login";
import React, { useCallback, useEffect } from "react";
import Footer from "components/footer";
import Header from "components/header";
import { loadAccessToken, loadRefreshToken } from "features/auth/utils";
import { loginAsync, selectLoginApiCallStatus } from "features/login";
import React, { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { isErrorObject } from "common/errors";
const LoginPage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
const dispatch: AppDispatch = useDispatch();
const navigate = useNavigate();
const [, i18n] = useTranslation();
const status = useSelector(selectLoginApiCallStatus);
const localStorageKeyforIdToken = useSelector(
selectLocalStorageKeyforIdToken
);
const login = useCallback(
const tokenSet = useCallback(
async (idToken: string) => {
// ログイン処理呼び出し
const { meta } = await dispatch(loginAsync({ idToken }));
const { meta, payload } = await dispatch(loginAsync({ idToken }));
// ログイン失敗した場合、B2Cをログアウトしてからエラーページに遷移する
if (meta.requestStatus === "rejected") {
if (isErrorObject(payload)) {
// 未同意の規約がある場合は利用規約同意画面に遷移する
if (payload.error.code === "E010209") {
navigate("/terms");
return;
}
}
instance.logoutRedirect({
postLogoutRedirectUri: "/AuthError",
});
@ -48,53 +57,26 @@ const LoginPage: React.FC = (): JSX.Element => {
[dispatch, i18n.language, instance, navigate]
);
// TODO 将来的にトークンの取得処理をoperations.ts側に移動させたい。useEffect内で非同期処理を行いたくない。
useEffect(() => {
if (status !== "none") {
// ログイン処理で、何回か本画面が描画される契機があるが、認証処理は一度だけ実施すればよいため認証処理実行済みであれば何もしない
// AADB2Cのログイン画面とLoginPageを経由していない場合はトップページに遷移する
if (!localStorageKeyforIdToken) {
navigate("/");
return;
}
(async () => {
try {
const loginResult = await instance.handleRedirectPromise();
// eslint-disable-next-line
console.log({ loginResult }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除eslint-disable含めてする。
if (loginResult && loginResult.account) {
const { homeAccountId, idTokenClaims } = loginResult.account;
if (idTokenClaims && idTokenClaims.aud) {
// IDトークンの取得
const idTokenString = localStorage.getItem(
`${homeAccountId}-${
import.meta.env.VITE_B2C_KNOWNAUTHORITIES
}-idtoken-${idTokenClaims.aud}----`
);
if (idTokenString) {
const idTokenObject = JSON.parse(idTokenString);
if (isIdToken(idTokenObject)) {
await login(idTokenObject.secret);
}
}
}
}
} catch (e) {
// eslint-disable-next-line
console.log({ e }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除eslint-disable含めてする。
// AAD B2Cの多要素認証画面やパスワードリセット画面で「cancel」をクリックすると、handleRedirectPromise()にてエラーが発生するため、
// それをハンドリングして適切な画面遷移処理を行う。
if (e instanceof AuthError) {
// エラーコードはerrorMessageの中の一部として埋め込まれており完全一致で取得するのは筋が悪いため、部分一致で取得する。
// TODO 他にもAADB2Cのエラーコードを使用する箇所が出てきた場合、定数化すること
if (e.errorMessage.startsWith("AADB2C90091")) {
navigate("/");
}
// IDトークンの取得
const idTokenString = localStorage.getItem(localStorageKeyforIdToken);
if (idTokenString) {
const idTokenObject = JSON.parse(idTokenString);
if (isIdToken(idTokenObject)) {
await tokenSet(idTokenObject.secret);
}
}
})();
}, [instance, login, navigate, status]);
// 画面描画後のみ実行するため引数を設定しない
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>

View File

@ -14,6 +14,7 @@ import {
selectEmail,
selectPassword,
selectSelectedDealer,
selectEulaVersion,
} from "../../features/signup/selectors";
import { signupAsync } from "../../features/signup/operations";
@ -27,6 +28,7 @@ const SignupConfirm: React.FC = (): JSX.Element => {
const adminMail = useSelector(selectEmail);
const adminPassword = useSelector(selectPassword);
const dealer = useSelector(selectSelectedDealer);
const acceptedEulaVersion = useSelector(selectEulaVersion);
const onSubmit = useCallback(() => {
dispatch(
@ -37,7 +39,8 @@ const SignupConfirm: React.FC = (): JSX.Element => {
adminName,
adminMail,
adminPassword,
acceptedTermsVersion: "",
acceptedEulaVersion,
acceptedDpaVersion: "",
token: "",
})
);
@ -49,6 +52,7 @@ const SignupConfirm: React.FC = (): JSX.Element => {
adminName,
adminMail,
adminPassword,
acceptedEulaVersion,
]);
return (

View File

@ -24,7 +24,10 @@ import { useDispatch, useSelector } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { getTranslationID } from "translation";
import styles from "styles/app.module.scss";
import { getDealersAsync } from "features/signup/operations";
import {
getDealersAsync,
getLatestEulaVersionAsync,
} from "features/signup/operations";
import { LANGUAGE_LIST } from "features/top/constants";
import { openSnackbar } from "features/ui";
import { COUNTRY_LIST } from "./constants";
@ -84,6 +87,7 @@ const SignupInput: React.FC = (): JSX.Element => {
// 入力画面の初期化時の処理
useEffect(() => {
dispatch(getDealersAsync());
dispatch(getLatestEulaVersionAsync());
}, [dispatch]);
useEffect(() => {
@ -264,10 +268,9 @@ const SignupInput: React.FC = (): JSX.Element => {
/>
{isPushCreateButton && hasErrorEmptyAdminName && (
<span className={styles.formError}>
{" "}
{t(
{` ${t(
getTranslationID("signupPage.message.inputEmptyError")
)}
)}`}
</span>
)}
</dd>
@ -369,8 +372,9 @@ const SignupInput: React.FC = (): JSX.Element => {
}}
>
{t(getTranslationID("signupPage.label.termsLink"))}
</a>{" "}
{t(getTranslationID("signupPage.label.termsLinkFor"))} <br />
</a>
{` ${t(getTranslationID("signupPage.label.termsLinkFor"))} `}
<br />
<label htmlFor="check-box">
<input
id="check-box"

View File

@ -0,0 +1,188 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import Header from "components/header";
import { AppDispatch } from "app/store";
import { useDispatch, useSelector } from "react-redux";
import styles from "styles/app.module.scss";
import { TIERS } from "components/auth/constants";
import Footer from "components/footer";
import { useCallback, useEffect, useState } from "react";
import {
getAccountInfoMinimalAccessAsync,
getTermsInfoAsync,
updateAcceptedVersionAsync,
selectTier,
selectTermVersions,
} from "features//terms";
import { selectLocalStorageKeyforIdToken } from "features/login";
import { useNavigate } from "react-router-dom";
const TermsPage: React.FC = (): JSX.Element => {
const [t] = useTranslation();
const dispatch: AppDispatch = useDispatch();
const navigate = useNavigate();
const updateAccceptVersions = useSelector(selectTermVersions);
const localStorageKeyforIdToken = useSelector(
selectLocalStorageKeyforIdToken
);
const tier = useSelector(selectTier);
const [isCheckedEula, setIsCheckedEula] = useState(false);
const [isCheckedDpa, setIsCheckedDpa] = useState(false);
const [isClickedEulaLink, setIsClickedEulaLink] = useState(false);
const [isClickedDpaLink, setIsClickedDpaLink] = useState(false);
// 画面起動時
useEffect(() => {
dispatch(getTermsInfoAsync());
if (localStorageKeyforIdToken) {
dispatch(getAccountInfoMinimalAccessAsync({ localStorageKeyforIdToken }));
} else {
// ログイン画面を経由していないため、トップページに遷移する
navigate("/");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// ユーザーが第5階層であるかどうかを判定する(アクセストークンから自分の階層を取得できないので自前で作成)
const isTier5 = () => TIERS.TIER5.includes(tier.toString());
// ボタン押下可否判定ロジック
const canClickButton = () => {
if (isTier5()) {
return isCheckedEula;
}
return isCheckedEula && isCheckedDpa;
};
// ボタン押下時処理
const onAcceptTermsOfUse = useCallback(async () => {
if (
localStorageKeyforIdToken &&
updateAccceptVersions.acceptedVerDPA !== "" &&
updateAccceptVersions.acceptedVerEULA !== ""
) {
const { meta } = await dispatch(
updateAcceptedVersionAsync({
tier,
localStorageKeyforIdToken,
updateAccceptVersions,
})
);
// 同意済バージョンが更新できたら、再度トークン生成を行う
if (meta.requestStatus === "fulfilled") {
navigate("/login");
}
}
}, [
navigate,
localStorageKeyforIdToken,
updateAccceptVersions,
tier,
dispatch,
]);
return (
<div className={styles.wrap}>
<Header />
<main className={styles.main}>
<div className={styles.mainSmall}>
<div>
<h1 className={`${styles.marginBtm1} ${styles.alignCenter}`}>
{t(getTranslationID("termsPage.label.title"))}
</h1>
</div>
<section className={styles.form}>
<form action="" name="" method="">
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dd className={`${styles.full} ${styles.alignCenter}`}>
<p>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
href="/" /* TODO Eula用の利用規約リンクが決定したら設定を行う */
target="_blank"
className={styles.linkTx}
onClick={() => setIsClickedEulaLink(true)}
>
{t(getTranslationID("termsPage.label.linkOfEula"))}
</a>
{` ${t(getTranslationID("termsPage.label.forOdds"))}`}
</p>
<p>
<label>
<input
type="checkbox"
checked={isCheckedEula}
className={styles.formCheck}
value=""
onChange={(e) => setIsCheckedEula(e.target.checked)}
disabled={!isClickedEulaLink}
/>
{t(
getTranslationID("termsPage.label.checkBoxForConsent")
)}
</label>
</p>
</dd>
{/* 第五階層以外の場合はEulaのリンクをあわせて表示する */}
{!isTier5() && (
<dd className={`${styles.full} ${styles.alignCenter}`}>
<p>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
href="/" /* TODO Dpa用の利用規約リンクが決定したら設定を行う */
target="_blank"
className={styles.linkTx}
onClick={() => setIsClickedDpaLink(true)}
>
{t(getTranslationID("termsPage.label.linkOfDpa"))}
</a>
{` ${t(getTranslationID("termsPage.label.forOdds"))}`}
</p>
<p>
<label>
<input
type="checkbox"
checked={isCheckedDpa}
className={styles.formCheck}
value=""
onChange={(e) => setIsCheckedDpa(e.target.checked)}
disabled={!isClickedDpaLink}
/>
{t(
getTranslationID("termsPage.label.checkBoxForConsent")
)}
</label>
</p>
</dd>
)}
<dd className={`${styles.full} ${styles.alignCenter}`}>
<p>
<input
type="button"
name="submit"
value={t(getTranslationID("termsPage.label.button"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
canClickButton() ? styles.isActive : ""
}`}
onClick={onAcceptTermsOfUse}
/>
</p>
</dd>
</dl>
</form>
</section>
</div>
</main>
<Footer />
</div>
);
};
export default TermsPage;

View File

@ -200,7 +200,11 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
className={styles.formInput}
value={addUser.authorId ?? undefined}
onChange={(e) => {
dispatch(changeAuthorId({ authorId: e.target.value }));
dispatch(
changeAuthorId({
authorId: e.target.value.toUpperCase(),
})
);
}}
/>
{isPushCreateButton && hasErrorEmptyAuthorId && (

View File

@ -184,7 +184,9 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
className={styles.formInput}
onChange={(e) => {
dispatch(
changeUpdateAuthorId({ authorId: e.target.value })
changeUpdateAuthorId({
authorId: e.target.value.toUpperCase(),
})
);
}}
/>

View File

@ -1,7 +1,7 @@
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import Footer from "components/footer";
import Header from "components/header";
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { getTranslationID } from "translation";
import styles from "styles/app.module.scss";
import undo from "assets/images/undo.svg";
@ -18,6 +18,7 @@ import {
selectIsLoading,
selectWorktypes,
selectActiveWorktypeId,
deleteWorktypeAsync,
} from "features/workflow/worktype";
import { AppDispatch } from "app/store";
import { AddWorktypeIdPopup } from "./addWorktypeIdPopup";
@ -86,6 +87,23 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedActiveWorktypeId]);
// 削除ボタン押下時の処理
const onDeleteWoktype = useCallback(
async (worktypeId: number) => {
if (
/* eslint-disable-next-line no-alert */
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const { meta } = await dispatch(deleteWorktypeAsync({ worktypeId }));
if (meta.requestStatus === "fulfilled") {
dispatch(listWorktypesAsync());
}
},
[dispatch, t]
);
return (
<>
<AddWorktypeIdPopup
@ -253,9 +271,10 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
</a>
</li>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
// onClick={}
onClick={() => onDeleteWoktype(worktype.id)}
>
{t(getTranslationID("common.label.delete"))}
</a>

View File

@ -0,0 +1,313 @@
import React, { useCallback, useEffect, useState } from "react";
import { AppDispatch } from "app/store";
import progress_activit from "assets/images/progress_activit.svg";
import {
addAssignee,
removeAssignee,
changeAuthor,
changeTemplate,
changeWorktype,
clearWorkflow,
selectIsAddLoading,
selectWorkflowAssinee,
selectWorkflowError,
selectWorkflowRelations,
selectSelectedWorkflow,
selectAuthorId,
selectWorktypeId,
setAssignees,
selectTemplateId,
} from "features/workflow";
import {
getworkflowRelationsAsync,
listWorkflowAsync,
updateWorkflowAsync,
} from "features/workflow/operations";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import styles from "styles/app.module.scss";
import { getTranslationID } from "translation";
import close from "../../assets/images/close.svg";
interface EditWorkflowPopupProps {
onClose: () => void;
}
export const EditWorkflowPopup: React.FC<EditWorkflowPopupProps> = (
props
): JSX.Element => {
const { onClose } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
// 保存ボタンを押したかどうか
const [isPushEditButton, setIsPushEditButton] = useState<boolean>(false);
const workflow = useSelector(selectSelectedWorkflow);
const authorId = useSelector(selectAuthorId);
const worktypeId = useSelector(selectWorktypeId);
const templateId = useSelector(selectTemplateId);
const workflowRelations = useSelector(selectWorkflowRelations);
const { poolAssignees, selectedAssignees } = useSelector(
selectWorkflowAssinee
);
const isLoading = useSelector(selectIsAddLoading);
const { hasAuthorIdEmptyError, hasSelectedWorkflowAssineeEmptyError } =
useSelector(selectWorkflowError);
useEffect(() => {
dispatch(getworkflowRelationsAsync());
// ポップアップのアンマウント時に初期化を行う
return () => {
dispatch(clearWorkflow());
setIsPushEditButton(false);
};
}, [dispatch]);
useEffect(() => {
dispatch(changeAuthor({ authorId: workflow?.author?.id }));
dispatch(changeWorktype({ worktypeId: workflow?.worktype?.id }));
dispatch(changeTemplate({ templateId: workflow?.template?.id }));
dispatch(setAssignees({ assignees: workflow?.typists ?? [] }));
}, [dispatch, workflow]);
const changeWorktypeId = useCallback(
(target: string) => {
// 空文字の場合はundefinedをdispatchする
if (target === "") {
dispatch(changeWorktype({ worktypeId: undefined }));
} else if (!Number.isNaN(Number(target))) {
dispatch(changeWorktype({ worktypeId: Number(target) }));
}
},
[dispatch]
);
const changeTemplateId = useCallback(
(target: string) => {
// 空文字の場合はundefinedをdispatchする
if (target === "") {
dispatch(changeTemplate({ templateId: undefined }));
} else if (!Number.isNaN(Number(target))) {
dispatch(changeTemplate({ templateId: Number(target) }));
}
},
[dispatch]
);
const changeAuthorId = useCallback(
(target: string) => {
if (!Number.isNaN(target)) {
dispatch(changeAuthor({ authorId: Number(target) }));
}
},
[dispatch]
);
// 保存ボタン押下時の処理
const handleSave = useCallback(async () => {
setIsPushEditButton(true);
// エラーチェック
if (hasAuthorIdEmptyError || hasSelectedWorkflowAssineeEmptyError) {
return;
}
const { meta } = await dispatch(updateWorkflowAsync());
if (meta.requestStatus === "fulfilled") {
onClose();
dispatch(listWorkflowAsync());
}
}, [
dispatch,
hasAuthorIdEmptyError,
hasSelectedWorkflowAssineeEmptyError,
onClose,
]);
return (
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("workflowPage.label.editRoutingRule"))}
{/* 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={onClose}
/>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>{t(getTranslationID("workflowPage.label.authorID"))}</dt>
<dd>
<select
className={styles.formInput}
value={authorId}
onChange={(e) => {
changeAuthorId(e.target.value);
}}
>
<option value="" hidden>
{`-- ${t(
getTranslationID("workflowPage.label.selectAuthor")
)} --`}
</option>
{workflowRelations?.authors.map((author) => (
<option key={author.authorId} value={author.id}>
{author.authorId}
</option>
))}
</select>
{isPushEditButton && hasAuthorIdEmptyError && (
<span className={styles.formError}>
{t(getTranslationID("workflowPage.message.inputEmptyError"))}
</span>
)}
</dd>
<dt className={styles.overLine}>
{t(getTranslationID("workflowPage.label.worktypeOptional"))}
</dt>
<dd>
<select
className={styles.formInput}
value={worktypeId}
onChange={(e) => {
changeWorktypeId(e.target.value);
}}
>
<option value="" hidden>
{`-- ${t(
getTranslationID("workflowPage.label.selectWorktypeId")
)} --`}
</option>
<option value="">
{`-- ${t(getTranslationID("common.label.notSelected"))} --`}
</option>
{workflowRelations?.worktypes.map((worktype) => (
<option key={worktype.id} value={worktype.id}>
{worktype.worktypeId}
</option>
))}
</select>
</dd>
<dt className={styles.formTitle}>
{t(getTranslationID("typistGroupSetting.label.transcriptionist"))}
</dt>
<dd className={`${styles.formChange} ${styles.last}`}>
<ul className={styles.chooseMember}>
<li className={styles.changeTitle}>
{t(getTranslationID("workflowPage.label.selected"))}
</li>
{selectedAssignees?.map((x) => {
const key = `${x.typistName}_${
x.typistUserId ?? x.typistGroupId
}`;
return (
<li key={key}>
<input
type="checkbox"
className={styles.formCheck}
value={x.typistName}
id={key}
checked
onClick={() => {
dispatch(removeAssignee({ assignee: x }));
}}
/>
<label htmlFor={key} title="Remove">
{x.typistName}
</label>
</li>
);
})}
</ul>
<p />
<ul className={styles.holdMember}>
<li className={styles.changeTitle}>
{t(getTranslationID("workflowPage.label.pool"))}
</li>
{poolAssignees?.map((x) => {
const key = `${x.typistName}_${
x.typistUserId ?? x.typistGroupId
}`;
return (
<li key={key}>
<input
type="checkbox"
className={styles.formCheck}
value={x.typistName}
id={key}
onClick={() => dispatch(addAssignee({ assignee: x }))}
/>
<label htmlFor={key} title="Add">
{x.typistName}
</label>
</li>
);
})}
</ul>
{isPushEditButton && hasSelectedWorkflowAssineeEmptyError && (
<span
className={styles.formError}
style={{ margin: "0px 30px 0px 30px" }}
>
{t(
getTranslationID(
"workflowPage.message.selectedTypistEmptyError"
)
)}
</span>
)}
</dd>
<dt className={styles.overLine}>
{t(getTranslationID("workflowPage.label.templateOptional"))}
</dt>
<dd className={styles.last}>
<select
className={styles.formInput}
value={templateId}
onChange={(e) => {
changeTemplateId(e.target.value);
}}
>
<option value="" hidden>
{`-- ${t(
getTranslationID("workflowPage.label.selectTemplate")
)} --`}
</option>
<option value="">
{`-- ${t(getTranslationID("common.label.notSelected"))} --`}
</option>
{workflowRelations?.templates.map((template) => (
<option
key={`${template.name}_${template.id}`}
value={template.id}
>
{template.name}
</option>
))}
</select>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
value={t(getTranslationID("common.label.save"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading ? styles.isActive : ""
}`}
onClick={handleSave}
/>
{isLoading && (
<img
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import Header from "components/header";
import Footer from "components/footer";
import styles from "styles/app.module.scss";
@ -10,23 +10,50 @@ import groupSettingImg from "assets/images/group_setting.svg";
import { AppDispatch } from "app/store";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { listWorkflowAsync } from "features/workflow/operations";
import { selectIsLoading, selectWorkflows } from "features/workflow";
import {
deleteWorkflowAsync,
listWorkflowAsync,
changeSelectedWorkflow,
selectIsLoading,
selectWorkflows,
} from "features/workflow";
import progress_activit from "assets/images/progress_activit.svg";
import { getTranslationID } from "translation";
import { AddWorkflowPopup } from "./addworkflowPopup";
import { EditWorkflowPopup } from "./editworkflowPopup";
const WorkflowPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
// 追加Popupの表示制御
const [isShowAddPopup, setIsShowAddPopup] = useState<boolean>(false);
// 編集Popupの表示制御
const [isShowEditPopup, setIsShowEditPopup] = useState<boolean>(false);
const workflows = useSelector(selectWorkflows);
const isLoading = useSelector(selectIsLoading);
useEffect(() => {
dispatch(listWorkflowAsync());
}, [dispatch]);
// ワークフロー削除
const onDeleteWorkflow = useCallback(
async (workflowId: number) => {
if (
/* eslint-disable-next-line no-alert */
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const { meta } = await dispatch(deleteWorkflowAsync({ workflowId }));
if (meta.requestStatus === "fulfilled") {
dispatch(listWorkflowAsync());
}
},
[dispatch, t]
);
return (
<>
{isShowAddPopup && (
@ -36,6 +63,13 @@ const WorkflowPage: React.FC = (): JSX.Element => {
}}
/>
)}
{isShowEditPopup && (
<EditWorkflowPopup
onClose={() => {
setIsShowEditPopup(false);
}}
/>
)}
<div className={styles.wrap}>
<Header userName="XXXXXX" />
<UpdateTokenTimer />
@ -136,14 +170,29 @@ const WorkflowPage: React.FC = (): JSX.Element => {
<td className={styles.clm0}>
<ul className={styles.menuInTable}>
<li>
<a href="">
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
onClick={() => {
dispatch(
changeSelectedWorkflow({
workflowId: workflow.id,
})
);
setIsShowEditPopup(true);
}}
>
{t(
getTranslationID("workflowPage.label.editRule")
)}
</a>
</li>
<li>
<a href="">
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
onClick={() => {
onDeleteWorkflow(workflow.id);
}}
>
{t(getTranslationID("common.label.delete"))}
</a>
</li>

View File

@ -293,6 +293,31 @@ h5 {
width: 1.4rem;
vertical-align: top;
}
.accountSignout {
display: inline-block;
margin-left: 1rem;
color: #999999;
font-size: 0.8rem;
cursor: pointer;
-moz-transition: all 0.3s ease-out;
-ms-transition: all 0.3s ease-out;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.accountSignout .accountIcon {
opacity: 0.5;
margin-right: 0.2rem;
-moz-transition: all 0.3s ease-out;
-ms-transition: all 0.3s ease-out;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.accountSignout:hover {
color: #333333;
}
.accountSignout:hover .accountIcon {
opacity: 1;
}
_:-ms-lang(x)::-ms-backdrop,
.header {
@ -647,6 +672,7 @@ h3 + .brCrumb .tlIcon {
padding: 0.6rem 0;
margin-right: 1rem;
cursor: pointer;
white-space: pre-line;
}
.form label:has(input:disabled) {
cursor: default;
@ -870,6 +896,24 @@ h3 + .brCrumb .tlIcon {
text-align: right;
word-break: break-all;
}
.listDocument {
margin-bottom: 3rem;
}
.listDocument li {
padding: 0 0 0 1.5rem;
margin-bottom: 1rem;
position: relative;
}
.listDocument li::before {
content: "";
width: 0.5rem;
height: 0.5rem;
background: #333333;
border-radius: 50%;
position: absolute;
top: 0.2rem;
left: 0;
}
.boxFlex {
display: flex;
@ -1428,7 +1472,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license > div,
.dictation > div,
.partners > div,
.workflow > div {
.workflow > div,
.support > div {
padding: 0 2rem;
position: relative;
}
@ -1437,7 +1482,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license > div .icLoading,
.dictation > div .icLoading,
.partners > div .icLoading,
.workflow > div .icLoading {
.workflow > div .icLoading,
.support > div .icLoading {
top: 5.5rem;
left: calc(50% - 25px);
}
@ -1446,7 +1492,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table tr.tableHeader th.clm0,
.dictation .table tr.tableHeader th.clm0,
.partners .table tr.tableHeader th.clm0,
.workflow .table tr.tableHeader th.clm0 {
.workflow .table tr.tableHeader th.clm0,
.support .table tr.tableHeader th.clm0 {
width: 0px;
padding: 0 0;
}
@ -1455,7 +1502,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table tr:not(.tableHeader),
.dictation .table tr:not(.tableHeader),
.partners .table tr:not(.tableHeader),
.workflow .table tr:not(.tableHeader) {
.workflow .table tr:not(.tableHeader),
.support .table tr:not(.tableHeader) {
position: relative;
}
.account .table tr:not(.tableHeader):hover .menuInTable,
@ -1463,7 +1511,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table tr:not(.tableHeader):hover .menuInTable,
.dictation .table tr:not(.tableHeader):hover .menuInTable,
.partners .table tr:not(.tableHeader):hover .menuInTable,
.workflow .table tr:not(.tableHeader):hover .menuInTable {
.workflow .table tr:not(.tableHeader):hover .menuInTable,
.support .table tr:not(.tableHeader):hover .menuInTable {
opacity: 1;
}
.account .table tr:not(.tableHeader).isSelected,
@ -1471,7 +1520,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table tr:not(.tableHeader).isSelected,
.dictation .table tr:not(.tableHeader).isSelected,
.partners .table tr:not(.tableHeader).isSelected,
.workflow .table tr:not(.tableHeader).isSelected {
.workflow .table tr:not(.tableHeader).isSelected,
.support .table tr:not(.tableHeader).isSelected {
background: #0084b2;
color: #ffffff;
}
@ -1480,7 +1530,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table tr:not(.tableHeader).isSelected:hover,
.dictation .table tr:not(.tableHeader).isSelected:hover,
.partners .table tr:not(.tableHeader).isSelected:hover,
.workflow .table tr:not(.tableHeader).isSelected:hover {
.workflow .table tr:not(.tableHeader).isSelected:hover,
.support .table tr:not(.tableHeader).isSelected:hover {
color: #ffffff;
}
.account .table tr:not(.tableHeader).isSelected .menuInTable,
@ -1488,7 +1539,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table tr:not(.tableHeader).isSelected .menuInTable,
.dictation .table tr:not(.tableHeader).isSelected .menuInTable,
.partners .table tr:not(.tableHeader).isSelected .menuInTable,
.workflow .table tr:not(.tableHeader).isSelected .menuInTable {
.workflow .table tr:not(.tableHeader).isSelected .menuInTable,
.support .table tr:not(.tableHeader).isSelected .menuInTable {
display: block;
}
.account .table td,
@ -1496,7 +1548,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table td,
.dictation .table td,
.partners .table td,
.workflow .table td {
.workflow .table td,
.support .table td {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
@ -1508,7 +1561,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table td.clm0,
.dictation .table td.clm0,
.partners .table td.clm0,
.workflow .table td.clm0 {
.workflow .table td.clm0,
.support .table td.clm0 {
width: 0px;
padding: 0 0;
overflow: visible;
@ -1521,7 +1575,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table.user,
.dictation .table.user,
.partners .table.user,
.workflow .table.user {
.workflow .table.user,
.support .table.user {
margin-bottom: 5rem;
}
.account .table.user th::after,
@ -1529,7 +1584,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table.user th::after,
.dictation .table.user th::after,
.partners .table.user th::after,
.workflow .table.user th::after {
.workflow .table.user th::after,
.support .table.user th::after {
display: none;
}
.account .table.user tr:not(.tableHeader) td,
@ -1537,7 +1593,8 @@ _:-ms-lang(x)::-ms-backdrop,
.license .table.user tr:not(.tableHeader) td,
.dictation .table.user tr:not(.tableHeader) td,
.partners .table.user tr:not(.tableHeader) td,
.workflow .table.user tr:not(.tableHeader) td {
.workflow .table.user tr:not(.tableHeader) td,
.support .table.user tr:not(.tableHeader) td {
padding-bottom: 2rem;
vertical-align: top;
}
@ -2500,6 +2557,9 @@ tr.isSelected .menuInTable li a.isDisable {
padding: 0 3rem;
}
.txContents {
padding: 3rem;
}
.txNormal {
font-size: 16px;
line-height: 1.7;

View File

@ -7,6 +7,7 @@ declare const classNames: {
readonly isActive: "isActive";
readonly accountInfo: "accountInfo";
readonly accountIcon: "accountIcon";
readonly accountSignout: "accountSignout";
readonly main: "main";
readonly mainSmall: "mainSmall";
readonly mainLogin: "mainLogin";
@ -45,10 +46,10 @@ declare const classNames: {
readonly formBack: "formBack";
readonly formButtonTx: "formButtonTx";
readonly formDone: "formDone";
readonly formDelete: "formDelete";
readonly formTrash: "formTrash";
readonly listVertical: "listVertical";
readonly listHeader: "listHeader";
readonly listDocument: "listDocument";
readonly boxFlex: "boxFlex";
readonly aru: "aru";
readonly btw: "btw";
@ -101,11 +102,11 @@ declare const classNames: {
readonly dictation: "dictation";
readonly partners: "partners";
readonly workflow: "workflow";
readonly support: "support";
readonly clm0: "clm0";
readonly menuInTable: "menuInTable";
readonly isSelected: "isSelected";
readonly formCheckToggle: "formCheckToggle";
readonly toggleBase: "toggleBase";
readonly alignRight: "alignRight";
readonly menuAction: "menuAction";
readonly inTable: "inTable";
readonly menuLink: "menuLink";
@ -197,7 +198,6 @@ declare const classNames: {
readonly worktype: "worktype";
readonly selectMenu: "selectMenu";
readonly alignLeft: "alignLeft";
readonly alignRight: "alignRight";
readonly floatNone: "floatNone";
readonly floatLeft: "floatLeft";
readonly floatRight: "floatRight";
@ -217,8 +217,8 @@ declare const classNames: {
readonly paddSide1: "paddSide1";
readonly paddSide2: "paddSide2";
readonly paddSide3: "paddSide3";
readonly txContents: "txContents";
readonly txIcon: "txIcon";
readonly txWswrap: "txWswrap";
readonly required: "required";
};
export = classNames;

View File

@ -358,6 +358,7 @@
"label": {
"title": "Arbeitsablauf",
"addRoutingRule": "(de)Add Routing Rule",
"editRoutingRule": "(de)Edit Routing Rule",
"templateSetting": "(de)Template Setting",
"worktypeIdSetting": "(de)WorktypeID Setting",
"typistGroupSetting": "(de)Transcriptionist Group Setting",
@ -428,7 +429,8 @@
"optionItemInvalidError": "(de)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "(de)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "(de)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "(de)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
"updateActiveWorktypeFailedError": "(de)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください",
"worktypeInUseError": "(de)このWorktype IDはルーティングルールで使用されているため削除できません。"
}
},
"templateFilePage": {
@ -497,5 +499,15 @@
"message": "(de)Your account has been deleted. Thank you for using our services.",
"backToTopPageLink": "(de)Back to TOP Page"
}
},
"termsPage": {
"label": {
"title": "(de)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(de)Click here to read the terms of use.",
"linkOfDpa": "(de)Click here to read the terms of use.",
"checkBoxForConsent": "(de)Yes, I agree to the terms of use.",
"forOdds": "(de)for ODDS.",
"button": "(de)Continue"
}
}
}

View File

@ -358,6 +358,7 @@
"label": {
"title": "Workflow",
"addRoutingRule": "Add Routing Rule",
"editRoutingRule": "Edit Routing Rule",
"templateSetting": "Template Setting",
"worktypeIdSetting": "WorktypeID Setting",
"typistGroupSetting": "Transcriptionist Group Setting",
@ -428,7 +429,8 @@
"optionItemInvalidError": "Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
"updateActiveWorktypeFailedError": "Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください",
"worktypeInUseError": "このWorktype IDはルーティングルールで使用されているため削除できません。"
}
},
"templateFilePage": {
@ -497,5 +499,15 @@
"message": "Your account has been deleted. Thank you for using our services.",
"backToTopPageLink": "Back to TOP Page"
}
},
"termsPage": {
"label": {
"title": "Terms of Use has updated. Please confirm again.",
"linkOfEula": "Click here to read the terms of use.",
"linkOfDpa": "Click here to read the terms of use.",
"checkBoxForConsent": "Yes, I agree to the terms of use.",
"forOdds": "for ODDS.",
"button": "Continue"
}
}
}

View File

@ -358,6 +358,7 @@
"label": {
"title": "flujo de trabajo",
"addRoutingRule": "(es)Add Routing Rule",
"editRoutingRule": "(es)Edit Routing Rule",
"templateSetting": "(es)Template Setting",
"worktypeIdSetting": "(es)WorktypeID Setting",
"typistGroupSetting": "(es)Transcriptionist Group Setting",
@ -428,7 +429,8 @@
"optionItemInvalidError": "(es)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "(es)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "(es)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "(es)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
"updateActiveWorktypeFailedError": "(es)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください",
"worktypeInUseError": "(es)このWorktype IDはルーティングルールで使用されているため削除できません。"
}
},
"templateFilePage": {
@ -497,5 +499,15 @@
"message": "(es)Your account has been deleted. Thank you for using our services.",
"backToTopPageLink": "(es)Back to TOP Page"
}
},
"termsPage": {
"label": {
"title": "(es)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(es)Click here to read the terms of use.",
"linkOfDpa": "(es)Click here to read the terms of use.",
"checkBoxForConsent": "(es)Yes, I agree to the terms of use.",
"forOdds": "(es)for ODDS.",
"button": "(es)Continue"
}
}
}

View File

@ -358,6 +358,7 @@
"label": {
"title": "Flux de travail",
"addRoutingRule": "(fr)Add Routing Rule",
"editRoutingRule": "(fr)Edit Routing Rule",
"templateSetting": "(fr)Template Setting",
"worktypeIdSetting": "(fr)WorktypeID Setting",
"typistGroupSetting": "(fr)Transcriptionist Group Setting",
@ -428,7 +429,8 @@
"optionItemInvalidError": "(fr)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "(fr)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "(fr)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "(fr)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
"updateActiveWorktypeFailedError": "(fr)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください",
"worktypeInUseError": "(fr)このWorktype IDはルーティングルールで使用されているため削除できません。"
}
},
"templateFilePage": {
@ -497,5 +499,15 @@
"message": "(fr)Your account has been deleted. Thank you for using our services.",
"backToTopPageLink": "(fr)Back to TOP Page"
}
},
"termsPage": {
"label": {
"title": "(fr)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(fr)Click here to read the terms of use.",
"linkOfDpa": "(fr)Click here to read the terms of use.",
"checkBoxForConsent": "(fr)Yes, I agree to the terms of use.",
"forOdds": "(fr)for ODDS.",
"button": "(fr)Continue"
}
}
}

View File

@ -0,0 +1,33 @@
-- +migrate Up
ALTER TABLE `checkout_permission` DROP FOREIGN KEY `checkout_permission_fk_task_id`;
ALTER TABLE `tasks` DROP FOREIGN KEY `tasks_fk_account_id`;
ALTER TABLE `template_files` DROP FOREIGN KEY `template_files_fk_account_id`;
ALTER TABLE `option_items` DROP FOREIGN KEY `option_items_fk_worktype_id`;
ALTER TABLE `worktypes` DROP FOREIGN KEY `worktypes_fk_account_id`;
ALTER TABLE `audio_option_items` DROP FOREIGN KEY `audio_option_items_fk_audio_file_id`;
ALTER TABLE `audio_files` DROP FOREIGN KEY `audio_files_fk_account_id`;
ALTER TABLE `user_group_member` DROP FOREIGN KEY `user_group_member_fk_user_group_id`;
ALTER TABLE `user_group` DROP FOREIGN KEY `user_group_fk_account_id`;
ALTER TABLE `license_allocation_history` DROP FOREIGN KEY `license_allocation_history_fk_account_id`;
ALTER TABLE `card_licenses` DROP FOREIGN KEY `card_licenses_fk_license_id`;
ALTER TABLE `licenses` DROP FOREIGN KEY `licenses_fk_account_id`;
ALTER TABLE `license_orders` DROP FOREIGN KEY `license_orders_fk_from_account_id`;
ALTER TABLE `sort_criteria` DROP FOREIGN KEY `sort_criteria_fk_user_id`;
ALTER TABLE `users` DROP FOREIGN KEY `users_fk_account_id`;
-- +migrate Down
ALTER TABLE `checkout_permission` ADD CONSTRAINT `checkout_permission_fk_task_id` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `tasks` ADD CONSTRAINT `tasks_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `template_files` ADD CONSTRAINT `template_files_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `option_items` ADD CONSTRAINT `option_items_fk_worktype_id` FOREIGN KEY (`worktype_id`) REFERENCES `worktypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `worktypes` ADD CONSTRAINT `worktypes_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `audio_option_items` ADD CONSTRAINT `audio_option_items_fk_audio_file_id` FOREIGN KEY (`audio_file_id`) REFERENCES `audio_files` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `audio_files` ADD CONSTRAINT `audio_files_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `user_group_member` ADD CONSTRAINT `user_group_member_fk_user_group_id` FOREIGN KEY (`user_group_id`) REFERENCES `user_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `user_group` ADD CONSTRAINT `user_group_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `license_allocation_history` ADD CONSTRAINT `license_allocation_history_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `card_licenses` ADD CONSTRAINT `card_licenses_fk_license_id` FOREIGN KEY (`license_id`) REFERENCES `licenses` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `licenses` ADD CONSTRAINT `licenses_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `license_orders` ADD CONSTRAINT `license_orders_fk_from_account_id` FOREIGN KEY (`from_account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `sort_criteria` ADD CONSTRAINT `sort_criteria_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `users` ADD CONSTRAINT `users_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,9 @@
-- +migrate Up
insert into terms(terms.document_type, terms.version) values('EURA', 'V0.1');
insert into terms(terms.document_type, terms.version) values('DPA', 'V0.1');
commit;
-- +migrate Down
delete from terms where terms.document_type = 'EURA' and terms.version = 'V0.1';
delete from terms where terms.document_type = 'DPA' and terms.version = 'V0.1';
commit;

View File

@ -990,6 +990,59 @@
"security": [{ "bearer": [] }]
}
},
"/accounts/worktypes/{id}/delete": {
"post": {
"operationId": "deleteWorktype",
"summary": "",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"description": "Worktypeの内部ID",
"schema": { "type": "number" }
}
],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteWorktypeResponse"
}
}
}
},
"400": {
"description": "指定WorktypeIDが削除済み / 指定WorktypeIDがWorkflowで使用中",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["accounts"],
"security": [{ "bearer": [] }]
}
},
"/accounts/worktypes/{id}/option-items": {
"get": {
"operationId": "getOptionItems",
@ -3156,6 +3209,14 @@
}
}
},
"400": {
"description": "パラメータ不正エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"401": {
"description": "認証エラー",
"content": {
@ -3229,7 +3290,7 @@
}
},
"/terms": {
"post": {
"get": {
"operationId": "getTermsInfo",
"summary": "",
"parameters": [],
@ -3698,6 +3759,7 @@
"required": ["worktypeId"]
},
"UpdateWorktypeResponse": { "type": "object", "properties": {} },
"DeleteWorktypeResponse": { "type": "object", "properties": {} },
"GetWorktypeOptionItem": {
"type": "object",
"properties": {
@ -4403,7 +4465,7 @@
"licenseId": { "type": "number" },
"expiryDate": { "format": "date-time", "type": "string" }
},
"required": ["licenseId", "expiryDate"]
"required": ["licenseId"]
},
"GetAllocatableLicensesResponse": {
"type": "object",
@ -4561,7 +4623,24 @@
"required": ["pns", "handler"]
},
"RegisterResponse": { "type": "object", "properties": {} },
"GetTermsInfoResponse": { "type": "object", "properties": {} }
"TermInfo": {
"type": "object",
"properties": {
"documentType": { "type": "string", "description": "利用規約種別" },
"version": { "type": "string", "description": "バージョン" }
},
"required": ["documentType", "version"]
},
"GetTermsInfoResponse": {
"type": "object",
"properties": {
"termsInfo": {
"type": "array",
"items": { "$ref": "#/components/schemas/TermInfo" }
}
},
"required": ["termsInfo"]
}
}
}
}

View File

@ -32,6 +32,7 @@ export const ErrorCodes = [
'E010206', // DBのTierが想定外の値エラー
'E010207', // ユーザーのRole変更不可エラー
'E010208', // ユーザーの暗号化パスワード不足エラー
'E010209', // ユーザーの同意済み利用規約バージョンが最新でないエラー
'E010301', // メールアドレス登録済みエラー
'E010302', // authorId重複エラー
'E010401', // PONumber重複エラー
@ -56,6 +57,7 @@ export const ErrorCodes = [
'E011001', // ワークタイプ重複エラー
'E011002', // ワークタイプ登録上限超過エラー
'E011003', // ワークタイプ不在エラー
'E011004', // ワークタイプ使用中エラー
'E012001', // テンプレートファイル不在エラー
'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
'E013002', // ワークフロー不在エラー

View File

@ -21,6 +21,7 @@ export const errors: Errors = {
E010206: 'Tier from DB is unexpected value Error.',
E010207: 'User role change not allowed Error.',
E010208: 'User encryption password not found Error.',
E010209: 'Accepted term not latest Error.',
E010301: 'This email user already created Error',
E010302: 'This AuthorId already used Error',
E010401: 'This PoNumber already used Error',
@ -45,6 +46,7 @@ export const errors: Errors = {
E011001: 'This WorkTypeID already used Error',
E011002: 'WorkTypeID create limit exceeded Error',
E011003: 'WorkTypeID not found Error',
E011004: 'WorkTypeID is in use Error',
E012001: 'Template file not found Error',
E013001: 'AuthorId and WorktypeId pair already exists Error',
E013002: 'Workflow not found Error',

View File

@ -92,13 +92,15 @@ export class RoleGuard implements CanActivate {
* @returns true/false
*/
checkRole(role: string): boolean {
const { roles } = this.settings;
const settings = this.settings;
if (!settings || !settings.roles) {
return true;
}
const userRoles = role.split(' ');
// Role毎にAccessTokenの権限チェックを行う
for (let i = 0; i < roles.length; i++) {
const role = roles[i];
for (let i = 0; i < settings.roles.length; i++) {
const role = settings.roles[i];
let isValid = false;
if (Array.isArray(role)) {
isValid = role.every((x) => userRoles.includes(x));
@ -172,9 +174,12 @@ export class RoleGuard implements CanActivate {
* @returns true/false
*/
checkTier(tier: number): boolean {
const { tiers } = this.settings;
const settings = this.settings;
if (!settings || !settings.tiers) {
return true;
}
// 宣言された階層中にパラメータの内容が含まれていればtrue
return tiers.includes(tier as (typeof TIERS)[keyof typeof TIERS]);
return settings.tiers.includes(tier as (typeof TIERS)[keyof typeof TIERS]);
}
}

View File

@ -132,7 +132,7 @@ export const getPrivateKey = (configService: ConfigService): string => {
return (
// 開発環境用に改行コードを置換する
// 本番環境では\\nが含まれないため、置換が行われない想定
configService.get<string>('JWT_PRIVATE_KEY')?.replace(/\\n/g, '\n') ?? ''
configService.getOrThrow<string>('JWT_PRIVATE_KEY').replace(/\\n/g, '\n')
);
};
@ -140,6 +140,6 @@ export const getPublicKey = (configService: ConfigService): string => {
return (
// 開発環境用に改行コードを置換する
// 本番環境では\\nが含まれないため、置換が行われない想定
configService.get<string>('JWT_PUBLIC_KEY')?.replace(/\\n/g, '\n') ?? ''
configService.getOrThrow<string>('JWT_PUBLIC_KEY').replace(/\\n/g, '\n')
);
};

View File

@ -15,10 +15,9 @@ export const makePassword = (): string => {
// autoGeneratedPasswordが以上の条件を満たせばvalidがtrueになる
let valid = false;
let autoGeneratedPassword: string;
let autoGeneratedPassword: string = '';
while (!valid) {
autoGeneratedPassword = '';
// パスワードをランダムに決定
while (autoGeneratedPassword.length < passLength) {
// 上で決定したcharsの中からランダムに1文字ずつ追加

View File

@ -34,10 +34,13 @@ import { TemplatesService } from '../../features/templates/templates.service';
import { TemplatesModule } from '../../features/templates/templates.module';
import { WorkflowsService } from '../../features/workflows/workflows.service';
import { WorkflowsModule } from '../../features/workflows/workflows.module';
import { TermsService } from '../../features/terms/terms.service';
import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module';
import { TermsModule } from '../../features/terms/terms.module';
export const makeTestingModule = async (
datasource: DataSource,
): Promise<TestingModule> => {
): Promise<TestingModule | undefined> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [
@ -56,6 +59,7 @@ export const makeTestingModule = async (
LicensesModule,
TemplatesModule,
WorkflowsModule,
TermsModule,
AccountsRepositoryModule,
UsersRepositoryModule,
LicensesRepositoryModule,
@ -71,6 +75,7 @@ export const makeTestingModule = async (
AuthGuardsModule,
SortCriteriaRepositoryModule,
WorktypesRepositoryModule,
TermsRepositoryModule,
],
providers: [
AuthService,
@ -82,6 +87,7 @@ export const makeTestingModule = async (
LicensesService,
TemplatesService,
WorkflowsService,
TermsService,
],
})
.useMocker(async (token) => {

View File

@ -3,6 +3,7 @@ import { DataSource } from 'typeorm';
import { User, UserArchive } from '../../repositories/users/entity/user.entity';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { License } from '../../repositories/licenses/entity/license.entity';
type InitialTestDBState = {
tier1Accounts: { account: Account; users: User[] }[];
@ -57,11 +58,11 @@ export const makeHierarchicalAccounts = async (
}
// 第2階層を作成
{
const { account: tier1 } = state.tier1Accounts.slice().shift();
const tier1 = state.tier1Accounts.slice().shift();
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 2,
parent_account_id: tier1.id,
parent_account_id: tier1?.account.id,
company_name: 'OMDS_US',
});
state.tier2Accounts.push({
@ -72,7 +73,7 @@ export const makeHierarchicalAccounts = async (
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 2,
parent_account_id: tier1.id,
parent_account_id: tier1?.account.id,
company_name: 'OMDS_EU',
});
state.tier2Accounts.push({
@ -201,7 +202,7 @@ export const makeTestAccount = async (
}
// Accountの管理者を設定する
let secondaryAdminUserId = null;
let secondaryAdminUserId: number | null = null;
if (isPrimaryAdminNotExist && !isSecondaryAdminNotExist) {
secondaryAdminUserId = userId;
}
@ -224,6 +225,9 @@ export const makeTestAccount = async (
id: userId,
},
});
if (!account || !admin) {
throw new Error('Unexpected null');
}
return {
account: account,
@ -263,7 +267,9 @@ export const makeTestSimpleAccount = async (
id: result.id,
},
});
if (!account) {
throw new Error('Unexpected null');
}
return account;
};
@ -299,11 +305,15 @@ export const makeTestUser = async (
});
const result = identifiers.pop() as User;
return await datasource.getRepository(User).findOne({
const user = await datasource.getRepository(User).findOne({
where: {
id: result.id,
},
});
if (!user) {
throw new Error('Unexpected null');
}
return user;
};
/**
@ -312,7 +322,10 @@ export const makeTestUser = async (
* @param id ID
* @returns
*/
export const getAccount = async (dataSource: DataSource, id: number) => {
export const getAccount = async (
dataSource: DataSource,
id: number,
): Promise<Account | null> => {
return await dataSource.getRepository(Account).findOne({
where: { id: id },
});
@ -353,7 +366,7 @@ export const getUserFromExternalId = async (
export const getUser = async (
datasource: DataSource,
id: number,
): Promise<User> => {
): Promise<User | null> => {
const user = await datasource.getRepository(User).findOne({
where: {
id: id,
@ -381,3 +394,14 @@ export const getUserArchive = async (
): Promise<UserArchive[]> => {
return await dataSource.getRepository(UserArchive).find();
};
export const getLicenses = async (
datasource: DataSource,
account_id: number,
): Promise<License[]> => {
const licenses = await datasource.getRepository(License).find({
where: {
account_id: account_id,
},
});
return licenses;
};

View File

@ -244,7 +244,7 @@ export const OPTION_ITEM_VALUE_TYPE = {
* @const {string[]}
*/
export const ADB2C_SIGN_IN_TYPE = {
EAMILADDRESS: 'emailAddress',
EMAILADDRESS: 'emailAddress',
} as const;
/**
@ -252,3 +252,12 @@ export const ADB2C_SIGN_IN_TYPE = {
* @const {string}
*/
export const MANUAL_RECOVERY_REQUIRED = '[MANUAL_RECOVERY_REQUIRED]';
/**
*
* @const {string[]}
*/
export const TERM_TYPE = {
EULA: 'EULA',
DPA: 'DPA',
} as const;

View File

@ -2,10 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AccountsController } from './accounts.controller';
import { AccountsService } from './accounts.service';
import { ConfigModule } from '@nestjs/config';
import { AuthService } from '../auth/auth.service';
describe('AccountsController', () => {
let controller: AccountsController;
const mockAccountService = {};
const mockAuthService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -16,10 +18,12 @@ describe('AccountsController', () => {
}),
],
controllers: [AccountsController],
providers: [AccountsService],
providers: [AccountsService, AuthService],
})
.overrideProvider(AccountsService)
.useValue(mockAccountService)
.overrideProvider(AuthService)
.useValue(mockAuthService)
.compile();
controller = module.get<AccountsController>(AccountsController);

View File

@ -8,6 +8,7 @@ import {
UseGuards,
Param,
Query,
HttpException,
} from '@nestjs/common';
import {
ApiOperation,
@ -65,6 +66,8 @@ import {
GetAuthorsResponse,
GetAccountInfoMinimalAccessRequest,
GetAccountInfoMinimalAccessResponse,
DeleteWorktypeRequestParam,
DeleteWorktypeResponse,
} from './types/types';
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
import { AuthGuard } from '../../common/guards/auth/authguards';
@ -74,12 +77,15 @@ import { AccessToken } from '../../common/token';
import jwt from 'jsonwebtoken';
import { makeContext } from '../../common/log';
import { v4 as uuidv4 } from 'uuid';
import { AuthService } from '../auth/auth.service';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('accounts')
@Controller('accounts')
export class AccountsController {
constructor(
private readonly accountService: AccountsService, //private readonly cryptoService: CryptoService,
private readonly authService: AuthService,
) {}
@Post()
@ -194,14 +200,26 @@ export class AccountsController {
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get('me')
async getMyAccount(@Req() req: Request): Promise<GetMyAccountResponse> {
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(payload.userId);
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
//アカウントID取得処理
const accountInfo = await this.accountService.getAccountInfo(
context,
payload.userId,
userId,
);
return accountInfo;
}
@ -231,8 +249,21 @@ export class AccountsController {
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get('authors')
async getAuthors(@Req() req: Request): Promise<GetAuthorsResponse> {
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
const authors = await this.accountService.getAuthors(context, userId);
@ -264,10 +295,23 @@ export class AccountsController {
@UseGuards(AuthGuard)
@Get('typists')
async getTypists(@Req() req: Request): Promise<GetTypistsResponse> {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const typists = await this.accountService.getTypists(payload.userId);
const typists = await this.accountService.getTypists(userId);
return { typists };
}
@ -296,12 +340,23 @@ export class AccountsController {
@UseGuards(AuthGuard)
@Get('typist-groups')
async getTypistGroups(@Req() req: Request): Promise<GetTypistGroupsResponse> {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const typistGroups = await this.accountService.getTypistGroups(
payload.userId,
);
const typistGroups = await this.accountService.getTypistGroups(userId);
return { typistGroups };
}
@ -342,8 +397,22 @@ export class AccountsController {
const { typistGroupId } = param;
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -391,8 +460,22 @@ export class AccountsController {
): Promise<CreateTypistGroupResponse> {
const { typistGroupName, typistIds } = body;
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.accountService.createTypistGroup(
context,
@ -441,8 +524,22 @@ export class AccountsController {
const { typistGroupId } = param;
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -492,10 +589,24 @@ export class AccountsController {
@Body() body: CreatePartnerAccountRequest,
): Promise<CreatePartnerAccountResponse> {
const { companyName, country, email, adminName } = body;
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(payload.userId);
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, tier } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.accountService.createPartnerAccount(
context,
@ -503,8 +614,8 @@ export class AccountsController {
country,
email,
adminName,
payload.userId,
payload.tier,
userId,
tier,
);
return {};
@ -620,15 +731,28 @@ export class AccountsController {
): Promise<IssueLicenseResponse> {
const { orderedAccountId, poNumber } = body;
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, tier } = decodedAccessToken as AccessToken;
const context = makeContext(accessToken.userId);
const context = makeContext(userId);
await this.accountService.issueLicense(
context,
orderedAccountId,
accessToken.userId,
accessToken.tier,
userId,
tier,
poNumber,
);
return {};
@ -688,14 +812,27 @@ export class AccountsController {
@Req() req: Request,
@Body() body: CancelIssueRequest,
): Promise<CancelIssueResponse> {
const token = retrieveAuthorizationToken(req);
const payload = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(payload.userId);
const context = makeContext(userId);
await this.accountService.cancelIssue(
context,
payload.userId,
userId,
body.poNumber,
body.orderedAccountId,
);
@ -723,8 +860,21 @@ export class AccountsController {
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
async getWorktypes(@Req() req: Request): Promise<GetWorktypesResponse> {
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
const worktypes = await this.accountService.getWorktypes(context, userId);
@ -762,8 +912,22 @@ export class AccountsController {
@Body() body: CreateWorktypesRequest,
): Promise<CreateWorktypeResponse> {
const { worktypeId, description } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.accountService.createWorktype(
@ -808,8 +972,22 @@ export class AccountsController {
): Promise<UpdateWorktypeResponse> {
const { worktypeId, description } = body;
const { id } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -824,6 +1002,59 @@ export class AccountsController {
return {};
}
@Post('/worktypes/:id/delete')
@ApiResponse({
status: HttpStatus.OK,
type: DeleteWorktypeResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '指定WorktypeIDが削除済み / 指定WorktypeIDがWorkflowで使用中',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'deleteWorktype' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
async deleteWorktype(
@Req() req: Request,
@Param() param: DeleteWorktypeRequestParam,
): Promise<DeleteWorktypeResponse> {
const { id } = param;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.accountService.deleteWorktype(context, userId, id);
return {};
}
@Get('/worktypes/:id/option-items')
@ApiResponse({
status: HttpStatus.OK,
@ -854,8 +1085,22 @@ export class AccountsController {
@Param() param: GetOptionItemsRequestParam,
): Promise<GetOptionItemsResponse> {
const { id } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -900,8 +1145,22 @@ export class AccountsController {
): Promise<UpdateOptionItemsResponse> {
const { optionItems } = body;
const { id } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -945,8 +1204,22 @@ export class AccountsController {
@Body() body: PostActiveWorktypeRequest,
): Promise<PostActiveWorktypeResponse> {
const { id } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -989,8 +1262,22 @@ export class AccountsController {
@Query() query: GetPartnersRequest,
): Promise<GetPartnersResponse> {
const { limit, offset } = query;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
const response = await this.accountService.getPartners(
@ -1042,8 +1329,22 @@ export class AccountsController {
primaryAdminUserId,
secondryAdminUserId,
} = body;
const token = retrieveAuthorizationToken(req);
const { userId, tier } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, tier } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.accountService.updateAccountInfo(
@ -1056,7 +1357,7 @@ export class AccountsController {
secondryAdminUserId,
);
return;
return {};
}
@Post('/delete')
@ -1088,12 +1389,26 @@ export class AccountsController {
@Body() body: DeleteAccountRequest,
): Promise<DeleteAccountResponse> {
const { accountId } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.accountService.deleteAccountAndData(context, userId, accountId);
return;
return {};
}
@Post('/minimal-access')
@ -1116,11 +1431,22 @@ export class AccountsController {
async getAccountInfoMinimalAccess(
@Body() body: GetAccountInfoMinimalAccessRequest,
): Promise<GetAccountInfoMinimalAccessResponse> {
const context = makeContext(uuidv4());
// IDトークンの検証
const idToken = await this.authService.getVerifiedIdToken(body.idToken);
const isVerified = await this.authService.isVerifiedUser(idToken);
if (!isVerified) {
throw new HttpException(
makeErrorResponse('E010201'),
HttpStatus.BAD_REQUEST,
);
}
// TODO 仮実装。API実装タスクで本実装する。
// const idToken = await this.authService.getVerifiedIdToken(body.idToken);
// await this.accountService.getAccountInfoMinimalAccess(context, idToken);
return;
const context = makeContext(idToken.sub);
const tier = await this.accountService.getAccountInfoMinimalAccess(
context,
idToken.sub,
);
return { tier };
}
}

View File

@ -9,6 +9,7 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module';
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktypes.repository.module';
import { AuthService } from '../auth/auth.service';
@Module({
imports: [
@ -22,6 +23,6 @@ import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktype
BlobstorageModule,
],
controllers: [AccountsController],
providers: [AccountsService],
providers: [AccountsService, AuthService],
})
export class AccountsModule {}

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ import {
GetPartnersResponse,
PostWorktypeOptionItem,
Author,
Partner,
} from './types/types';
import {
DateWithZeroTime,
@ -65,12 +66,15 @@ import {
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
import {
WorktypeIdAlreadyExistsError,
WorktypeIdInUseError,
WorktypeIdMaxCountError,
WorktypeIdNotFoundError,
} from '../../repositories/worktypes/errors/types';
@Injectable()
export class AccountsService {
private readonly mailFrom =
this.configService.getOrThrow<string>('MAIL_FROM');
constructor(
private readonly accountRepository: AccountsRepositoryService,
private readonly licensesRepository: LicensesRepositoryService,
@ -256,9 +260,6 @@ export class AccountsService {
}
try {
// メールの送信元を取得
const from = this.configService.get<string>('MAIL_FROM') ?? '';
// メールの内容を構成
const { subject, text, html } =
await this.sendgridService.createMailContentFromEmailConfirm(
@ -272,7 +273,7 @@ export class AccountsService {
await this.sendgridService.sendMail(
context,
email,
from,
this.mailFrom,
subject,
text,
html,
@ -393,7 +394,7 @@ export class AccountsService {
userInfo.account_id,
);
let parentInfo: Account;
let parentInfo: Account | undefined;
if (accountInfo.parent_account_id) {
parentInfo = await this.accountRepository.findAccountById(
accountInfo.parent_account_id,
@ -480,14 +481,20 @@ export class AccountsService {
const { account_id } = await this.usersRepository.findUserByExternalId(
externalId,
);
const userGroup = await this.userGroupsRepository.getTypistGroup(
account_id,
typistGroupId,
);
const { name, userGroupMembers } =
await this.userGroupsRepository.getTypistGroup(
account_id,
typistGroupId,
);
if (!userGroupMembers) {
throw new TypistGroupNotExistError(
`Typist Group is not exist. typistGroupId: ${typistGroupId}`,
);
}
return {
typistGroupName: userGroup.name,
typistIds: userGroup.userGroupMembers.map((x) => x.user_id),
typistGroupName: name,
typistIds: userGroupMembers.map((x) => x.user_id),
};
} catch (e) {
this.logger.error(`error=${e}`);
@ -540,6 +547,11 @@ export class AccountsService {
const typists = typistUsers.map((x) => {
const user = adb2cUsers.find((adb2c) => adb2c.id === x.external_id);
if (!user) {
throw new Error(
`user not found. externalId: ${x.external_id}, userId: ${x.id}`,
);
}
return {
id: x.id,
name: user.displayName,
@ -585,6 +597,11 @@ export class AccountsService {
);
const authors = authorUsers.map((x) => {
if (!x.author_id) {
throw new Error(
`author_id is Not Found. externalId: ${x.external_id}, userId: ${x.id}`,
);
}
return {
id: x.id,
authorId: x.author_id,
@ -700,8 +717,8 @@ export class AccountsService {
creatorAccountTier + 1,
externalUser.sub,
USER_ROLES.NONE,
null,
null,
undefined,
undefined,
);
account = newAccount;
user = adminUser;
@ -742,7 +759,6 @@ export class AccountsService {
}
try {
const from = this.configService.get<string>('MAIL_FROM') || '';
const { subject, text, html } =
await this.sendgridService.createMailContentFromEmailConfirmForNormalUser(
account.id,
@ -752,7 +768,7 @@ export class AccountsService {
await this.sendgridService.sendMail(
context,
email,
from,
this.mailFrom,
subject,
text,
html,
@ -835,11 +851,19 @@ export class AccountsService {
// 各子アカウントのShortageを算出してreturn用の変数にマージする
const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
let childShortage;
const { allocatableLicenseWithMargin, expiringSoonLicense } =
childPartnerLicenseFromRepository;
let childShortage: number = 0;
if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
childShortage =
childPartnerLicenseFromRepository.allocatableLicenseWithMargin -
childPartnerLicenseFromRepository.expiringSoonLicense;
if (
allocatableLicenseWithMargin === undefined ||
expiringSoonLicense === undefined
) {
throw new Error(
`Tier5 account has no allocatableLicenseWithMargin or expiringSoonLicense. accountId: ${accountId}`,
);
}
childShortage = allocatableLicenseWithMargin - expiringSoonLicense;
} else {
childShortage =
childPartnerLicenseFromRepository.stockLicense -
@ -906,13 +930,13 @@ export class AccountsService {
licenseOrder.issued_at !== null
? new Date(licenseOrder.issued_at)
.toISOString()
.substr(0, 10)
.substring(0, 10)
.replace(/-/g, '/')
: null,
: undefined,
numberOfOrder: licenseOrder.quantity,
orderDate: new Date(licenseOrder.ordered_at)
.toISOString()
.substr(0, 10)
.substring(0, 10)
.replace(/-/g, '/'),
poNumber: licenseOrder.po_number,
status: licenseOrder.status,
@ -1417,6 +1441,81 @@ export class AccountsService {
}
}
/**
*
* @param context
* @param externalId
* @param id
* @returns worktype
*/
async deleteWorktype(
context: Context,
externalId: string,
id: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.deleteWorktype.name} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account, account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
if (!account) {
throw new AccountNotFoundError(
`account not found. externalId: ${externalId}`,
);
}
// ワークタイプを削除する
await this.worktypesRepository.deleteWorktype(accountId, id);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
// 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
HttpStatus.BAD_REQUEST,
);
// 内部IDで指定されたWorktypeがWorkflowで使用中の場合は400エラーを返す
case WorktypeIdInUseError:
throw new HttpException(
makeErrorResponse('E011004'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.deleteWorktype.name}`,
);
}
}
/**
*
* @param context
@ -1558,7 +1657,7 @@ export class AccountsService {
async updateActiveWorktype(
context: Context,
externalId: string,
id: number,
id: number | undefined,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateActiveWorktype.name} | params: { ` +
@ -1625,34 +1724,40 @@ export class AccountsService {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
const partners = await this.accountRepository.getPartners(
const partnersRecords = await this.accountRepository.getPartners(
accountId,
limit,
offset,
);
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
let externalIds = partners.partnersInfo.map(
let externalIds = partnersRecords.partnersInfo.map(
(x) => x.primaryAccountExternalId,
);
externalIds = externalIds.filter((item) => item !== undefined);
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
// DBから取得した情報とADB2Cから取得した情報をマージ
const response = partners.partnersInfo.map((db) => {
const partners = partnersRecords.partnersInfo.map((db): Partner => {
const adb2cUser = adb2cUsers.find(
(adb2c) => db.primaryAccountExternalId === adb2c.id,
);
let primaryAdmin = undefined;
let mail = undefined;
if (adb2cUser) {
primaryAdmin = adb2cUser.displayName;
mail = adb2cUser.identities.find(
(identity) =>
identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
).issuerAssignedId;
if (!adb2cUser) {
throw new Error(
`adb2c user not found. externalId: ${db.primaryAccountExternalId}`,
);
}
const primaryAdmin = adb2cUser.displayName;
const mail = adb2cUser.identities?.find(
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
)?.issuerAssignedId;
if (!mail) {
throw new Error(
`adb2c user mail not found. externalId: ${db.primaryAccountExternalId}`,
);
}
return {
name: db.name,
tier: db.tier,
@ -1665,17 +1770,15 @@ export class AccountsService {
});
return {
total: partners.total,
partners: response,
total: partnersRecords.total,
partners: partners,
};
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.getPartners.name}`);
}
@ -1843,4 +1946,61 @@ export class AccountsService {
`[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`,
);
}
/**
* IDトークンのsubからアカウントの階層情報を取得します
* @param context
* @param externalId
* @returns account info minimal access
*/
async getAccountInfoMinimalAccess(
context: Context,
externalId: string,
): Promise<number> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.getAccountInfoMinimalAccess.name} | params: { externalId: ${externalId} };`,
);
try {
const { account } = await this.usersRepository.findUserByExternalId(
externalId,
);
if (!account) {
throw new AccountNotFoundError(
`Account not found. externalId: ${externalId}`,
);
}
return account.tier;
} catch (e) {
this.logger.error(`[${context.trackingId}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.getAccountInfoMinimalAccess.name}`,
);
}
}
}

View File

@ -30,7 +30,7 @@ export type LicensesRepositoryMockValue = {
orderHistories: LicenseOrder[];
}
| Error;
issueLicense: undefined | Error;
issueLicense: void | Error;
};
export type UsersRepositoryMockValue = {
findUserById: User | Error;
@ -61,10 +61,12 @@ export type ConfigMockValue = {
get: string | Error;
};
export type AccountsRepositoryMockValue = {
getLicenseSummaryInfo: {
licenseSummary: LicenseSummaryInfo;
isStorageAvailable: boolean;
};
getLicenseSummaryInfo:
| {
licenseSummary: LicenseSummaryInfo;
isStorageAvailable: boolean;
}
| Error;
createAccount: { newAccount: Account; adminUser: User } | Error;
};
@ -181,18 +183,7 @@ export const makeLicensesRepositoryMock = (
issueLicense:
issueLicense instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(issueLicense)
: jest
.fn<
Promise<{
context: Context;
orderedAccountId: number;
myAccountId: number;
tier: number;
poNumber: string;
}>,
[]
>()
.mockResolvedValue(issueLicense),
: jest.fn<Promise<void>, []>().mockResolvedValue(issueLicense),
};
};
export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
@ -355,7 +346,7 @@ export const makeDefaultAccountsRepositoryMockValue =
user.created_by = 'test';
user.created_at = new Date();
user.updated_by = null;
user.updated_at = null;
user.updated_at = new Date();
return {
getLicenseSummaryInfo: {
licenseSummary: licenseSummaryInfo,
@ -385,7 +376,7 @@ export const makeDefaultUsersRepositoryMockValue =
user.created_by = 'test';
user.created_at = new Date();
user.updated_by = null;
user.updated_at = null;
user.updated_at = new Date();
const typists: User[] = [];
typists.push(
@ -434,7 +425,7 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user.created_by = 'test';
user.created_at = new Date();
user.updated_by = null;
user.updated_at = null;
user.updated_at = new Date();
return {
getUserGroups: [
@ -444,6 +435,10 @@ export const makeDefaultUserGroupsRepositoryMockValue =
name: 'GroupA',
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
userGroupMembers: null,
},
{
id: 2,
@ -451,6 +446,10 @@ export const makeDefaultUserGroupsRepositoryMockValue =
name: 'GroupB',
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
userGroupMembers: null,
},
],
};

View File

@ -23,14 +23,14 @@ export const getSortCriteria = async (dataSource: DataSource) => {
export const createLicense = async (
datasource: DataSource,
licenseId: number,
expiry_date: Date,
expiry_date: Date | null,
accountId: number,
type: string,
status: string,
allocated_user_id: number,
order_id: number,
deleted_at: Date,
delete_order_id: number,
allocated_user_id: number | null,
order_id: number | null,
deleted_at: Date | null,
delete_order_id: number | null,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
id: licenseId,
@ -54,7 +54,7 @@ export const createLicense = async (
export const createLicenseSetExpiryDateAndStatus = async (
datasource: DataSource,
accountId: number,
expiryDate: Date,
expiryDate: Date | null,
status: string,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
@ -171,19 +171,21 @@ export const createOptionItems = async (
datasource: DataSource,
worktypeId: number,
): Promise<OptionItem[]> => {
const optionItems = [];
const optionItems: OptionItem[] = [];
for (let i = 0; i < 10; i++) {
optionItems.push({
worktype_id: worktypeId,
item_label: '',
default_value_type: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initial_value: '',
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const optionItem = new OptionItem();
{
optionItem.worktype_id = worktypeId;
optionItem.item_label = '';
optionItem.default_value_type = OPTION_ITEM_VALUE_TYPE.DEFAULT;
optionItem.initial_value = '';
optionItem.created_by = 'test_runner';
optionItem.created_at = new Date();
optionItem.updated_by = 'updater';
optionItem.updated_at = new Date();
}
optionItems.push(optionItem);
}
await datasource.getRepository(OptionItem).insert(optionItems);

View File

@ -327,8 +327,8 @@ export class GetOrderHistoriesRequest {
export class LicenseOrder {
@ApiProperty({ description: '注文日付' })
orderDate: string;
@ApiProperty({ description: '発行日付' })
issueDate: string;
@ApiProperty({ description: '発行日付', required: false })
issueDate?: string;
@ApiProperty({ description: '注文数' })
numberOfOrder: number;
@ApiProperty({ description: 'POナンバー' })
@ -497,6 +497,16 @@ export class UpdateWorktypeRequestParam {
id: number;
}
export class DeleteWorktypeRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(0)
id: number;
}
export class DeleteWorktypeResponse {}
export class PostActiveWorktypeRequest {
@ApiProperty({
required: false,

View File

@ -5,12 +5,19 @@ import {
makeAdB2cServiceMock,
makeDefaultAdB2cMockValue,
} from './test/auth.service.mock';
import { ConfigModule } from '@nestjs/config';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.local', '.env'],
isGlobal: true,
}),
],
controllers: [AuthController],
providers: [AuthService],
})

View File

@ -67,6 +67,18 @@ export class AuthController {
const context = makeContext(uuidv4());
// 同意済み利用規約バージョンが最新かチェック
const isAcceptedLatestVersion =
await this.authService.isAcceptedLatestVersion(context, idToken);
// 最新でなければエラー
if (!isAcceptedLatestVersion) {
throw new HttpException(
makeErrorResponse('E010209'),
HttpStatus.UNAUTHORIZED,
);
}
const refreshToken = await this.authService.generateRefreshToken(
context,
idToken,

View File

@ -4,8 +4,14 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module';
@Module({
imports: [ConfigModule, AdB2cModule, UsersRepositoryModule],
imports: [
ConfigModule,
AdB2cModule,
UsersRepositoryModule,
TermsRepositoryModule,
],
controllers: [AuthController],
providers: [AuthService],
})

View File

@ -3,13 +3,22 @@ import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
makeAuthServiceMock,
makeDefaultAdB2cMockValue,
makeDefaultConfigValue,
makeDefaultGetPublicKeyFromJwk,
} from './test/auth.service.mock';
import { DataSource } from 'typeorm';
import { makeContext } from '../../common/log';
import { makeTestingModule } from '../../common/test/modules';
import { makeTestAccount } from '../../common/test/utility';
import { AuthService } from './auth.service';
import { createTermInfo } from './test/utility';
import { v4 as uuidv4 } from 'uuid';
describe('AuthService', () => {
it('IDトークンの検証とペイロードの取得に成功する', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
//JWKの生成→PEM変換を自力で表現することが厳しいためMockで代替
service.getPublicKeyFromJwk = makeDefaultGetPublicKeyFromJwk;
const token =
@ -20,7 +29,8 @@ describe('AuthService', () => {
it('IDトークンの形式が不正な場合、形式不正エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
const token = 'invalid.id.token';
await expect(service.getVerifiedIdToken(token)).rejects.toEqual(
@ -30,7 +40,8 @@ describe('AuthService', () => {
it('IDトークンの有効期限が切れている場合、有効期限切れエラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
//JWKの生成→PEM変換を自力で表現することが厳しいためMockで代替
service.getPublicKeyFromJwk = makeDefaultGetPublicKeyFromJwk;
const token =
@ -43,7 +54,8 @@ describe('AuthService', () => {
it('IDトークンが開始日より前の場合、開始前エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
//JWKの生成→PEM変換を自力で表現することが厳しいためMockで代替
service.getPublicKeyFromJwk = makeDefaultGetPublicKeyFromJwk;
const token =
@ -56,7 +68,8 @@ describe('AuthService', () => {
it('IDトークンの署名が不正な場合、署名不正エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwidmVyIjoiMS4wIiwiaXNzIjoiaXNzdXNlciIsInN1YiI6InN1YiIsImF1ZCI6ImF1ZCIsIm5vbmNlIjoiZGVmYXVsdE5vbmNlIiwiaWF0IjoxMDAwMDAwMDAwLCJhdXRoX3RpbWUiOjEwMDAwMDAwMDAsImVtYWlscyI6WyJ4eHhAeHguY29tIl0sInRmcCI6InNpZ25pbl91c2VyZmxvdyJ9.sign';
@ -67,7 +80,8 @@ describe('AuthService', () => {
it('IDトークンの発行元が想定と異なる場合、発行元不正エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
//JWKの生成→PEM変換を自力で表現することが厳しいためMockで代替
service.getPublicKeyFromJwk = makeDefaultGetPublicKeyFromJwk;
const token =
@ -80,8 +94,9 @@ describe('AuthService', () => {
it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。メタデータ', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const configMockValue = makeDefaultConfigValue();
adb2cParam.getMetaData = new Error('failed get metadata');
const service = await makeAuthServiceMock(adb2cParam);
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwidmVyIjoiMS4wIiwiaXNzIjoiaXNzdWVyIiwic3ViIjoic3ViIiwiYXVkIjoiYXVkIiwibm9uY2UiOiJkZWZhdWx0Tm9uY2UiLCJpYXQiOjEwMDAwMDAwMDAsImF1dGhfdGltZSI6MTAwMDAwMDAwMCwiZW1haWxzIjpbInh4eEB4eC5jb20iXSwidGZwIjoic2lnbmluX3VzZXJmbG93In0.RyieW-VHsHPQOjXbbhRc307AYJOc1sq2hrcu4SW1-K0pvLlkplepxvx02a3vCwQrnBYrIP5w6HExG-S_JgW5nYyWr6DeY11mA484n9KA8GeAcAXV37StH1gfWUJvfGb4C8BaMbMM9Ix4Z9NGwKA9vjNwevfmBZnz9lQUePgv6BJNmyvCt8ElJ01O-1WODbZuojJ4xXymA1OqluzfbphPOsqWTSNmTn0emkLjjnlMQf1iwM4C_kvvr8dUCFg0_UGDfQVJnzPEKB38UqnhLnC5WacrddDwQ0kBuGKZgZ_63Q_7fOvqAZivqLK7BPmbPxi6mx3R1S9Eq2ugzpY1LfJOjA';
@ -94,8 +109,9 @@ describe('AuthService', () => {
});
it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。キーセット', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const configMockValue = makeDefaultConfigValue();
adb2cParam.getSignKeySets = new Error('failed get keyset');
const service = await makeAuthServiceMock(adb2cParam);
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwidmVyIjoiMS4wIiwiaXNzIjoiaXNzdWVyIiwic3ViIjoic3ViIiwiYXVkIjoiYXVkIiwibm9uY2UiOiJkZWZhdWx0Tm9uY2UiLCJpYXQiOjEwMDAwMDAwMDAsImF1dGhfdGltZSI6MTAwMDAwMDAwMCwiZW1haWxzIjpbInh4eEB4eC5jb20iXSwidGZwIjoic2lnbmluX3VzZXJmbG93In0.RyieW-VHsHPQOjXbbhRc307AYJOc1sq2hrcu4SW1-K0pvLlkplepxvx02a3vCwQrnBYrIP5w6HExG-S_JgW5nYyWr6DeY11mA484n9KA8GeAcAXV37StH1gfWUJvfGb4C8BaMbMM9Ix4Z9NGwKA9vjNwevfmBZnz9lQUePgv6BJNmyvCt8ElJ01O-1WODbZuojJ4xXymA1OqluzfbphPOsqWTSNmTn0emkLjjnlMQf1iwM4C_kvvr8dUCFg0_UGDfQVJnzPEKB38UqnhLnC5WacrddDwQ0kBuGKZgZ_63Q_7fOvqAZivqLK7BPmbPxi6mx3R1S9Eq2ugzpY1LfJOjA';
@ -109,10 +125,11 @@ describe('AuthService', () => {
it('Azure ADB2Cから取得した鍵が一致しない場合、エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const configMockValue = makeDefaultConfigValue();
adb2cParam.getSignKeySets = [
{ kid: 'invalid', kty: 'RSA', nbf: 0, use: 'sig', e: '', n: '' },
];
const service = await makeAuthServiceMock(adb2cParam);
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwidmVyIjoiMS4wIiwiaXNzIjoiaXNzdWVyIiwic3ViIjoic3ViIiwiYXVkIjoiYXVkIiwibm9uY2UiOiJkZWZhdWx0Tm9uY2UiLCJpYXQiOjEwMDAwMDAwMDAsImF1dGhfdGltZSI6MTAwMDAwMDAwMCwiZW1haWxzIjpbInh4eEB4eC5jb20iXSwidGZwIjoic2lnbmluX3VzZXJmbG93In0.RyieW-VHsHPQOjXbbhRc307AYJOc1sq2hrcu4SW1-K0pvLlkplepxvx02a3vCwQrnBYrIP5w6HExG-S_JgW5nYyWr6DeY11mA484n9KA8GeAcAXV37StH1gfWUJvfGb4C8BaMbMM9Ix4Z9NGwKA9vjNwevfmBZnz9lQUePgv6BJNmyvCt8ElJ01O-1WODbZuojJ4xXymA1OqluzfbphPOsqWTSNmTn0emkLjjnlMQf1iwM4C_kvvr8dUCFg0_UGDfQVJnzPEKB38UqnhLnC5WacrddDwQ0kBuGKZgZ_63Q_7fOvqAZivqLK7BPmbPxi6mx3R1S9Eq2ugzpY1LfJOjA';
@ -125,6 +142,140 @@ describe('AuthService', () => {
});
});
describe('checkIsAcceptedLatestVersion', () => {
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
database: ':memory:',
logging: false,
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
});
return source.initialize();
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('同意済み利用規約バージョンが最新のときにチェックが通ること(第五)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(AuthService);
const { admin } = await makeTestAccount(source, {
tier: 5,
});
const context = makeContext(uuidv4());
const idToken = {
emails: [],
sub: admin.external_id,
exp: 0,
iat: 0,
};
await createTermInfo(source, 'EULA', '1.0');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(true);
});
it('同意済み利用規約バージョンが最新のときにチェックが通ること(第一~第四)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(AuthService);
const { admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(uuidv4());
const idToken = {
emails: [],
sub: admin.external_id,
exp: 0,
iat: 0,
};
await createTermInfo(source, 'EULA', '1.0');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(true);
});
it('同意済み利用規約バージョンが最新でないときにチェックが通らないこと(第五)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(AuthService);
const { admin } = await makeTestAccount(source, {
tier: 5,
});
const context = makeContext(uuidv4());
const idToken = {
emails: [],
sub: admin.external_id,
exp: 0,
iat: 0,
};
await createTermInfo(source, 'EULA', '1.1');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false);
});
it('同意済み利用規約EULAバージョンが最新でないときにチェックが通らないこと第一第四', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(AuthService);
const { admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(uuidv4());
const idToken = {
emails: [],
sub: admin.external_id,
exp: 0,
iat: 0,
};
await createTermInfo(source, 'EULA', '1.1');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false);
});
it('同意済み利用規約バージョンDPAが最新でないときにチェックが通らないこと第一第四', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(AuthService);
const { admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(uuidv4());
const idToken = {
emails: [],
sub: admin.external_id,
exp: 0,
iat: 0,
};
await createTermInfo(source, 'EULA', '1.0');
await createTermInfo(source, 'DPA', '1.1');
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false);
});
});
const idTokenPayload = {
exp: 9000000000,
nbf: 1000000000,

View File

@ -17,7 +17,7 @@ import {
RefreshToken,
isIDToken,
} from '../../common/token';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { ADMIN_ROLES, TIERS, USER_ROLES } from '../../constants';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { User } from '../../repositories/users/entity/user.entity';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
@ -25,6 +25,13 @@ import { Context } from '../../common/log';
@Injectable()
export class AuthService {
private readonly refreshTokenLifetimeWeb =
this.configService.getOrThrow<number>('REFRESH_TOKEN_LIFETIME_WEB');
private readonly refreshTokenLifetimeDefault =
this.configService.getOrThrow<number>('REFRESH_TOKEN_LIFETIME_DEFAULT');
private readonly accessTokenlifetime = this.configService.getOrThrow<number>(
'ACCESS_TOKEN_LIFETIME_WEB',
);
constructor(
private readonly adB2cService: AdB2cService,
private readonly configService: ConfigService,
@ -68,10 +75,7 @@ export class AuthService {
this.logger.log(
`[IN] [${context.trackingId}] ${this.generateRefreshToken.name}`,
);
const lifetimeWeb = this.configService.get('REFRESH_TOKEN_LIFETIME_WEB');
const lifetimeDefault = this.configService.get(
'REFRESH_TOKEN_LIFETIME_DEFAULT',
);
let user: User;
// ユーザー情報とユーザーが属しているアカウント情報を取得
try {
@ -106,7 +110,10 @@ export class AuthService {
);
}
// 要求された環境用トークンの寿命を決定
const refreshTokenLifetime = type === 'web' ? lifetimeWeb : lifetimeDefault;
const refreshTokenLifetime =
type === 'web'
? this.refreshTokenLifetimeWeb
: this.refreshTokenLifetimeDefault;
const privateKey = getPrivateKey(this.configService);
// ユーザーのロールを設定
@ -165,7 +172,6 @@ export class AuthService {
this.logger.log(
`[IN] [${context.trackingId}] ${this.generateAccessToken.name}`,
);
const lifetime = this.configService.get('ACCESS_TOKEN_LIFETIME_WEB');
const privateKey = getPrivateKey(this.configService);
const pubkey = getPublicKey(this.configService);
@ -188,7 +194,7 @@ export class AuthService {
tier: token.tier,
userId: token.userId,
},
lifetime,
this.accessTokenlifetime,
privateKey,
);
@ -205,11 +211,14 @@ export class AuthService {
async getVerifiedIdToken(token: string): Promise<IDToken> {
this.logger.log(`[IN] ${this.getVerifiedIdToken.name}`);
let kid = '';
let kid: string | undefined = '';
try {
// JWTトークンのヘッダを見るため一度デコードする
const decodedToken = jwt.decode(token, { complete: true });
kid = decodedToken.header.kid;
kid = decodedToken?.header.kid;
if (!kid) {
throw new Error('kid not found');
}
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
@ -331,4 +340,59 @@ export class AuthService {
HttpStatus.UNAUTHORIZED,
);
};
/**
*
* @param idToken AzureAD B2Cにより発行されたIDトークン
* @returns boolean
*/
async isAcceptedLatestVersion(
context: Context,
idToken: IDToken,
): Promise<boolean> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.isAcceptedLatestVersion.name} | params: { ` +
`idToken.sub: ${idToken.sub}, };`,
);
try {
// DBからユーザーの同意済み利用規約バージョンと最新バージョンを取得
const {
acceptedEulaVersion,
latestEulaVersion,
acceptedDpaVersion,
latestDpaVersion,
tier,
} = await this.usersRepository.getAcceptedAndLatestVersion(idToken.sub);
// 第五階層はEULAのみ判定
if (tier === TIERS.TIER5) {
if (!acceptedEulaVersion) {
return false;
}
// 最新バージョンに同意済みか判定
const eulaAccepted = acceptedEulaVersion === latestEulaVersion;
return eulaAccepted;
} else {
// 第一第四階層はEULA、DPAを判定
if (!acceptedEulaVersion || !acceptedDpaVersion) {
return false;
}
// 最新バージョンに同意済みか判定
const eulaAccepted = acceptedEulaVersion === latestEulaVersion;
const dpaAccepted = acceptedDpaVersion === latestDpaVersion;
return eulaAccepted && dpaAccepted;
}
} catch (e) {
this.logger.error(`[${context.trackingId}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.isAcceptedLatestVersion.name}`,
);
}
}
}

View File

@ -9,9 +9,13 @@ export type AdB2cMockValue = {
getMetaData: B2cMetadata | Error;
getSignKeySets: JwkSignKey[] | Error;
};
export type ConfigMockValue = {
getOrThrow: number;
};
export const makeAuthServiceMock = async (
adB2cMockValue: AdB2cMockValue,
configMockValue: ConfigMockValue,
): Promise<AuthService> => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
@ -21,7 +25,7 @@ export const makeAuthServiceMock = async (
case AdB2cService:
return makeAdB2cServiceMock(adB2cMockValue);
case ConfigService:
return {};
return makeConfigMock(configMockValue);
case UsersRepositoryService:
return {};
}
@ -80,3 +84,16 @@ export const makeDefaultGetPublicKeyFromJwk = (jwkKey: JwkSignKey): string => {
'-----END PUBLIC KEY-----',
].join('\n');
};
export const makeConfigMock = (value: ConfigMockValue) => {
const { getOrThrow } = value;
return {
getOrThrow: jest.fn<Promise<number>, []>().mockResolvedValue(getOrThrow),
};
};
export const makeDefaultConfigValue = (): ConfigMockValue => {
return {
getOrThrow: 80000,
};
};

View File

@ -0,0 +1,18 @@
import { DataSource } from 'typeorm';
import { Term } from '../../../repositories/terms/entity/term.entity';
export const createTermInfo = async (
datasource: DataSource,
documentType: string,
version: string,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(Term).insert({
document_type: documentType,
version: version,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
identifiers.pop() as Term;
};

View File

@ -17,3 +17,11 @@ export class AccessTokenResponse {
accessToken: string;
}
export class AccessTokenRequest {}
export type TermsCheckInfo = {
tier: number;
acceptedEulaVersion?: string;
acceptedDpaVersion?: string;
latestEulaVersion: string;
latestDpaVersion: string;
};

View File

@ -2,6 +2,7 @@ import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Post,
Query,
@ -37,6 +38,7 @@ import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { Request } from 'express';
import { makeContext } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('files')
@Controller('files')
@ -70,16 +72,28 @@ export class FilesController {
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR] }))
@Post('audio/upload-finished')
async uploadFinished(
@Req() req: Request,
@Body() body: AudioUploadFinishedRequest,
): Promise<AudioUploadFinishedResponse> {
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(accessToken.userId);
const context = makeContext(userId);
const {
url,
@ -100,7 +114,7 @@ export class FilesController {
const res = await this.filesService.uploadFinished(
context,
accessToken.userId,
userId,
url,
authorId,
fileName,
@ -143,7 +157,6 @@ export class FilesController {
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR] }))
async uploadLocation(
@Req() req: Request,
// クエリパラメータ AudioUploadLocationRequest は空であるため内部で使用しない。
@ -151,10 +164,23 @@ export class FilesController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@Query() _query: AudioUploadLocationRequest,
): Promise<AudioUploadLocationResponse> {
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(accessToken.userId);
const context = makeContext(userId);
const url = await this.filesService.publishUploadSas(context, accessToken);
return { url };
@ -197,14 +223,27 @@ export class FilesController {
): Promise<AudioDownloadLocationResponse> {
const { audioFileId } = body;
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(accessToken.userId);
const context = makeContext(userId);
const url = await this.filesService.publishAudioFileDownloadSas(
context,
accessToken.userId,
userId,
audioFileId,
);
@ -248,14 +287,27 @@ export class FilesController {
): Promise<TemplateDownloadLocationResponse> {
const { audioFileId } = body;
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(accessToken.userId);
const context = makeContext(userId);
const url = await this.filesService.publishTemplateFileDownloadSas(
context,
accessToken.userId,
userId,
audioFileId,
);
@ -289,8 +341,21 @@ export class FilesController {
async uploadTemplateLocation(
@Req() req: Request,
): Promise<TemplateUploadLocationResponse> {
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -335,8 +400,21 @@ export class FilesController {
@Body() body: TemplateUploadFinishedRequest,
): Promise<TemplateUploadFinishedReqponse> {
const { name, url } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.filesService.templateUploadFinished(context, userId, url, name);

View File

@ -35,11 +35,10 @@ describe('音声ファイルアップロードURL取得', () => {
);
expect(
await service.publishUploadSas(makeContext('trackingId'), {
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
role: 'Author',
tier: 5,
}),
await service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
).toEqual('https://blob-storage?sas-token');
});
@ -57,11 +56,10 @@ describe('音声ファイルアップロードURL取得', () => {
);
expect(
await service.publishUploadSas(makeContext('trackingId'), {
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
role: 'Author',
tier: 5,
}),
await service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
).toEqual('https://blob-storage?sas-token');
});
@ -78,11 +76,10 @@ describe('音声ファイルアップロードURL取得', () => {
);
await expect(
service.publishUploadSas(makeContext('trackingId'), {
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
role: 'Author',
tier: 5,
}),
service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
);
@ -102,11 +99,10 @@ describe('音声ファイルアップロードURL取得', () => {
blobParam.publishUploadSas = new Error('Azure service down');
await expect(
service.publishUploadSas(makeContext('trackingId'), {
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
role: 'Author',
tier: 5,
}),
service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
);
@ -295,7 +291,7 @@ describe('タスク作成', () => {
});
describe('音声ファイルダウンロードURL取得', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -308,11 +304,13 @@ describe('音声ファイルダウンロードURL取得', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ダウンロードSASトークンが乗っているURLを取得できる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const {
external_id: externalId,
@ -333,7 +331,7 @@ describe('音声ファイルダウンロードURL取得', () => {
'test.zip',
'InProgress',
undefined,
authorId,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
@ -341,6 +339,7 @@ describe('音声ファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
expect(
@ -353,6 +352,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: accountId,
@ -382,6 +382,7 @@ describe('音声ファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -396,6 +397,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Typistの場合、自身が担当するタスクでない場合エラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
@ -429,6 +431,7 @@ describe('音声ファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -443,6 +446,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Authorの場合、自身が登録したタスクでない場合エラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: accountId,
@ -467,6 +471,7 @@ describe('音声ファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -481,6 +486,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Taskが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
@ -492,6 +498,7 @@ describe('音声ファイルダウンロードURL取得', () => {
const blobParam = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -506,6 +513,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('blobストレージにファイルが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const {
external_id: externalId,
@ -526,7 +534,7 @@ describe('音声ファイルダウンロードURL取得', () => {
'test.zip',
'InProgress',
undefined,
authorId,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
@ -534,6 +542,7 @@ describe('音声ファイルダウンロードURL取得', () => {
blobParam.fileExists = false;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -549,7 +558,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
describe('テンプレートファイルダウンロードURL取得', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -562,11 +571,13 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ダウンロードSASトークンが乗っているURLを取得できる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, author_id: authorId } = await makeTestUser(
source,
@ -586,7 +597,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
'test.zip',
'InProgress',
undefined,
authorId,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
@ -594,6 +605,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
expect(
@ -606,6 +618,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: accountId,
@ -629,6 +642,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -643,6 +657,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Typistの場合、自身が担当するタスクでない場合エラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
@ -672,6 +687,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -686,6 +702,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Authorの場合、自身が登録したタスクでない場合エラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
@ -710,6 +727,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -724,6 +742,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Taskが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
@ -735,6 +754,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
const blobParam = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -749,6 +769,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('blobストレージにファイルが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, author_id: authorId } = await makeTestUser(
source,
@ -768,7 +789,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
'test.zip',
'InProgress',
undefined,
authorId,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
@ -776,6 +797,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
blobParam.fileExists = false;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -791,7 +813,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
describe('publishTemplateFileUploadSas', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -804,12 +826,15 @@ describe('publishTemplateFileUploadSas', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('テンプレートファイルアップロードSASトークンが乗っているURLを取得できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
@ -832,7 +857,9 @@ describe('publishTemplateFileUploadSas', () => {
});
it('blobストレージにコンテナが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
@ -858,7 +885,9 @@ describe('publishTemplateFileUploadSas', () => {
});
it('SASトークンの取得に失敗した場合はエラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
@ -887,7 +916,7 @@ describe('publishTemplateFileUploadSas', () => {
});
describe('templateUploadFinished', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -900,12 +929,15 @@ describe('templateUploadFinished', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('アップロード完了後のテンプレートファイル情報をDBに保存できる新規追加', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
@ -937,7 +969,9 @@ describe('templateUploadFinished', () => {
});
it('アップロード完了後のテンプレートファイル情報をDBに保存できる更新', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
@ -975,7 +1009,9 @@ describe('templateUploadFinished', () => {
});
it('DBへの保存に失敗した場合はエラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });

View File

@ -24,6 +24,7 @@ import {
} from '../../repositories/tasks/errors/types';
import { Context } from '../../common/log';
import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
@Injectable()
export class FilesService {
@ -206,7 +207,7 @@ export class FilesService {
*/
async publishUploadSas(
context: Context,
token: AccessToken,
externalId: string,
): Promise<string> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.publishUploadSas.name}`,
@ -216,10 +217,11 @@ export class FilesService {
let accountId: number;
let country: string;
try {
const user = await this.usersRepository.findUserByExternalId(
token.userId,
);
accountId = user.account.id;
const user = await this.usersRepository.findUserByExternalId(externalId);
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
accountId = user.account_id;
country = user.account.country;
} catch (e) {
this.logger.error(`error=${e}`);
@ -291,14 +293,17 @@ export class FilesService {
let userId: number;
let country: string;
let isTypist: boolean;
let authorId: string;
let authorId: string | undefined;
try {
const user = await this.usersRepository.findUserByExternalId(externalId);
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
accountId = user.account.id;
userId = user.id;
country = user.account.country;
isTypist = user.role === USER_ROLES.TYPIST;
authorId = user.author_id;
authorId = user.author_id ?? undefined;
} catch (e) {
this.logger.error(`error=${e}`);
@ -321,7 +326,7 @@ export class FilesService {
accountId,
status,
);
const file = task.file;
const { file } = task;
// タスクに紐づく音声ファイルだけが消される場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
@ -332,9 +337,9 @@ export class FilesService {
}
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
if (!isTypist && task.file.author_id !== authorId) {
if (!isTypist && file.author_id !== authorId) {
throw new AuthorUserNotMatchError(
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${task.file.author_id}, authorId:${authorId}`,
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${file.author_id}, authorId:${authorId}`,
);
}
@ -425,14 +430,17 @@ export class FilesService {
let userId: number;
let country: string;
let isTypist: boolean;
let authorId: string;
let authorId: string | undefined;
try {
const user = await this.usersRepository.findUserByExternalId(externalId);
accountId = user.account.id;
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
accountId = user.account_id;
userId = user.id;
country = user.account.country;
isTypist = user.role === USER_ROLES.TYPIST;
authorId = user.author_id;
authorId = user.author_id ?? undefined;
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.log(
@ -454,6 +462,15 @@ export class FilesService {
accountId,
status,
);
const { file } = task;
// タスクに紐づく音声ファイルだけが消される場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
if (!file) {
throw new AudioFileNotFoundError(
`Audio file is not exists in DB. audio_file_id:${audioFileId}`,
);
}
const template_file = task.template_file;
@ -466,9 +483,9 @@ export class FilesService {
}
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
if (!isTypist && task.file.author_id !== authorId) {
if (!isTypist && file.author_id !== authorId) {
throw new AuthorUserNotMatchError(
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${task.file.author_id}, authorId:${authorId}`,
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${file.author_id}, authorId:${authorId}`,
);
}
@ -515,6 +532,7 @@ export class FilesService {
makeErrorResponse('E010603'),
HttpStatus.BAD_REQUEST,
);
case AudioFileNotFoundError:
case TemplateFileNotFoundError:
throw new HttpException(
makeErrorResponse('E010701'),
@ -552,15 +570,18 @@ export class FilesService {
`[IN] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name} | params: { externalId: ${externalId} };`,
);
try {
const {
account: { id: accountId, country },
} = await this.usersRepository.findUserByExternalId(externalId);
const { account } = await this.usersRepository.findUserByExternalId(
externalId,
);
if (!account) {
throw new AccountNotFoundError('account not found.');
}
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
const isContainerExists = await this.blobStorageService.containerExists(
context,
accountId,
country,
account.id,
account.country,
);
if (!isContainerExists) {
throw new Error('container not found.');
@ -569,8 +590,8 @@ export class FilesService {
// SASトークン発行
const url = await this.blobStorageService.publishTemplateUploadSas(
context,
accountId,
country,
account.id,
account.country,
);
return url;

View File

@ -134,12 +134,15 @@ export const makeDefaultUsersRepositoryMockValue =
created_by: 'test',
created_at: new Date(),
updated_by: null,
updated_at: null,
updated_at: new Date(),
auto_renew: true,
license_alert: true,
notification: true,
encryption: false,
prompt: false,
encryption_password: null,
license: null,
userGroupMembers: null,
account: {
id: 2,
parent_account_id: 2,
@ -154,7 +157,10 @@ export const makeDefaultUsersRepositoryMockValue =
created_by: '',
created_at: new Date(),
updated_by: '',
updated_at: null,
updated_at: new Date(),
active_worktype_id: null,
secondary_admin_user_id: null,
user: null,
},
},
};
@ -172,6 +178,14 @@ export const makeDefaultTasksRepositoryMockValue =
status: 'Uploaded',
priority: '01',
created_at: new Date(),
finished_at: null,
started_at: null,
template_file_id: null,
typist_user_id: null,
file: null,
option_items: null,
template_file: null,
typist_user: null,
},
getTasksFromAccountId: {
tasks: [],

View File

@ -98,7 +98,7 @@ export const createTask = async (
export const makeTestingModuleWithBlob = async (
datasource: DataSource,
blobStorageService: BlobstorageServiceMockValue,
): Promise<TestingModule> => {
): Promise<TestingModule | undefined> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [

View File

@ -2,6 +2,7 @@ import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Post,
Req,
@ -34,6 +35,7 @@ import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES, TIERS } from '../../constants';
import jwt from 'jsonwebtoken';
import { makeContext } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('licenses')
@Controller('licenses')
@ -73,12 +75,25 @@ export class LicensesController {
@Req() req: Request,
@Body() body: CreateOrdersRequest,
): Promise<CreateOrdersResponse> {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
// ライセンス注文処理
await this.licensesService.licenseOrders(
payload,
userId,
body.poNumber,
body.orderCount,
);
@ -111,11 +126,24 @@ export class LicensesController {
@Req() req: Request,
@Body() body: IssueCardLicensesRequest,
): Promise<IssueCardLicensesResponse> {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const cardLicenseKeys = await this.licensesService.issueCardLicenseKeys(
payload.userId,
userId,
body.createCount,
);
@ -154,11 +182,24 @@ export class LicensesController {
@Req() req: Request,
@Body() body: ActivateCardLicensesRequest,
): Promise<ActivateCardLicensesResponse> {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
await this.licensesService.activateCardLicenseKey(
payload.userId,
userId,
body.cardLicenseKey,
);
@ -194,16 +235,26 @@ export class LicensesController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@Req() req: Request,
): Promise<GetAllocatableLicensesResponse> {
const token = retrieveAuthorizationToken(req);
const payload = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(payload.userId);
const context = makeContext(userId);
const allocatableLicenses =
await this.licensesService.getAllocatableLicenses(
context,
payload.userId,
);
await this.licensesService.getAllocatableLicenses(context, userId);
return allocatableLicenses;
}
@ -245,16 +296,25 @@ export class LicensesController {
@Req() req: Request,
@Body() body: CancelOrderRequest,
): Promise<CancelOrderResponse> {
const token = retrieveAuthorizationToken(req);
const payload = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(payload.userId);
const context = makeContext(userId);
await this.licensesService.cancelOrder(
context,
payload.userId,
body.poNumber,
);
await this.licensesService.cancelOrder(context, userId, body.poNumber);
return {};
}
}

View File

@ -56,11 +56,11 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.orderCount = 1000;
body.poNumber = '1';
expect(
await service.licenseOrders(token, body.poNumber, body.orderCount),
await service.licenseOrders(userId, body.poNumber, body.orderCount),
).toEqual(undefined);
});
it('ユーザID取得できなかった場合、エラーとなる', async () => {
@ -78,11 +78,11 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '', role: '', tier: 5 };
const userId = '';
body.orderCount = 1000;
body.poNumber = '1';
await expect(
service.licenseOrders(token, body.poNumber, body.orderCount),
service.licenseOrders(userId, body.poNumber, body.orderCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
@ -105,11 +105,11 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.orderCount = 1000;
body.poNumber = '1';
await expect(
service.licenseOrders(token, body.poNumber, body.orderCount),
service.licenseOrders(userId, body.poNumber, body.orderCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
@ -130,11 +130,11 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.orderCount = 1000;
body.poNumber = '1';
await expect(
service.licenseOrders(token, body.poNumber, body.orderCount),
service.licenseOrders(userId, body.poNumber, body.orderCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E010401'),
@ -154,7 +154,7 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new IssueCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.createCount = 10;
const issueCardLicensesResponse: IssueCardLicensesResponse = {
cardLicenseKeys: [
@ -171,7 +171,7 @@ describe('LicensesService', () => {
],
};
expect(
await service.issueCardLicenseKeys(token.userId, body.createCount),
await service.issueCardLicenseKeys(userId, body.createCount),
).toEqual(issueCardLicensesResponse);
});
it('カードライセンス発行に失敗した場合、エラーになる', async () => {
@ -187,10 +187,10 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new IssueCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.createCount = 1000;
await expect(
service.issueCardLicenseKeys(token.userId, body.createCount),
service.issueCardLicenseKeys(userId, body.createCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
@ -210,10 +210,10 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
expect(
await service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
await service.activateCardLicenseKey(userId, body.cardLicenseKey),
).toEqual(undefined);
});
it('カードライセンス取り込みに失敗した場合、エラーになるDBエラー', async () => {
@ -229,10 +229,10 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
await expect(
service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
service.activateCardLicenseKey(userId, body.cardLicenseKey),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
@ -254,10 +254,10 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
await expect(
service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
service.activateCardLicenseKey(userId, body.cardLicenseKey),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010801'), HttpStatus.BAD_REQUEST),
);
@ -276,10 +276,10 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
await expect(
service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
service.activateCardLicenseKey(userId, body.cardLicenseKey),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010802'), HttpStatus.BAD_REQUEST),
);
@ -287,7 +287,7 @@ describe('LicensesService', () => {
});
describe('DBテスト', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -300,12 +300,15 @@ describe('DBテスト', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('カードライセンス発行が完了する(発行数が合っているか確認)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
@ -323,7 +326,9 @@ describe('DBテスト', () => {
});
it('カードライセンス取り込みが完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
@ -362,13 +367,15 @@ describe('DBテスト', () => {
);
const dbSelectResultFromLicense = await selectLicense(source, license_id);
expect(
dbSelectResultFromCardLicense.cardLicense.activated_at,
dbSelectResultFromCardLicense?.cardLicense?.activated_at,
).toBeDefined();
expect(dbSelectResultFromLicense.license.account_id).toEqual(accountId);
expect(dbSelectResultFromLicense?.license?.account_id).toEqual(accountId);
});
it('取込可能なライセンスのみが取得できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const now = new Date();
const { id: accountId } = await makeTestSimpleAccount(source);
@ -513,7 +520,7 @@ describe('DBテスト', () => {
});
describe('ライセンス割り当て', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -526,12 +533,15 @@ describe('ライセンス割り当て', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('未割当のライセンスに対して、ライセンス割り当てが完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -567,11 +577,11 @@ describe('ライセンス割り当て', () => {
await service.allocateLicense(makeContext('trackingId'), userId, 1);
const resultLicense = await selectLicense(source, 1);
expect(resultLicense.license.allocated_user_id).toBe(userId);
expect(resultLicense.license.status).toBe(
expect(resultLicense.license?.allocated_user_id).toBe(userId);
expect(resultLicense.license?.status).toBe(
LICENSE_ALLOCATED_STATUS.ALLOCATED,
);
expect(resultLicense.license.expiry_date.setMilliseconds(0)).toEqual(
expect(resultLicense.license?.expiry_date?.setMilliseconds(0)).toEqual(
expiry_date.setMilliseconds(0),
);
const licenseAllocationHistory = await selectLicenseAllocationHistory(
@ -579,22 +589,24 @@ describe('ライセンス割り当て', () => {
userId,
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
userId,
);
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.license_id).toBe(
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
true,
);
expect(licenseAllocationHistory.licenseAllocationHistory.account_id).toBe(
expect(
licenseAllocationHistory.licenseAllocationHistory?.is_allocated,
).toBe(true);
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId,
);
});
it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -630,30 +642,32 @@ describe('ライセンス割り当て', () => {
await service.allocateLicense(makeContext('trackingId'), userId, 1);
const result = await selectLicense(source, 1);
expect(result.license.allocated_user_id).toBe(userId);
expect(result.license.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED);
expect(result.license.expiry_date).toEqual(date);
expect(result.license?.allocated_user_id).toBe(userId);
expect(result.license?.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED);
expect(result.license?.expiry_date).toEqual(date);
const licenseAllocationHistory = await selectLicenseAllocationHistory(
source,
userId,
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
userId,
);
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.license_id).toBe(
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
true,
);
expect(licenseAllocationHistory.licenseAllocationHistory.account_id).toBe(
expect(
licenseAllocationHistory.licenseAllocationHistory?.is_allocated,
).toBe(true);
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId,
);
});
it('未割当のライセンスに対して、別のライセンスが割り当てられているユーザーの割り当てが完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -705,32 +719,32 @@ describe('ライセンス割り当て', () => {
// もともと割り当てられていたライセンスの状態確認
const result1 = await selectLicense(source, 1);
expect(result1.license.allocated_user_id).toBe(null);
expect(result1.license.status).toBe(LICENSE_ALLOCATED_STATUS.REUSABLE);
expect(result1.license.expiry_date).toEqual(date);
expect(result1.license?.allocated_user_id).toBe(null);
expect(result1.license?.status).toBe(LICENSE_ALLOCATED_STATUS.REUSABLE);
expect(result1.license?.expiry_date).toEqual(date);
const licenseAllocationHistory = await selectLicenseAllocationHistory(
source,
userId,
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
userId,
);
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.license_id).toBe(
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
false,
);
expect(licenseAllocationHistory.licenseAllocationHistory.account_id).toBe(
expect(
licenseAllocationHistory.licenseAllocationHistory?.is_allocated,
).toBe(false);
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId,
);
// 新たに割り当てたライセンスの状態確認
const result2 = await selectLicense(source, 2);
expect(result2.license.allocated_user_id).toBe(userId);
expect(result2.license.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED);
expect(result2.license.expiry_date.setMilliseconds(0)).toEqual(
expect(result2.license?.allocated_user_id).toBe(userId);
expect(result2.license?.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED);
expect(result2.license?.expiry_date?.setMilliseconds(0)).toEqual(
expiry_date.setMilliseconds(0),
);
const newlicenseAllocationHistory = await selectLicenseAllocationHistory(
@ -738,22 +752,24 @@ describe('ライセンス割り当て', () => {
userId,
2,
);
expect(newlicenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
expect(newlicenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
userId,
);
expect(
newlicenseAllocationHistory.licenseAllocationHistory.license_id,
newlicenseAllocationHistory.licenseAllocationHistory?.license_id,
).toBe(2);
expect(
newlicenseAllocationHistory.licenseAllocationHistory.is_allocated,
newlicenseAllocationHistory.licenseAllocationHistory?.is_allocated,
).toBe(true);
expect(
newlicenseAllocationHistory.licenseAllocationHistory.account_id,
newlicenseAllocationHistory.licenseAllocationHistory?.account_id,
).toBe(accountId);
});
it('割り当て時にライセンス履歴テーブルへの登録が完了する元がNORMALのとき', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -806,12 +822,14 @@ describe('ライセンス割り当て', () => {
2,
);
expect(
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
licenseAllocationHistory.licenseAllocationHistory?.switch_from_type,
).toBe('NONE');
});
it('割り当て時にライセンス履歴テーブルへの登録が完了する元がCARDのとき', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -864,12 +882,14 @@ describe('ライセンス割り当て', () => {
2,
);
expect(
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
licenseAllocationHistory.licenseAllocationHistory?.switch_from_type,
).toBe('CARD');
});
it('割り当て時にライセンス履歴テーブルへの登録が完了する元がTRIALのとき', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -922,12 +942,14 @@ describe('ライセンス割り当て', () => {
2,
);
expect(
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
licenseAllocationHistory.licenseAllocationHistory?.switch_from_type,
).toBe('TRIAL');
});
it('有効期限が切れているライセンスを割り当てようとした場合、エラーになる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -961,7 +983,9 @@ describe('ライセンス割り当て', () => {
});
it('割り当て不可なライセンスを割り当てようとした場合、エラーになる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -1013,7 +1037,7 @@ describe('ライセンス割り当て', () => {
});
describe('ライセンス割り当て解除', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -1026,12 +1050,15 @@ describe('ライセンス割り当て解除', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ライセンスの割り当て解除が完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -1068,11 +1095,11 @@ describe('ライセンス割り当て解除', () => {
// 割り当て解除したライセンスの状態確認
const deallocatedLicense = await selectLicense(source, 1);
expect(deallocatedLicense.license.allocated_user_id).toBe(null);
expect(deallocatedLicense.license.status).toBe(
expect(deallocatedLicense.license?.allocated_user_id).toBe(null);
expect(deallocatedLicense.license?.status).toBe(
LICENSE_ALLOCATED_STATUS.REUSABLE,
);
expect(deallocatedLicense.license.expiry_date).toEqual(date);
expect(deallocatedLicense.license?.expiry_date).toEqual(date);
// ライセンス履歴テーブルの状態確認
const licenseAllocationHistory = await selectLicenseAllocationHistory(
@ -1080,25 +1107,27 @@ describe('ライセンス割り当て解除', () => {
userId,
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
userId,
);
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.license_id).toBe(
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
false,
);
expect(licenseAllocationHistory.licenseAllocationHistory.account_id).toBe(
expect(
licenseAllocationHistory.licenseAllocationHistory?.is_allocated,
).toBe(false);
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId,
);
expect(
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
licenseAllocationHistory.licenseAllocationHistory?.switch_from_type,
).toBe('NONE');
});
it('ライセンスが既に割り当て解除されていた場合、エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -1158,7 +1187,7 @@ describe('ライセンス割り当て解除', () => {
});
describe('ライセンス注文キャンセル', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -1171,12 +1200,15 @@ describe('ライセンス注文キャンセル', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ライセンス注文のキャンセルが完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
);
@ -1185,7 +1217,7 @@ describe('ライセンス注文キャンセル', () => {
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
tier2Accounts[0].account.parent_account_id ?? 0,
null,
10,
'Issue Requesting',
@ -1195,7 +1227,7 @@ describe('ライセンス注文キャンセル', () => {
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
tier2Accounts[0].account.parent_account_id ?? 0,
null,
10,
'Order Canceled',
@ -1214,12 +1246,14 @@ describe('ライセンス注文キャンセル', () => {
tier2Accounts[0].account.id,
poNumber,
);
expect(orderRecord.orderLicense.canceled_at).toBeDefined();
expect(orderRecord.orderLicense.status).toBe('Order Canceled');
expect(orderRecord.orderLicense?.canceled_at).toBeDefined();
expect(orderRecord.orderLicense?.status).toBe('Order Canceled');
});
it('ライセンスが既に発行済みの場合、エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
);
@ -1228,7 +1262,7 @@ describe('ライセンス注文キャンセル', () => {
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
tier2Accounts[0].account.parent_account_id ?? 0,
null,
10,
'Issued',
@ -1247,7 +1281,9 @@ describe('ライセンス注文キャンセル', () => {
});
it('ライセンスが既にキャンセル済みの場合、エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
@ -1257,7 +1293,7 @@ describe('ライセンス注文キャンセル', () => {
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
tier2Accounts[0].account.parent_account_id ?? 0,
null,
10,
'Order Canceled',

View File

@ -33,20 +33,20 @@ export class LicensesService {
* @param body
*/
async licenseOrders(
accessToken: AccessToken,
externalId: string,
poNumber: string,
orderCount: number,
): Promise<void> {
//アクセストークンからユーザーIDを取得する
this.logger.log(`[IN] ${this.licenseOrders.name}`);
const userId = accessToken.userId;
let myAccountId: number;
let parentAccountId: number;
let parentAccountId: number | undefined;
// ユーザIDからアカウントIDを取得する
try {
myAccountId = (await this.usersRepository.findUserByExternalId(userId))
.account_id;
myAccountId = (
await this.usersRepository.findUserByExternalId(externalId)
).account_id;
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {
@ -65,9 +65,13 @@ export class LicensesService {
// 親アカウントIDを取得
try {
parentAccountId = (
await this.accountsRepository.findAccountById(myAccountId)
).parent_account_id;
parentAccountId =
(await this.accountsRepository.findAccountById(myAccountId))
.parent_account_id ?? undefined;
// 親アカウントIDが取得できない場合はエラー
if (parentAccountId === undefined) {
throw new Error('parent account id is undefined');
}
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {

View File

@ -124,11 +124,11 @@ export const makeDefaultUsersRepositoryMockValue =
user1.notification = false;
user1.encryption = false;
user1.prompt = false;
user1.deleted_at = undefined;
user1.deleted_at = null;
user1.created_by = 'test';
user1.created_at = new Date();
user1.updated_by = undefined;
user1.updated_at = undefined;
user1.updated_by = null;
user1.updated_at = new Date();
return {
findUserByExternalId: user1,

View File

@ -12,14 +12,14 @@ import {
export const createLicense = async (
datasource: DataSource,
licenseId: number,
expiry_date: Date,
expiry_date: Date | null,
accountId: number,
type: string,
status: string,
allocated_user_id: number,
order_id: number,
deleted_at: Date,
delete_order_id: number,
allocated_user_id: number | null,
order_id: number | null,
deleted_at: Date | null,
delete_order_id: number | null,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
id: licenseId,
@ -107,7 +107,7 @@ export const createOrder = async (
poNumber: string,
fromId: number,
toId: number,
issuedAt: Date,
issuedAt: Date | null,
quantity: number,
status: string,
): Promise<void> => {
@ -138,7 +138,7 @@ export const selectCardLicensesCount = async (
export const selectCardLicense = async (
datasource: DataSource,
cardLicenseKey: string,
): Promise<{ cardLicense: CardLicense }> => {
): Promise<{ cardLicense: CardLicense | null }> => {
const cardLicense = await datasource.getRepository(CardLicense).findOne({
where: {
card_license_key: cardLicenseKey,
@ -150,7 +150,7 @@ export const selectCardLicense = async (
export const selectLicense = async (
datasource: DataSource,
id: number,
): Promise<{ license: License }> => {
): Promise<{ license: License | null }> => {
const license = await datasource.getRepository(License).findOne({
where: {
id: id,
@ -163,7 +163,7 @@ export const selectLicenseAllocationHistory = async (
datasource: DataSource,
userId: number,
licence_id: number,
): Promise<{ licenseAllocationHistory: LicenseAllocationHistory }> => {
): Promise<{ licenseAllocationHistory: LicenseAllocationHistory | null }> => {
const licenseAllocationHistory = await datasource
.getRepository(LicenseAllocationHistory)
.findOne({
@ -182,7 +182,7 @@ export const selectOrderLicense = async (
datasource: DataSource,
accountId: number,
poNumber: string,
): Promise<{ orderLicense: LicenseOrder }> => {
): Promise<{ orderLicense: LicenseOrder | null }> => {
const orderLicense = await datasource.getRepository(LicenseOrder).findOne({
where: {
from_account_id: accountId,

View File

@ -47,8 +47,8 @@ export class GetAllocatableLicensesRequest {}
export class AllocatableLicenseInfo {
@ApiProperty()
licenseId: number;
@ApiProperty()
expiryDate: Date;
@ApiProperty({ required: false })
expiryDate?: Date;
}
export class GetAllocatableLicensesResponse {
@ApiProperty({ type: [AllocatableLicenseInfo] })

View File

@ -1,6 +1,7 @@
import {
Body,
Controller,
HttpException,
HttpStatus,
Post,
Req,
@ -21,6 +22,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper';
import { AccessToken } from '../../common/token';
import jwt from 'jsonwebtoken';
import { makeContext } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('notification')
@Controller('notification')
@ -57,7 +59,20 @@ export class NotificationController {
const { handler, pns } = body;
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);

View File

@ -77,14 +77,14 @@ export const makeDefaultUsersRepositoryMockValue =
user.external_id = 'external_id';
user.account_id = 123;
user.role = 'none';
user.author_id = undefined;
user.author_id = null;
user.accepted_eula_version = '1.0';
user.accepted_dpa_version = '1.0';
user.email_verified = true;
user.auto_renew = false;
user.license_alert = false;
user.notification = false;
user.deleted_at = undefined;
user.deleted_at = null;
user.created_by = 'test';
user.created_at = new Date();
user.updated_by = 'test';

View File

@ -3,6 +3,7 @@ import {
Controller,
Get,
Headers,
HttpException,
HttpStatus,
Param,
ParseIntPipe,
@ -32,6 +33,8 @@ import {
TasksResponse,
} from './types/types';
import {
SortDirection,
TaskListSortableAttribute,
isSortDirection,
isTaskListSortableAttribute,
} from '../../common/types/sort';
@ -43,6 +46,7 @@ import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { Roles } from '../../common/types/role';
import { makeContext } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('tasks')
@Controller('tasks')
@ -80,22 +84,38 @@ export class TasksController {
@Req() req,
@Query() body: TasksRequest,
): Promise<TasksResponse> {
const accessToken = retrieveAuthorizationToken(req);
const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
const context = makeContext(decodedToken.userId);
const context = makeContext(userId);
const { limit, offset, status } = body;
const paramName = isTaskListSortableAttribute(body.paramName)
? body.paramName
const paramName = isTaskListSortableAttribute(body.paramName ?? '')
? (body.paramName as TaskListSortableAttribute)
: undefined;
const direction = isSortDirection(body.direction)
? body.direction
const direction = isSortDirection(body.direction ?? '')
? (body.direction as SortDirection)
: undefined;
const { tasks, total } = await this.taskService.getTasks(
context,
decodedToken,
userId,
roles,
offset,
limit,
// statusが指定されていない場合は全てのステータスを取得する
@ -183,10 +203,22 @@ export class TasksController {
@Param() param: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
const { role, userId } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
@ -241,10 +273,22 @@ export class TasksController {
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, role } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -296,10 +340,22 @@ export class TasksController {
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
const { userId, role } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
@ -353,10 +409,22 @@ export class TasksController {
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -491,11 +559,22 @@ export class TasksController {
@Body() body: PostCheckoutPermissionRequest,
): Promise<PostCheckoutPermissionResponse> {
const { assignees } = body;
const accessToken = retrieveAuthorizationToken(req);
const { role, userId } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];

File diff suppressed because it is too large Load Diff

View File

@ -45,10 +45,10 @@ export class TasksService {
private readonly notificationhubService: NotificationhubService,
) {}
// TODO [Task2244] 引数にAccessTokenがあるのは不適切なのでController側で分解したい
async getTasks(
context: Context,
accessToken: AccessToken,
userId: string,
roles: Roles[],
offset: number,
limit: number,
status?: string[],
@ -59,10 +59,6 @@ export class TasksService {
`[IN] [${context.trackingId}] ${this.getTasks.name} | params: { offset: ${offset}, limit: ${limit}, status: ${status}, paramName: ${paramName}, direction: ${direction} };`,
);
const { role, userId } = accessToken;
// TODO [Task2244] Roleに型で定義されている値が入っているかをチェックして異常値を弾く実装に修正する
const roles = role.split(' ');
// パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える
const defaultParamName: TaskListSortableAttribute = 'JOB_NUMBER';
const defaultDirection: SortDirection = 'ASC';
@ -95,6 +91,10 @@ export class TasksService {
return { tasks: tasks, total: result.count };
}
if (roles.includes(USER_ROLES.AUTHOR)) {
// API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (!author_id) {
throw new Error('AuthorID not found');
}
const result =
await this.taskRepository.getTasksFromAuthorIdAndAccountId(
author_id,
@ -179,6 +179,10 @@ export class TasksService {
await this.usersRepository.findUserByExternalId(externalId);
if (roles.includes(USER_ROLES.AUTHOR)) {
// API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (!author_id) {
throw new Error('AuthorID not found');
}
await this.taskRepository.getTaskFromAudioFileId(
audioFileId,
account_id,
@ -407,30 +411,21 @@ export class TasksService {
permissions: CheckoutPermission[],
): Promise<AdB2cUser[]> {
// 割り当て候補の外部IDを列挙
const assigneesExternalIds = permissions.map((x) => {
if (x.user) {
return x.user.external_id;
}
});
const assigneesExternalIds = permissions.flatMap((permission) =>
permission.user ? [permission.user.external_id] : [],
);
// 割り当てられているタイピストの外部IDを列挙
const typistExternalIds = tasks.flatMap((x) => {
if (x.typist_user) {
return x.typist_user.external_id;
}
});
const typistExternalIds = tasks.flatMap((task) =>
task.typist_user ? [task.typist_user.external_id] : [],
);
//重複をなくす
const distinctedExternalIds = [
...new Set(assigneesExternalIds.concat(typistExternalIds)),
];
// undefinedがあった場合、取り除く
const filteredExternalIds: string[] = distinctedExternalIds.filter(
(x): x is string => x !== undefined,
);
// B2Cからユーザー名を取得する
return await this.adB2cService.getUsers(context, filteredExternalIds);
return await this.adB2cService.getUsers(context, distinctedExternalIds);
}
/**
*
@ -451,10 +446,14 @@ export class TasksService {
);
const { author_id, account_id } =
await this.usersRepository.findUserByExternalId(externalId);
// RoleがAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (role.includes(USER_ROLES.AUTHOR) && !author_id) {
throw new Error('AuthorID not found');
}
await this.taskRepository.changeCheckoutPermission(
audioFileId,
author_id,
author_id ?? undefined,
account_id,
role,
assignees,
@ -462,11 +461,16 @@ export class TasksService {
// すべての割り当て候補ユーザーを取得する
const assigneesGroupIds = assignees
.filter((x) => x.typistGroupId)
.map((x) => x.typistGroupId);
.filter((assignee) => assignee.typistGroupId)
.flatMap((assignee) =>
assignee.typistGroupId ? [assignee.typistGroupId] : [],
);
const assigneesUserIds = assignees
.filter((x) => x.typistUserId)
.map((x) => x.typistUserId);
.filter((assignee) => assignee.typistUserId)
.flatMap((assignee) =>
assignee.typistUserId ? [assignee.typistUserId] : [],
);
const groupMembers =
await this.userGroupsRepositoryService.getGroupMembersFromGroupIds(

View File

@ -263,6 +263,11 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user_id: 1,
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
user: null,
userGroup: null,
},
{
id: 2,
@ -270,6 +275,11 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user_id: 2,
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
user: null,
userGroup: null,
},
{
id: 3,
@ -277,6 +287,11 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user_id: 1,
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
user: null,
userGroup: null,
},
{
id: 4,
@ -284,6 +299,11 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user_id: 1,
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
user: null,
userGroup: null,
},
{
id: 5,
@ -291,6 +311,11 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user_id: 3,
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
user: null,
userGroup: null,
},
],
};
@ -307,7 +332,7 @@ export const makeDefaultUsersRepositoryMockValue =
user1.auto_renew = false;
user1.license_alert = false;
user1.notification = false;
user1.deleted_at = undefined;
user1.deleted_at = null;
user1.created_by = 'test';
user1.created_at = new Date();
user1.author_id = 'abcdef';
@ -331,66 +356,82 @@ const defaultTasksRepositoryMockValue: {
status: 'Uploaded',
priority: '00',
created_at: new Date('2023-01-01T01:01:01.000Z'),
finished_at: null,
started_at: null,
typist_user_id: null,
template_file_id: null,
typist_user: null,
template_file: null,
option_items: [
{
id: 1,
audio_file_id: 1,
label: 'label01',
value: 'value01',
task: null,
},
{
id: 2,
audio_file_id: 1,
label: 'label02',
value: 'value02',
task: null,
},
{
id: 3,
audio_file_id: 1,
label: 'label03',
value: 'value03',
task: null,
},
{
id: 4,
audio_file_id: 1,
label: 'label04',
value: 'value04',
task: null,
},
{
id: 5,
audio_file_id: 1,
label: 'label05',
value: 'value05',
task: null,
},
{
id: 6,
audio_file_id: 1,
label: 'label06',
value: 'value06',
task: null,
},
{
id: 7,
audio_file_id: 1,
label: 'label07',
value: 'value07',
task: null,
},
{
id: 8,
audio_file_id: 1,
label: 'label08',
value: 'value08',
task: null,
},
{
id: 9,
audio_file_id: 1,
label: 'label09',
value: 'value09',
task: null,
},
{
id: 10,
audio_file_id: 1,
label: 'label10',
value: 'value10',
task: null,
},
],
file: {
@ -410,6 +451,8 @@ const defaultTasksRepositoryMockValue: {
audio_format: 'DS',
comment: 'comment',
is_encrypted: true,
deleted_at: null,
task: null,
},
},
],
@ -435,7 +478,16 @@ const defaultTasksRepositoryMockValue: {
created_at: new Date(),
updated_by: 'test',
updated_at: new Date(),
account: null,
author_id: null,
deleted_at: null,
encryption_password: null,
license: null,
userGroupMembers: null,
},
task: null,
user_group_id: null,
user_group: null,
},
],
count: 1,

View File

@ -39,10 +39,10 @@ import {
makeNotificationhubServiceMock,
} from './tasks.service.mock';
export const makeTaskTestingModule = async (
export const makeTaskTestingModuleWithNotificaiton = async (
datasource: DataSource,
notificationhubServiceMockValue: NotificationhubServiceMockValue,
): Promise<TestingModule> => {
): Promise<TestingModule | undefined> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [
@ -205,7 +205,7 @@ export const createUserGroup = async (
export const getTask = async (
datasource: DataSource,
task_id: number,
): Promise<Task> => {
): Promise<Task | null> => {
const task = await datasource.getRepository(Task).findOne({
where: {
id: task_id,

View File

@ -45,7 +45,7 @@ const createTask = (
const assignees = createAssignees(permissions, b2cUserInfo);
// RepositoryDTO => ControllerDTOに変換
const typist: Typist =
const typist: Typist | undefined =
typist_user != null
? convertUserToTypist(typist_user, b2cUserInfo)
: undefined;
@ -113,7 +113,10 @@ const convertUserToAssignee = (
): Assignee => {
const typistName = b2cUserInfo.find(
(x) => x.id === user.external_id,
).displayName;
)?.displayName;
if (!typistName) {
throw new Error('typistName not found.');
}
return {
typistUserId: user.id,
typistName,
@ -135,7 +138,10 @@ const convertUserToTypist = (
): Typist => {
const typistName = b2cUserInfo.find(
(x) => x.id === user.external_id,
).displayName;
)?.displayName;
if (!typistName) {
throw new Error('typistName not found.');
}
return {
id: user.id,
name: typistName,

View File

@ -1,4 +1,11 @@
import { Controller, Get, HttpStatus, Req, UseGuards } from '@nestjs/common';
import {
Controller,
Get,
HttpException,
HttpStatus,
Req,
UseGuards,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
@ -16,6 +23,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper';
import { Request } from 'express';
import { makeContext } from '../../common/log';
import { TemplatesService } from './templates.service';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('templates')
@Controller('templates')
@ -46,8 +54,21 @@ export class TemplatesController {
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get()
async getTemplates(@Req() req: Request): Promise<GetTemplatesResponse> {
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
const templates = await this.templatesService.getTemplates(context, userId);

View File

@ -9,7 +9,7 @@ import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
describe('getTemplates', () => {
let source: DataSource = null;
let source: DataSource | undefined = undefined;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -22,12 +22,16 @@ describe('getTemplates', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
source = undefined;
});
it('テンプレートファイル一覧を取得できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<TemplatesService>(TemplatesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
@ -65,7 +69,10 @@ describe('getTemplates', () => {
});
it('テンプレートファイル一覧を取得できる0件', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<TemplatesService>(TemplatesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
@ -80,7 +87,10 @@ describe('getTemplates', () => {
});
it('テンプレートファイル一覧の取得に失敗した場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<TemplatesService>(TemplatesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });

View File

@ -23,6 +23,9 @@ export const createTemplateFile = async (
id: template.id,
},
});
if (!templateFile) {
fail();
}
return templateFile;
};

View File

@ -6,11 +6,14 @@ describe('TermsController', () => {
let controller: TermsController;
beforeEach(async () => {
const mockTermsService = {};
const module: TestingModule = await Test.createTestingModule({
controllers: [TermsController],
providers: [TermsService],
}).compile();
})
.overrideProvider(TermsService)
.useValue(mockTermsService)
.compile();
controller = module.get<TermsController>(TermsController);
});

Some files were not shown because too many files have changed in this diff Show More