Merge branch 'develop'

This commit is contained in:
saito.k 2023-10-10 17:32:49 +09:00
commit e733eb7668
89 changed files with 6316 additions and 226 deletions

View File

@ -21,6 +21,7 @@ import TypistGroupSettingPage from "pages/TypistGroupSettingPage";
import WorktypeIdSettingPage from "pages/WorkTypeIdSettingPage";
import AccountPage from "pages/AccountPage";
import { TemplateFilePage } from "pages/TemplateFilePage";
import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess";
const AppRouter: React.FC = () => (
<Routes>
@ -81,6 +82,8 @@ const AppRouter: React.FC = () => (
path="/partners"
element={<RouteAuthGuard component={<PartnerPage />} />}
/>
<Route path="/accountDeleteSuccess" element={<AccountDeleteSuccess />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
);

View File

@ -335,6 +335,25 @@ export interface AudioUploadLocationResponse {
*/
'url': string;
}
/**
*
* @export
* @interface Author
*/
export interface Author {
/**
* Authorユーザーの内部ID
* @type {number}
* @memberof Author
*/
'id': number;
/**
* AuthorID
* @type {string}
* @memberof Author
*/
'authorId': string;
}
/**
*
* @export
@ -504,6 +523,37 @@ export interface CreateTypistGroupRequest {
*/
'typistIds': Array<number>;
}
/**
*
* @export
* @interface CreateWorkflowsRequest
*/
export interface CreateWorkflowsRequest {
/**
* Authornの内部ID
* @type {number}
* @memberof CreateWorkflowsRequest
*/
'authorId': number;
/**
* Worktypeの内部ID
* @type {number}
* @memberof CreateWorkflowsRequest
*/
'worktypeId'?: number;
/**
* ID
* @type {number}
* @memberof CreateWorkflowsRequest
*/
'templateId'?: number;
/**
* /
* @type {Array<WorkflowTypist>}
* @memberof CreateWorkflowsRequest
*/
'typists': Array<WorkflowTypist>;
}
/**
*
* @export
@ -606,6 +656,19 @@ export interface GetAllocatableLicensesResponse {
*/
'allocatableLicenses': Array<AllocatableLicenseInfo>;
}
/**
*
* @export
* @interface GetAuthorsResponse
*/
export interface GetAuthorsResponse {
/**
*
* @type {Array<Author>}
* @memberof GetAuthorsResponse
*/
'authors': Array<Author>;
}
/**
*
* @export
@ -989,6 +1052,19 @@ export interface GetUsersResponse {
*/
'users': Array<User>;
}
/**
*
* @export
* @interface GetWorkflowsResponse
*/
export interface GetWorkflowsResponse {
/**
*
* @type {Array<Workflow>}
* @memberof GetWorkflowsResponse
*/
'workflows': Array<Workflow>;
}
/**
*
* @export
@ -1963,6 +2039,100 @@ export interface User {
*/
'licenseStatus': string;
}
/**
*
* @export
* @interface Workflow
*/
export interface Workflow {
/**
* ID
* @type {number}
* @memberof Workflow
*/
'id': number;
/**
*
* @type {Author}
* @memberof Workflow
*/
'author': Author;
/**
*
* @type {WorkflowWorktype}
* @memberof Workflow
*/
'worktype'?: WorkflowWorktype;
/**
*
* @type {WorkflowTemplate}
* @memberof Workflow
*/
'template'?: WorkflowTemplate;
/**
* /
* @type {Array<Assignee>}
* @memberof Workflow
*/
'typists': Array<Assignee>;
}
/**
*
* @export
* @interface WorkflowTemplate
*/
export interface WorkflowTemplate {
/**
* ID
* @type {number}
* @memberof WorkflowTemplate
*/
'id': number;
/**
*
* @type {string}
* @memberof WorkflowTemplate
*/
'fileName': string;
}
/**
*
* @export
* @interface WorkflowTypist
*/
export interface WorkflowTypist {
/**
* ID
* @type {number}
* @memberof WorkflowTypist
*/
'typistId'?: number;
/**
* ID
* @type {number}
* @memberof WorkflowTypist
*/
'typistGroupId'?: number;
}
/**
*
* @export
* @interface WorkflowWorktype
*/
export interface WorkflowWorktype {
/**
* Worktypeの内部ID
* @type {number}
* @memberof WorkflowWorktype
*/
'id': number;
/**
* WorktypeID
* @type {string}
* @memberof WorkflowWorktype
*/
'worktypeId': string;
}
/**
*
* @export
@ -2271,6 +2441,40 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
options: localVarRequestOptions,
};
},
/**
* Author一覧を取得します
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAuthors: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/accounts/authors`;
// 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;
// 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
@ -2980,6 +3184,16 @@ export const AccountsApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAccount(deleteAccountRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
* Author一覧を取得します
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAuthors(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetAuthorsResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAuthors(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
@ -3235,6 +3449,15 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
deleteAccount(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise<object> {
return localVarFp.deleteAccount(deleteAccountRequest, options).then((request) => request(axios, basePath));
},
/**
* Author一覧を取得します
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAuthors(options?: any): AxiosPromise<GetAuthorsResponse> {
return localVarFp.getAuthors(options).then((request) => request(axios, basePath));
},
/**
*
* @summary
@ -3488,6 +3711,17 @@ export class AccountsApi extends BaseAPI {
return AccountsApiFp(this.configuration).deleteAccount(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
* Author一覧を取得します
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AccountsApi
*/
public getAuthors(options?: AxiosRequestConfig) {
return AccountsApiFp(this.configuration).getAuthors(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
@ -6481,3 +6715,179 @@ export class UsersApi extends BaseAPI {
/**
* WorkflowsApi - axios parameter creator
* @export
*/
export const WorkflowsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @summary
* @param {CreateWorkflowsRequest} createWorkflowsRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createWorkflows: async (createWorkflowsRequest: CreateWorkflowsRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'createWorkflowsRequest' is not null or undefined
assertParamExists('createWorkflows', 'createWorkflowsRequest', createWorkflowsRequest)
const localVarPath = `/workflows`;
// 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(createWorkflowsRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getWorkflows: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/workflows`;
// 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;
// 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,
};
},
}
};
/**
* WorkflowsApi - functional programming interface
* @export
*/
export const WorkflowsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = WorkflowsApiAxiosParamCreator(configuration)
return {
/**
*
* @summary
* @param {CreateWorkflowsRequest} createWorkflowsRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createWorkflows(createWorkflowsRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getWorkflows(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetWorkflowsResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getWorkflows(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* WorkflowsApi - factory interface
* @export
*/
export const WorkflowsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = WorkflowsApiFp(configuration)
return {
/**
*
* @summary
* @param {CreateWorkflowsRequest} createWorkflowsRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: any): AxiosPromise<object> {
return localVarFp.createWorkflows(createWorkflowsRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getWorkflows(options?: any): AxiosPromise<GetWorkflowsResponse> {
return localVarFp.getWorkflows(options).then((request) => request(axios, basePath));
},
};
};
/**
* WorkflowsApi - object-oriented interface
* @export
* @class WorkflowsApi
* @extends {BaseAPI}
*/
export class WorkflowsApi extends BaseAPI {
/**
*
* @summary
* @param {CreateWorkflowsRequest} createWorkflowsRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof WorkflowsApi
*/
public createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: AxiosRequestConfig) {
return WorkflowsApiFp(this.configuration).createWorkflows(createWorkflowsRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof WorkflowsApi
*/
public getWorkflows(options?: AxiosRequestConfig) {
return WorkflowsApiFp(this.configuration).getWorkflows(options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -17,6 +17,7 @@ import typistGroup from "features/workflow/typistGroup/typistGroupSlice";
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";
export const store = configureStore({
reducer: {
@ -38,6 +39,7 @@ export const store = configureStore({
worktype,
account,
template,
workflow,
},
});

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.8.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M24,32.3l-9.6-9.6l2.1-2.1l6,6V8h3v18.6l6-6l2.2,2.1L24,32.3z M11,40c-0.8,0-1.5-0.3-2.1-0.9
C8.3,38.5,8,37.8,8,37v-7.1h3V37h26v-7.1h3V37c0,0.8-0.3,1.5-0.9,2.1C38.5,39.7,37.8,40,37,40H11z"/>
</svg>

After

Width:  |  Height:  |  Size: 623 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.8.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M9,42c-0.8,0-1.5-0.3-2.1-0.9C6.3,40.5,6,39.8,6,39V28.5h3V39h30V9H9v10.5H6V9c0-0.8,0.3-1.5,0.9-2.1S8.2,6,9,6
h30c0.8,0,1.5,0.3,2.1,0.9C41.7,7.5,42,8.2,42,9v30c0,0.8-0.3,1.5-0.9,2.1C40.5,41.7,39.8,42,39,42H9z M20.6,33.6l-2.2-2.3l5.9-5.9
H6v-3h18.3l-5.9-5.9l2.2-2.2l9.7,9.6L20.6,33.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 721 B

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<g>
<path class="st0" d="M25.7,23.7c2.5,0.6,4.8,0.2,6.7-1.2s2.9-3.4,2.9-6.1s-1-4.8-2.9-6.1c-1.9-1.3-4.1-1.7-6.7-1.1
c0.9,1.1,1.5,2.2,1.9,3.3c0.4,1.1,0.6,2.5,0.6,4s-0.2,2.8-0.6,3.9C27.2,21.5,26.6,22.6,25.7,23.7z"/>
<path class="st0" d="M17.8,24c2.2,0,4-0.7,5.4-2.1c1.4-1.4,2.1-3.2,2.1-5.4c0-2.2-0.7-4-2.1-5.4C21.8,9.6,20,9,17.8,9
s-4,0.7-5.4,2.1c-1.4,1.4-2.1,3.2-2.1,5.4c0,2.2,0.7,4,2.1,5.4C13.8,23.2,15.5,24,17.8,24z M14.5,13.2c0.8-0.8,1.9-1.3,3.2-1.3
s2.4,0.4,3.2,1.3c0.9,0.9,1.3,1.9,1.3,3.2c0,1.3-0.4,2.4-1.3,3.2C20.1,20.5,19,21,17.8,21s-2.4-0.4-3.2-1.3
c-0.9-0.8-1.3-1.9-1.3-3.2C13.2,15.1,13.7,14.1,14.5,13.2z"/>
<path class="st0" d="M44,36.5c0-0.6-0.1-1.3-0.2-2.1l1.9-1.5l-1.8-2.8l-2.1,1c-0.4-0.4-1-0.8-1.7-1.2c-0.7-0.4-1.4-0.7-2-0.9
l-0.3-2.5h-3l-0.2,2.5c-0.7,0.2-1.4,0.5-2.1,0.9c-0.7,0.4-1.3,0.8-1.7,1.2l-2.1-1l-1.8,2.8l1.9,1.5c-0.2,0.8-0.2,1.5-0.2,2.1
c0,0.6,0.1,1.3,0.2,2.1l-1.9,1.5l1.8,2.7l2.1-1c0.4,0.5,1,0.9,1.7,1.3c0.7,0.4,1.4,0.7,2.1,0.9l0.2,2.4h3l0.3-2.4
c0.7-0.2,1.3-0.5,2-0.9c0.7-0.4,1.3-0.8,1.7-1.3l2.1,1l1.8-2.7l-1.9-1.5C43.9,37.8,44,37.1,44,36.5z M39.9,40.1
c-1,1-2.2,1.5-3.6,1.5c-1.5,0-2.7-0.5-3.7-1.5s-1.5-2.2-1.5-3.6c0-1.5,0.5-2.7,1.5-3.7s2.2-1.5,3.7-1.5c1.5,0,2.7,0.5,3.6,1.5
s1.5,2.2,1.5,3.7C41.3,38,40.8,39.2,39.9,40.1z"/>
<path class="st0" d="M5,37v-1.7c0-0.5,0.1-1,0.4-1.5s0.7-0.8,1.2-1.1c2.4-1.1,4.3-1.8,5.9-2.2c1.6-0.4,3.3-0.5,5.2-0.5
s3.7,0.2,5.2,0.5c0.4,0.1,0.9,0.2,1.4,0.4c0.7-0.8,1.5-1.6,2.4-2.3c-1.1-0.4-2.2-0.7-3.1-1c-1.9-0.5-3.8-0.7-5.9-0.7
s-4,0.2-5.9,0.7c-1.9,0.5-4,1.2-6.4,2.3c-1,0.5-1.9,1.2-2.5,2.1c-0.6,1-0.9,2-0.9,3.2V40h19.1c0-1,0.1-2,0.3-3H5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.9.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M9,42c-0.8,0-1.5-0.3-2.1-0.9C6.3,40.5,6,39.8,6,39V9c0-0.8,0.3-1.5,0.9-2.1S8.2,6,9,6h15v3H9v30h15v3H9z
M33.3,32.7l-2.1-2.1l5.1-5.1H18v-3h18.2l-5.1-5.1l2.1-2.1L42,24L33.3,32.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 614 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M6.6,40v-3h6.2l-0.7-0.7c-2-2-3.4-4-4.3-6.1s-1.3-4.1-1.3-6c0-3.6,1-6.9,3.1-9.8s4.8-4.8,8.2-5.9v3.1
c-2.5,1-4.6,2.6-6.1,4.9s-2.3,4.9-2.3,7.7c0,1.7,0.3,3.4,0.9,4.9c0.6,1.6,1.6,3,3,4.2l1.5,1.3v-5.9h3V40H6.6z M41.5,23.3h-3
c-0.1-1.7-0.4-3.3-1-4.7c-0.6-1.5-1.5-2.8-2.8-3.9l-1.5-1.4v5.8h-3V8h11.2v3h-6.2l0.8,0.7c1.9,1.8,3.3,3.8,4.2,5.8
S41.5,21.4,41.5,23.3z M31.9,42.6v-6.5h-6.5v-3.9h6.5v-6.5h3.9v6.5h6.5v3.9h-6.5v6.5H31.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 857 B

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M11,44c-0.8,0-1.5-0.3-2.1-0.9C8.3,42.5,8,41.8,8,41V7c0-0.8,0.3-1.5,0.9-2.1C9.5,4.3,10.2,4,11,4h17l12,12v5.8
h-3V18H26V7H11v34h13v3H11z M11,41V7V41z M34.9,45.7l-0.2-2.4c-0.7-0.2-1.4-0.5-2.1-0.9c-0.7-0.4-1.3-0.8-1.7-1.3l-2.1,1L27,39.5
l1.9-1.5c-0.2-0.8-0.2-1.5-0.2-2.1s0.1-1.3,0.2-2.1L27,32.3l1.8-2.8l2.1,1c0.4-0.4,1-0.8,1.7-1.2c0.7-0.4,1.4-0.7,2.1-0.9l0.2-2.5h3
l0.3,2.5c0.7,0.2,1.3,0.5,2,0.9s1.3,0.8,1.7,1.2l2.1-1l1.8,2.8l-1.9,1.5c0.2,0.8,0.2,1.5,0.2,2.1S44,37.2,43.9,38l1.9,1.5L44,42.2
l-2.1-1c-0.4,0.5-1,0.9-1.7,1.3c-0.7,0.4-1.4,0.7-2,0.9l-0.3,2.4H34.9z M36.4,41c1.5,0,2.7-0.5,3.6-1.5c1-1,1.5-2.2,1.5-3.7
S41,33.2,40,32.2c-1-1-2.2-1.5-3.6-1.5c-1.5,0-2.7,0.5-3.7,1.5c-1,1-1.5,2.2-1.5,3.6s0.5,2.7,1.5,3.7C33.7,40.5,34.9,41,36.4,41z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M9,40c-0.8,0-1.5-0.3-2.1-0.9C6.3,38.5,6,37.8,6,37V7c0-0.8,0.3-1.5,0.9-2.1S8.2,4,9,4h30
c0.8,0,1.5,0.3,2.1,0.9C41.7,5.5,42,6.2,42,7v15.1c-0.3,0-0.5-0.1-0.7-0.1c-0.2,0-0.5,0-0.7,0H39V7H9v30h6.9c0.2,0.5,0.3,1,0.5,1.5
s0.4,1,0.7,1.5H9z M9,34v3V7V34z M14.5,31.5H16c0.4-1.2,1-2.5,1.8-3.7c0.8-1.2,1.8-2.3,2.8-3.3h-6.1V31.5z M14.5,19.5h7v-7h-7V19.5z
M26.5,19.5h7v-7h-7V19.5z M31.5,45.8l-0.2-2.4c-0.7-0.2-1.4-0.5-2-0.9c-0.7-0.4-1.3-0.8-1.7-1.3l-2.1,1l-1.8-2.7l1.9-1.5
c-0.2-0.8-0.2-1.5-0.2-2.1c0-0.6,0.1-1.3,0.2-2.1l-1.9-1.5l1.8-2.7l2.1,1c0.4-0.4,1-0.8,1.7-1.2c0.7-0.4,1.4-0.7,2-0.9l0.2-2.5h3
l0.3,2.5c0.7,0.2,1.3,0.5,2,0.9s1.3,0.8,1.7,1.2l2.1-1l1.8,2.7l-1.9,1.5c0.2,0.8,0.2,1.5,0.2,2.1c0,0.6-0.1,1.3-0.2,2.1l1.9,1.5
l-1.8,2.7l-2.1-1c-0.4,0.5-1,0.9-1.7,1.3c-0.7,0.4-1.4,0.7-2,0.9l-0.3,2.4H31.5z M33,41.1c1.5,0,2.7-0.5,3.6-1.5
c1-1,1.5-2.2,1.5-3.6c0-1.5-0.5-2.7-1.5-3.7c-1-1-2.2-1.5-3.6-1.5c-1.5,0-2.7,0.5-3.7,1.5s-1.5,2.2-1.5,3.7c0,1.5,0.5,2.7,1.5,3.6
S31.5,41.1,33,41.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -55,4 +55,5 @@ export const errorCodes = [
"E011001", // ワークタイプ重複エラー
"E011002", // ワークタイプ登録上限超過エラー
"E011003", // ワークタイプ不在エラー
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
] as const;

View File

@ -117,6 +117,10 @@ export const dictationSlice = createSlice({
state.apps.direction = action.payload.direction;
state.apps.paramName = action.payload.paramName;
});
// 画面起動時にgetSortColumnAsyncがrejectedするとisLoadingがtrueのままになるため
builder.addCase(getSortColumnAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(listTypistsAsync.fulfilled, (state, action) => {
state.domain.typists = action.payload.typists;
});

View File

@ -41,6 +41,13 @@ export const listUsersAsync = createAsyncThunk<
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

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

View File

@ -0,0 +1,213 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import {
AccountsApi,
Author,
Configuration,
GetWorkflowsResponse,
TemplateFile,
TemplatesApi,
Typist,
TypistGroup,
WorkflowTypist,
WorkflowsApi,
Worktype,
} from "api";
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,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/listWorkflowAsync", 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);
try {
const { data } = await workflowsApi.getWorkflows({
headers: { authorization: `Bearer ${accessToken}` },
});
return 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 createWorkflowAsync = createAsyncThunk<
{
/* Empty Object */
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/createWorkflowAsync", 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 { selectedAssignees, authorId, templateId, worktypeId } =
state.workflow.apps;
try {
if (authorId === undefined) {
throw new Error("authorId is not found");
}
// 選択されたタイピストを取得し、リクエスト用の型に変換する
const typists = selectedAssignees.map(
(item): WorkflowTypist => ({
typistId: item.typistUserId,
typistGroupId: item.typistGroupId,
})
);
await workflowsApi.createWorkflows(
{
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;
// AuthorIDとWorktypeIDが一致するものが既に存在する場合
if (code === "E013001") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"workflowPage.message.workflowConflictError"
),
})
);
return thunkApi.rejectWithValue({ error });
}
// パラメータが存在しない場合
if (statusCode === 400) {
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[];
typists: Typist[];
typistGroups: TypistGroup[];
templates: TemplateFile[];
worktypes: Worktype[];
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/getworkflowRelationsAsync", async (args, thunkApi) => {
// 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);
const templatesApi = new TemplatesApi(config);
try {
const { authors } = (
await accountsApi.getAuthors({
headers: { authorization: `Bearer ${accessToken}` },
})
).data;
const { typists } = (
await accountsApi.getTypists({
headers: { authorization: `Bearer ${accessToken}` },
})
).data;
const { typistGroups } = (
await accountsApi.getTypistGroups({
headers: { authorization: `Bearer ${accessToken}` },
})
).data;
const { templates } = (
await templatesApi.getTemplates({
headers: { authorization: `Bearer ${accessToken}` },
})
).data;
const { worktypes } = (
await accountsApi.getWorktypes({
headers: { authorization: `Bearer ${accessToken}` },
})
).data;
return {
authors,
typists,
typistGroups,
templates,
worktypes,
};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,52 @@
import { Assignee } from "api";
import { RootState } from "app/store";
export const selectWorkflows = (state: RootState) =>
state.workflow.domain.workflows;
export const selectIsLoading = (state: RootState) =>
state.workflow.apps.isLoading;
export const selectWorkflowRelations = (state: RootState) =>
state.workflow.domain.workflowRelations;
export const selectWorkflowAssinee = (state: RootState) => {
// 選択されたassigneeを取得
const { selectedAssignees } = state.workflow.apps;
// すべてのassigneeを取得
const assignees = state.workflow.domain.workflowRelations?.assignees ?? [];
// assigneeが選択されているかどうかを判定する
const isAssigneeSelected = (assignee: Assignee) =>
selectedAssignees.some(
(sa) =>
sa.typistUserId === assignee.typistUserId &&
sa.typistGroupId === assignee.typistGroupId
);
// 未選択のassigneeを取得する
const poolAssignees = assignees.filter(
(assignee) => !isAssigneeSelected(assignee)
);
// selectedAssigneesとpoolAssigneesをtypistNameでソートして返す
return {
selectedAssignees: [...selectedAssignees].sort((a, b) =>
a.typistName.localeCompare(b.typistName)
),
poolAssignees: poolAssignees.sort((a, b) =>
a.typistName.localeCompare(b.typistName)
),
};
};
export const selectIsAddLoading = (state: RootState) =>
state.workflow.apps.isAddLoading;
export const selectWorkflowError = (state: RootState) => {
// authorIdがundefinedの場合はエラーを返す
const hasAuthorIdEmptyError = state.workflow.apps.authorId === undefined;
// workflowAssineeのselectedが空の場合はエラーを返す
const hasSelectedWorkflowAssineeEmptyError =
state.workflow.apps.selectedAssignees.length === 0;
return {
hasAuthorIdEmptyError,
hasSelectedWorkflowAssineeEmptyError,
};
};

View File

@ -0,0 +1,27 @@
import { Assignee, Author, TemplateFile, Workflow, Worktype } from "api";
export interface WorkflowState {
apps: Apps;
domain: Domain;
}
export interface Apps {
isLoading: boolean;
isAddLoading: boolean;
selectedAssignees: Assignee[];
authorId?: number;
worktypeId?: number;
templateId?: number;
}
export interface Domain {
workflows?: Workflow[];
workflowRelations?: WorkflowRelations;
}
export interface WorkflowRelations {
authors: Author[];
assignees: Assignee[];
templates: TemplateFile[];
worktypes: Worktype[];
}

View File

@ -0,0 +1,142 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { Assignee } from "api";
import {
createWorkflowAsync,
getworkflowRelationsAsync,
listWorkflowAsync,
} from "./operations";
import { WorkflowState } from "./state";
const initialState: WorkflowState = {
apps: {
isLoading: false,
isAddLoading: false,
selectedAssignees: [],
},
domain: {},
};
export const workflowSlice = createSlice({
name: "workflow",
initialState,
reducers: {
clearWorkflow: (state) => {
state.apps.selectedAssignees = [];
state.apps.authorId = undefined;
state.apps.worktypeId = undefined;
state.apps.templateId = undefined;
state.domain.workflowRelations = undefined;
},
addAssignee: (state, action: PayloadAction<{ assignee: Assignee }>) => {
const { assignee } = action.payload;
const { selectedAssignees } = state.apps;
// assigneeがselectedAssigneesに存在するか確認する
const isDuplicate = selectedAssignees.some(
(x) =>
x.typistUserId === assignee.typistUserId &&
x.typistGroupId === assignee.typistGroupId
);
// 重複していなければ追加する
if (!isDuplicate) {
const newSelectedAssignees = [...selectedAssignees, assignee];
// stateに保存する
state.apps.selectedAssignees = newSelectedAssignees;
}
},
removeAssignee: (state, action: PayloadAction<{ assignee: Assignee }>) => {
const { assignee } = action.payload;
const { selectedAssignees } = state.apps;
// selectedAssigneeの要素からassigneeを削除する
state.apps.selectedAssignees = selectedAssignees.filter(
(x) =>
x.typistUserId !== assignee.typistUserId ||
x.typistGroupId !== assignee.typistGroupId
);
},
changeAuthor: (state, action: PayloadAction<{ authorId: number }>) => {
const { authorId } = action.payload;
state.apps.authorId = authorId;
},
changeWorktype: (state, action: PayloadAction<{ worktypeId?: number }>) => {
const { worktypeId } = action.payload;
state.apps.worktypeId = worktypeId;
},
changeTemplate: (state, action: PayloadAction<{ templateId?: number }>) => {
const { templateId } = action.payload;
state.apps.templateId = templateId;
},
},
extraReducers: (builder) => {
builder.addCase(listWorkflowAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(listWorkflowAsync.fulfilled, (state, action) => {
const { workflows } = action.payload;
state.domain.workflows = workflows;
state.apps.isLoading = false;
});
builder.addCase(listWorkflowAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getworkflowRelationsAsync.pending, (state) => {
state.apps.isAddLoading = true;
});
builder.addCase(getworkflowRelationsAsync.fulfilled, (state, action) => {
const { authors, typistGroups, typists, templates, worktypes } =
action.payload;
// 取得したtypistsとtypistGroupsを型変換
const assineeTypists = typists.map(
(typist): Assignee => ({
typistUserId: typist.id,
typistGroupId: undefined,
typistName: typist.name,
})
);
const assineeTypistGroups = typistGroups.map(
(typistGroup): Assignee => ({
typistUserId: undefined,
typistGroupId: typistGroup.id,
typistName: typistGroup.name,
})
);
// 取得したtypistsとtypistGroupsを結合
const assinees = [...assineeTypists, ...assineeTypistGroups];
// storeに保存
state.domain.workflowRelations = {
authors,
assignees: assinees,
templates,
worktypes,
};
state.apps.isAddLoading = false;
});
builder.addCase(getworkflowRelationsAsync.rejected, (state) => {
state.apps.isAddLoading = false;
});
builder.addCase(createWorkflowAsync.pending, (state) => {
state.apps.isAddLoading = true;
});
builder.addCase(createWorkflowAsync.fulfilled, (state) => {
state.apps.isAddLoading = false;
});
builder.addCase(createWorkflowAsync.rejected, (state) => {
state.apps.isAddLoading = false;
});
},
});
export const {
addAssignee,
removeAssignee,
changeAuthor,
changeWorktype,
changeTemplate,
clearWorkflow,
} = workflowSlice.actions;
export default workflowSlice.reducer;

View File

@ -0,0 +1,54 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import Header from "components/header";
import Footer from "components/footer";
import styles from "styles/app.module.scss";
import { Link } from "react-router-dom";
import { clearToken } from "features/auth";
import { AppDispatch } from "app/store";
import { useDispatch } from "react-redux";
export const AccountDeleteSuccess: React.FC = (): JSX.Element => {
const { t } = useTranslation();
const dispatch: AppDispatch = useDispatch();
// アカウントの削除完了時に遷移するページなので、遷移と同時にログアウト状態とする
useEffect(() => {
dispatch(clearToken());
}, [dispatch]);
return (
<div className={styles.wrap}>
<Header />
<main className={styles.main}>
<div className={styles.mainSmall}>
<div>
<h1 className={styles.marginBtm1}>
{t(getTranslationID("accountDeleteSuccess.label.title"))}
</h1>
</div>
<section>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dd className={`${styles.full} ${styles.alignCenter}`}>
{t(getTranslationID("accountDeleteSuccess.label.message"))}
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<Link to="/">
{t(
getTranslationID(
"accountDeleteSuccess.label.backToTopPageLink"
)
)}
</Link>
</dd>
</dl>
</section>
</div>
</main>
<Footer />
</div>
);
};

View File

@ -2,18 +2,19 @@ import React, { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { AppDispatch } from "app/store";
import { useDispatch, useSelector } from "react-redux";
import { selectAccountInfo, selectIsLoading } from "features/account";
import { deleteAccountAsync } from "features/account/operations";
import { useMsal } from "@azure/msal-react";
import styles from "../../styles/app.module.scss";
import { getTranslationID } from "../../translation";
import close from "../../assets/images/close.svg";
import deleteButton from "../../assets/images/delete.svg";
import { selectAccountInfo, selectIsLoading } from "features/account";
import { deleteAccountAsync } from "features/account/operations";
interface deleteAccountPopupProps {
interface DeleteAccountPopupProps {
onClose: () => void;
}
export const DeleteAccountPopup: React.FC<deleteAccountPopupProps> = (
export const DeleteAccountPopup: React.FC<DeleteAccountPopupProps> = (
props
) => {
const { onClose } = props;
@ -21,6 +22,8 @@ export const DeleteAccountPopup: React.FC<deleteAccountPopupProps> = (
const dispatch: AppDispatch = useDispatch();
const isLoading = useSelector(selectIsLoading);
const { instance } = useMsal();
const accountInfo = useSelector(selectAccountInfo);
// ポップアップを閉じる処理
@ -31,13 +34,21 @@ export const DeleteAccountPopup: React.FC<deleteAccountPopupProps> = (
onClose();
}, [isLoading, onClose]);
const onDeleteAccount = useCallback(() => {
dispatch(
const onDeleteAccount = useCallback(async () => {
const { meta } = await dispatch(
deleteAccountAsync({
accountId: accountInfo.account.accountId,
})
);
}, [dispatch]);
// 削除成功後にAccountDeleteSuccess ページに遷移
if (meta.requestStatus === "fulfilled") {
instance.logoutRedirect({
postLogoutRedirectUri: "/accountDeleteSuccess",
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [instance]);
// HTML
return (
@ -74,7 +85,7 @@ export const DeleteAccountPopup: React.FC<deleteAccountPopupProps> = (
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="submit"
type="button"
name="submit"
value={t(
getTranslationID("deleteAccountPopup.label.deleteButton")

View File

@ -331,6 +331,16 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
</tr>
))}
</table>
{!isLoading && licenseOrderHistory.length === 0 && (
<p
style={{
textAlign: "center",
width: "1000px",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
</div>
{isLoading && (
<img

View File

@ -452,6 +452,16 @@ const PartnerLicense: React.FC = (): JSX.Element => {
</tr>
))}
</table>
{!isLoading && childrenPartnerLicensesInfo.length === 0 && (
<p
style={{
margin: "10px",
textAlign: "center",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{/* pagenation */}
<div className={styles.pagenation}>
<nav className={styles.pagenationNav}>

View File

@ -189,6 +189,16 @@ const PartnerPage: React.FC = (): JSX.Element => {
</tr>
))}
</table>
{!isLoading && partnerInfo.partners.length === 0 && (
<p
style={{
textAlign: "center",
width: "1420px",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{/** pagenation */}
<div className={styles.pagenation}>
<nav className={styles.pagenationNav}>

View File

@ -101,16 +101,17 @@ export const TemplateFilePage: React.FC = () => {
</td>
</tr>
))}
{!isLoading && templates?.length === 0 && (
<p
style={{
margin: "10px",
textAlign: "center",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{!isLoading &&
(templates === undefined || templates.length === 0) && (
<p
style={{
margin: "10px",
textAlign: "center",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{isLoading && (
<img
src={progress_activit}

View File

@ -290,6 +290,16 @@ const UserListPage: React.FC = (): JSX.Element => {
})}
</tbody>
</table>
{!isLoading && users.length === 0 && (
<p
style={{
margin: "10px",
textAlign: "center",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{isLoading && (
<img
src={progress_activit}

View File

@ -265,17 +265,18 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
</tr>
))}
</table>
{!isLoading && worktypes?.length === 0 && (
<p
style={{
margin: "10px",
textAlign: "center",
width: "1000px",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{!isLoading &&
(worktypes === undefined || worktypes.length === 0) && (
<p
style={{
margin: "10px",
textAlign: "center",
width: "1000px",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{isLoading && (
<img
src={progress_activit}

View File

@ -0,0 +1,293 @@
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,
} from "features/workflow";
import {
createWorkflowAsync,
getworkflowRelationsAsync,
listWorkflowAsync,
} 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 AddWorkflowPopupProps {
onClose: () => void;
}
export const AddWorkflowPopup: React.FC<AddWorkflowPopupProps> = (
props
): JSX.Element => {
const { onClose } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
// 保存ボタンを押したかどうか
const [isPushAddButton, setIsPushAddButton] = useState<boolean>(false);
const workflowRelations = useSelector(selectWorkflowRelations);
const { poolAssignees, selectedAssignees } = useSelector(
selectWorkflowAssinee
);
const isLoading = useSelector(selectIsAddLoading);
const { hasAuthorIdEmptyError, hasSelectedWorkflowAssineeEmptyError } =
useSelector(selectWorkflowError);
useEffect(() => {
dispatch(getworkflowRelationsAsync());
// ポップアップのアンマウント時に初期化を行う
return () => {
dispatch(clearWorkflow());
setIsPushAddButton(false);
};
}, [dispatch]);
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 handleAdd = useCallback(async () => {
setIsPushAddButton(true);
// エラーチェック
if (hasAuthorIdEmptyError || hasSelectedWorkflowAssineeEmptyError) {
return;
}
const { meta } = await dispatch(createWorkflowAsync());
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.addRoutingRule"))}
{/* 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}
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>
{isPushAddButton && 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}
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>
{isPushAddButton && 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}
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={handleAdd}
/>
{isLoading && (
<img
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -1,34 +1,195 @@
import React from "react";
import React, { useEffect, useState } from "react";
import Header from "components/header";
import Footer from "components/footer";
import styles from "styles/app.module.scss";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import ruleAddImg from "assets/images/rule_add.svg";
import templateSettingImg from "assets/images/template_setting.svg";
import worktypeSettingImg from "assets/images/worktype_setting.svg";
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 progress_activit from "assets/images/progress_activit.svg";
import { getTranslationID } from "translation";
import { AddWorkflowPopup } from "./addworkflowPopup";
const WorkflowPage: React.FC = (): JSX.Element => (
<div className={styles.wrap}>
<Header userName="XXXXXX" />
<UpdateTokenTimer />
<main className={styles.main}>
<div className="">
<span>
<a style={{ margin: 20 }} href="/workflow/typist-group">
Transcriptionist Group Setting
</a>
</span>
<span>
<a style={{ margin: 20 }} href="/workflow/worktype-id">
Worktype ID Setting
</a>
</span>
<span>
<a style={{ margin: 20 }} href="/workflow/template">
Template File
</a>
</span>
const WorkflowPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
// 追加Popupの表示制御
const [isShowAddPopup, setIsShowAddPopup] = useState<boolean>(false);
const workflows = useSelector(selectWorkflows);
const isLoading = useSelector(selectIsLoading);
useEffect(() => {
dispatch(listWorkflowAsync());
}, [dispatch]);
return (
<>
{isShowAddPopup && (
<AddWorkflowPopup
onClose={() => {
setIsShowAddPopup(false);
}}
/>
)}
<div className={styles.wrap}>
<Header userName="XXXXXX" />
<UpdateTokenTimer />
<main className={styles.main}>
<div className="">
<div className={styles.pageHeader}>
<h1 className={styles.pageTitle}>
{t(getTranslationID("workflowPage.label.title"))}
</h1>
</div>
<section className={styles.workflow}>
<div>
<ul className={`${styles.menuAction} ${styles.alignRight}`}>
<li className={styles.floatLeft}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={() => {
setIsShowAddPopup(true);
}}
>
<img
src={ruleAddImg}
alt="addRoutingRule"
className={styles.menuIcon}
/>
{t(getTranslationID("workflowPage.label.addRoutingRule"))}
</a>
</li>
<li>
<a
href="/workflow/template"
className={`${styles.menuLink} ${styles.isActive}`}
>
<img
src={templateSettingImg}
alt="templateSetting"
className={styles.menuIcon}
/>
{t(
getTranslationID("workflowPage.label.templateSetting")
)}
</a>
</li>
<li>
<a
href="/workflow/worktype-id"
className={`${styles.menuLink} ${styles.isActive}`}
>
<img
src={worktypeSettingImg}
alt="worktypeIdSetting"
className={styles.menuIcon}
/>
{t(
getTranslationID("workflowPage.label.worktypeIdSetting")
)}
</a>
</li>
<li>
<a
href="/workflow/typist-group"
className={`${styles.menuLink} ${styles.isActive}`}
>
<img
src={groupSettingImg}
alt="typistGroupSetting"
className={styles.menuIcon}
/>
{t(
getTranslationID(
"workflowPage.label.typistGroupSetting"
)
)}
</a>
</li>
</ul>
<table className={`${styles.table} ${styles.workflow}`}>
<tr className={styles.tableHeader}>
<th className={styles.clm0}>{/** empty th */}</th>
<th>
{t(getTranslationID("workflowPage.label.authorID"))}
</th>
<th>
{t(getTranslationID("workflowPage.label.worktype"))}
</th>
<th>
{t(
getTranslationID("workflowPage.label.transcriptionist")
)}
</th>
<th>
{t(getTranslationID("workflowPage.label.template"))}
</th>
</tr>
{workflows?.map((workflow) => (
<tr key={workflow.id}>
<td className={styles.clm0}>
<ul className={styles.menuInTable}>
<li>
<a href="">
{t(
getTranslationID("workflowPage.label.editRule")
)}
</a>
</li>
<li>
<a href="">
{t(getTranslationID("common.label.delete"))}
</a>
</li>
</ul>
</td>
<td>{workflow.author.authorId}</td>
<td>{workflow.worktype?.worktypeId ?? "-"}</td>
<td className={styles.txWsline}>
{workflow.typists.map((typist, i) => (
<>
{typist.typistName}
{i !== workflow.typists.length - 1 && <br />}
</>
))}
</td>
<td>{workflow.template?.fileName ?? "-"}</td>
</tr>
))}
</table>
{!isLoading &&
(workflows === undefined || workflows.length === 0) && (
<p
style={{
margin: "10px",
textAlign: "center",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{isLoading && (
<img
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</div>
</section>
</div>
</main>
<Footer />
</div>
</main>
<Footer />
</div>
);
</>
);
};
export default WorkflowPage;

View File

@ -356,7 +356,29 @@
},
"workflowPage": {
"label": {
"title": "Arbeitsablauf"
"title": "Arbeitsablauf",
"addRoutingRule": "(de)Add Routing Rule",
"templateSetting": "(de)Template Setting",
"worktypeIdSetting": "(de)WorktypeID Setting",
"typistGroupSetting": "(de)Transcriptionist Group Setting",
"authorID": "Autoren-ID",
"worktype": "Aufgabentypkennung",
"worktypeOptional": "(de)Worktype ID (Optional)",
"transcriptionist": "Transkriptionist",
"template": "(de)Template",
"templateOptional": "(de)Template (Optional)",
"editRule": "(de)Edit Rule",
"selected": "Ausgewählter transkriptionist",
"pool": "Transkriptionsliste",
"selectAuthor": "(de)Select Author ID",
"selectWorktypeId": "(de)Select Worktype ID",
"selectTemplate": "(de)Select Template"
},
"message": {
"selectedTypistEmptyError": "(de)Transcriptionist,TranscriptionistGroupがいないルーティングルールは保存できません。ルーティング先を1つ以上選択してください。",
"workflowConflictError": "(de)指定したAuthorIDとWorktypeIDの組み合わせで既にルーティングルールが登録されています。他の組み合わせで登録してください。",
"inputEmptyError": "Pflichtfeld",
"saveFailedError": "(de)ルーティングルールの保存に失敗しました。画面を更新し、再度実行してください"
}
},
"typistGroupSetting": {
@ -468,5 +490,12 @@
"deleteButton": "(de)Delete account",
"cancelButton": "(de)Cancel"
}
},
"accountDeleteSuccess": {
"label": {
"title": "(de)Account Delete Success",
"message": "(de)Your account has been deleted. Thank you for using our services.",
"backToTopPageLink": "(de)Back to TOP Page"
}
}
}

View File

@ -356,7 +356,29 @@
},
"workflowPage": {
"label": {
"title": "Workflow"
"title": "Workflow",
"addRoutingRule": "Add Routing Rule",
"templateSetting": "Template Setting",
"worktypeIdSetting": "WorktypeID Setting",
"typistGroupSetting": "Transcriptionist Group Setting",
"authorID": "Author ID",
"worktype": "Worktype ID",
"worktypeOptional": "Worktype ID (Optional)",
"transcriptionist": "Transcriptionist",
"template": "Template",
"templateOptional": "Template (Optional)",
"editRule": "Edit Rule",
"selected": "Selected Transcriptionist",
"pool": "Transcription List",
"selectAuthor": "Select Author ID",
"selectWorktypeId": "Select Worktype ID",
"selectTemplate": "Select Template"
},
"message": {
"selectedTypistEmptyError": "Transcriptionist,TranscriptionistGroupがいないルーティングルールは保存できません。ルーティング先を1つ以上選択してください。",
"workflowConflictError": "指定したAuthorIDとWorktypeIDの組み合わせで既にルーティングルールが登録されています。他の組み合わせで登録してください。",
"inputEmptyError": "Mandatory Field",
"saveFailedError": "ルーティングルールの保存に失敗しました。画面を更新し、再度実行してください"
}
},
"typistGroupSetting": {
@ -468,5 +490,12 @@
"deleteButton": "Delete account",
"cancelButton": "Cancel"
}
},
"accountDeleteSuccess": {
"label": {
"title": "Account Delete Success",
"message": "Your account has been deleted. Thank you for using our services.",
"backToTopPageLink": "Back to TOP Page"
}
}
}

View File

@ -356,7 +356,29 @@
},
"workflowPage": {
"label": {
"title": "flujo de trabajo"
"title": "flujo de trabajo",
"addRoutingRule": "(es)Add Routing Rule",
"templateSetting": "(es)Template Setting",
"worktypeIdSetting": "(es)WorktypeID Setting",
"typistGroupSetting": "(es)Transcriptionist Group Setting",
"authorID": "ID de autor",
"worktype": "ID de tipo de trabajo",
"worktypeOptional": "(es)Worktype ID (Optional)",
"transcriptionist": "Transcriptor",
"template": "(es)Template",
"templateOptional": "(es)Template (Optional)",
"editRule": "(es)Edit Rule",
"selected": "Transcriptor seleccionado",
"pool": "Lista de transcriptor",
"selectAuthor": "(es)Select Author ID",
"selectWorktypeId": "(es)Select Worktype ID",
"selectTemplate": "(es)Select Template"
},
"message": {
"selectedTypistEmptyError": "(es)Transcriptionist,TranscriptionistGroupがいないルーティングルールは保存できません。ルーティング先を1つ以上選択してください。",
"workflowConflictError": "(es)指定したAuthorIDとWorktypeIDの組み合わせで既にルーティングルールが登録されています。他の組み合わせで登録してください。",
"inputEmptyError": "Campo obligatorio",
"saveFailedError": "(es)ルーティングルールの保存に失敗しました。画面を更新し、再度実行してください"
}
},
"typistGroupSetting": {
@ -468,5 +490,12 @@
"deleteButton": "(es)Delete account",
"cancelButton": "(es)Cancel"
}
},
"accountDeleteSuccess": {
"label": {
"title": "(es)Account Delete Success",
"message": "(es)Your account has been deleted. Thank you for using our services.",
"backToTopPageLink": "(es)Back to TOP Page"
}
}
}

View File

@ -356,7 +356,29 @@
},
"workflowPage": {
"label": {
"title": "Flux de travail"
"title": "Flux de travail",
"addRoutingRule": "(fr)Add Routing Rule",
"templateSetting": "(fr)Template Setting",
"worktypeIdSetting": "(fr)WorktypeID Setting",
"typistGroupSetting": "(fr)Transcriptionist Group Setting",
"authorID": "Identifiant Auteur",
"worktype": "Identifiant du Type de travail",
"worktypeOptional": "(fr)Worktype ID (Optional)",
"transcriptionist": "Transcriptionniste",
"template": "(fr)Template",
"templateOptional": "(fr)Template (Optional)",
"editRule": "(fr)Edit Rule",
"selected": "Transcriptionniste sélectionné",
"pool": "Liste de transcriptionniste",
"selectAuthor": "(fr)Select Author ID",
"selectWorktypeId": "(fr)Select Worktype ID",
"selectTemplate": "(fr)Select Template"
},
"message": {
"selectedTypistEmptyError": "(fr)Transcriptionist,TranscriptionistGroupがいないルーティングルールは保存できません。ルーティング先を1つ以上選択してください。",
"workflowConflictError": "(fr)指定したAuthorIDとWorktypeIDの組み合わせで既にルーティングルールが登録されています。他の組み合わせで登録してください。",
"inputEmptyError": "Champ obligatoire",
"saveFailedError": "(fr)ルーティングルールの保存に失敗しました。画面を更新し、再度実行してください"
}
},
"typistGroupSetting": {
@ -468,5 +490,12 @@
"deleteButton": "(fr)Delete account",
"cancelButton": "(fr)Cancel"
}
},
"accountDeleteSuccess": {
"label": {
"title": "(fr)Account Delete Success",
"message": "(fr)Your account has been deleted. Thank you for using our services.",
"backToTopPageLink": "(fr)Back to TOP Page"
}
}
}

View File

@ -2,6 +2,7 @@ version: '3'
services:
dictation_server:
env_file: ../.env
build: .
working_dir: /app/dictation_server
ports:

View File

@ -1,16 +1,5 @@
DB_HOST=omds-mysql
DB_PORT=3306
DB_EXTERNAL_PORT=3306
DB_NAME=omds
DB_ROOT_PASS=omdsdbpass
DB_USERNAME=omdsdbuser
DB_PASSWORD=omdsdbpass
NO_COLOR=TRUE
ACCESS_TOKEN_LIFETIME_WEB=7200000
REFRESH_TOKEN_LIFETIME_WEB=86400000
REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000
TENANT_NAME=adb2codmsdev
SIGNIN_FLOW_NAME=b2c_1_signin_dev
EMAIL_CONFIRM_LIFETIME=86400000
APP_DOMAIN=https://10.1.0.10:4443/
STORAGE_TOKEN_EXPIRE_TIME=2

View File

@ -1,15 +1,14 @@
STAGE=local
NO_COLOR=TRUE
CORS=TRUE
PORT=8081
AZURE_TENANT_ID=xxxxxxxx
AZURE_CLIENT_ID=xxxxxxxx
AZURE_CLIENT_SECRET=xxxxxxxx
# 開発環境ではADB2Cが別テナントになる都合上、環境変数を分けている
TENANT_NAME=adb2codmsdev
SIGNIN_FLOW_NAME=b2c_1_signin_dev
ADB2C_TENANT_ID=xxxxxxxx
ADB2C_CLIENT_ID=xxxxxxxx
ADB2C_CLIENT_SECRET=xxxxxxxx
ADB2C_ORIGIN=https://zzzzzzzzzz
KEY_VAULT_NAME=kv-odms-secret-dev
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n"
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n"
SENDGRID_API_KEY=xxxxxxxxxxxxxxxx
@ -17,6 +16,7 @@ MAIL_FROM=xxxxx@xxxxx.xxxx
NOTIFICATION_HUB_NAME=ntf-odms-dev
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
APP_DOMAIN=http://localhost:8081/
STORAGE_TOKEN_EXPIRE_TIME=30
STORAGE_ACCOUNT_NAME_US=saodmsusdev
STORAGE_ACCOUNT_NAME_AU=saodmsaudev
STORAGE_ACCOUNT_NAME_EU=saodmseudev
@ -25,4 +25,8 @@ STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX
STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX
STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA
ACCESS_TOKEN_LIFETIME_WEB=7200000
REFRESH_TOKEN_LIFETIME_WEB=86400000
REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000
EMAIL_CONFIRM_LIFETIME=86400000

View File

@ -0,0 +1,44 @@
-- +migrate Up
CREATE TABLE IF NOT EXISTS `workflows` (
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'workflowの内部ID',
`account_id` BIGINT UNSIGNED NOT NULL COMMENT 'アカウントID',
`author_id` BIGINT UNSIGNED NOT NULL COMMENT 'authorユーザーの内部ID',
`worktype_id`BIGINT UNSIGNED COMMENT 'Worktypeの内部ID',
`template_id` BIGINT UNSIGNED COMMENT 'テンプレートファイルの内部ID',
`created_by` VARCHAR(255) COMMENT '作成者',
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
`updated_by` VARCHAR(255) COMMENT '更新者',
`updated_at` TIMESTAMP DEFAULT now() on UPDATE now() COMMENT '更新時刻',
UNIQUE worktype_id_index (account_id, author_id, worktype_id),
CONSTRAINT `workflows_fk_account_id` FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
CONSTRAINT `workflows_fk_author_id` FOREIGN KEY (author_id) REFERENCES users(id),
CONSTRAINT `workflows_fk_worktype_id` FOREIGN KEY (worktype_id) REFERENCES worktypes(id),
CONSTRAINT `workflows_fk_template_id` FOREIGN KEY (template_id) REFERENCES template_files(id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS `workflow_typists` (
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'worktypeの内部ID',
`workflow_id` BIGINT UNSIGNED NOT NULL COMMENT 'workflowの内部ID',
`typist_id` BIGINT UNSIGNED COMMENT 'タイピストユーザーの内部ID',
`typist_group_id` BIGINT UNSIGNED COMMENT 'タイピストグループの内部ID',
`created_by` VARCHAR(255) COMMENT '作成者',
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
`updated_by` VARCHAR(255) COMMENT '更新者',
`updated_at` TIMESTAMP DEFAULT now() on UPDATE now() COMMENT '更新時刻',
CONSTRAINT `workflow_typists_fk_workflow_id` FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE,
CONSTRAINT `workflow_typists_fk_typist_id` FOREIGN KEY (typist_id) REFERENCES users(id),
CONSTRAINT `workflow_typists_fk_typist_group_id` FOREIGN KEY (typist_group_id) REFERENCES user_group(id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
-- +migrate Down
ALTER TABLE `workflows` DROP FOREIGN KEY `workflows_fk_account_id`;
ALTER TABLE `workflows` DROP FOREIGN KEY `workflows_fk_author_id`;
ALTER TABLE `workflows` DROP FOREIGN KEY `workflows_fk_worktype_id`;
ALTER TABLE `workflows` DROP FOREIGN KEY `workflows_fk_template_id`;
ALTER TABLE `workflow_typists` DROP FOREIGN KEY `workflow_typists_fk_workflow_id`;
ALTER TABLE `workflow_typists` DROP FOREIGN KEY `workflow_typists_fk_typist_id`;
ALTER TABLE `workflow_typists` DROP FOREIGN KEY `workflow_typists_fk_typist_group_id`;
DROP TABLE IF EXISTS `workflows`;
DROP TABLE IF EXISTS `workflow_typists`;

View File

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

View File

@ -0,0 +1,13 @@
-- +migrate Up
CREATE TABLE IF NOT EXISTS `terms` (
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT '通番',
`document_type` VARCHAR(255) NOT NULL COMMENT '規約種別EULA/DPA',
`version` VARCHAR(255) NOT NULL COMMENT 'バージョン',
`created_by` VARCHAR(255) COMMENT '作成者',
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
`updated_by` VARCHAR(255) COMMENT '更新者',
`updated_at` TIMESTAMP DEFAULT now() on UPDATE now() COMMENT '更新時刻'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
-- +migrate Down
DROP TABLE IF EXISTS `terms`;

View File

@ -0,0 +1,13 @@
-- +migrate Up
ALTER TABLE `users` CHANGE COLUMN `accepted_terms_version` `accepted_eula_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(EULA)',
ADD COLUMN `accepted_dpa_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(DPA)' AFTER `accepted_eula_version`;
ALTER TABLE `users_archive` CHANGE COLUMN `accepted_terms_version` `accepted_eula_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(EULA)',
ADD COLUMN `accepted_dpa_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(DPA)' AFTER `accepted_eula_version`;
-- +migrate Down
ALTER TABLE `users` CHANGE COLUMN `accepted_eula_version` `accepted_terms_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(EULA)',
DROP COLUMN `accepted_dpa_version`;
ALTER TABLE `users_archive` CHANGE COLUMN `accepted_eula_version` `accepted_terms_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(EULA)',
DROP COLUMN `accepted_dpa_version`;

View File

@ -33,7 +33,7 @@
}
},
"401": {
"description": "認証エラー",
"description": "認証エラー/同意済み利用規約が最新でない場合",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
@ -1219,7 +1219,7 @@
},
"/accounts/delete": {
"post": {
"operationId": "deleteAccount",
"operationId": "deleteAccountAndData",
"summary": "",
"parameters": [],
"requestBody": {
@ -1262,6 +1262,52 @@
"security": [{ "bearer": [] }]
}
},
"/accounts/minimal-access": {
"post": {
"operationId": "getAccountInfoMinimalAccess",
"summary": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetAccountInfoMinimalAccessRequest"
}
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetAccountInfoMinimalAccessResponse"
}
}
}
},
"400": {
"description": "対象のユーザーIDが存在しない場合",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["accounts"]
}
},
"/users/confirm": {
"post": {
"operationId": "confirmUser",
@ -1728,6 +1774,53 @@
"security": [{ "bearer": [] }]
}
},
"/users/accepted-version": {
"post": {
"operationId": "updateAcceptedVersion",
"summary": "",
"description": "利用規約同意バージョンを更新",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateAcceptedVersionRequest"
}
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateAcceptedVersionResponse"
}
}
}
},
"400": {
"description": "パラメータ不正/対象のユーザidが存在しない場合",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["users"]
}
},
"/files/audio/upload-finished": {
"post": {
"operationId": "uploadFinished",
@ -2976,6 +3069,114 @@
"security": [{ "bearer": [] }]
}
},
"/workflows/{workflowId}": {
"post": {
"operationId": "updateWorkflow",
"summary": "",
"description": "アカウント内のワークフローを編集します",
"parameters": [
{
"name": "workflowId",
"required": true,
"in": "path",
"description": "ワークフローの内部ID",
"schema": { "type": "number" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/UpdateWorkflowRequest" }
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateWorkflowResponse"
}
}
}
},
"400": {
"description": "パラメータ不正エラー",
"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": ["workflows"],
"security": [{ "bearer": [] }]
}
},
"/workflows/{workflowId}/delete": {
"post": {
"operationId": "deleteWorkflow",
"summary": "",
"description": "アカウント内のワークフローを削除します",
"parameters": [
{
"name": "workflowId",
"required": true,
"in": "path",
"description": "ワークフローの内部ID",
"schema": { "type": "number" }
}
],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteWorkflowResponse"
}
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["workflows"],
"security": [{ "bearer": [] }]
}
},
"/notification/register": {
"post": {
"operationId": "register",
@ -3026,6 +3227,34 @@
"tags": ["notification"],
"security": [{ "bearer": [] }]
}
},
"/terms": {
"post": {
"operationId": "getTermsInfo",
"summary": "",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetTermsInfoResponse"
}
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["terms"]
}
}
},
"info": {
@ -3087,9 +3316,13 @@
"adminName": { "type": "string" },
"adminMail": { "type": "string" },
"adminPassword": { "type": "string" },
"acceptedTermsVersion": {
"acceptedEulaVersion": {
"type": "string",
"description": "同意済み利用規約のバージョン"
"description": "同意済み利用規約のバージョン(EULA)"
},
"acceptedDpaVersion": {
"type": "string",
"description": "同意済み利用規約のバージョン(DPA)"
},
"token": { "type": "string", "description": "reCAPTCHA Token" }
},
@ -3099,7 +3332,8 @@
"adminName",
"adminMail",
"adminPassword",
"acceptedTermsVersion",
"acceptedEulaVersion",
"acceptedDpaVersion",
"token"
]
},
@ -3597,6 +3831,18 @@
},
"required": ["accountId"]
},
"GetAccountInfoMinimalAccessRequest": {
"type": "object",
"properties": {
"idToken": { "type": "string", "description": "idトークン" }
},
"required": ["idToken"]
},
"GetAccountInfoMinimalAccessResponse": {
"type": "object",
"properties": { "tier": { "type": "number", "description": "階層" } },
"required": ["tier"]
},
"ConfirmRequest": {
"type": "object",
"properties": { "token": { "type": "string" } },
@ -3817,6 +4063,22 @@
"required": ["userId"]
},
"DeallocateLicenseResponse": { "type": "object", "properties": {} },
"UpdateAcceptedVersionRequest": {
"type": "object",
"properties": {
"idToken": { "type": "string", "description": "IDトークン" },
"acceptedEULAVersion": {
"type": "string",
"description": "更新バージョンEULA"
},
"acceptedDPAVersion": {
"type": "string",
"description": "更新バージョンDPA"
}
},
"required": ["idToken", "acceptedEULAVersion"]
},
"UpdateAcceptedVersionResponse": { "type": "object", "properties": {} },
"AudioOptionItem": {
"type": "object",
"properties": {
@ -4251,7 +4513,7 @@
"CreateWorkflowsRequest": {
"type": "object",
"properties": {
"authorId": { "type": "number", "description": "Authornの内部ID" },
"authorId": { "type": "number", "description": "Authorの内部ID" },
"worktypeId": { "type": "number", "description": "Worktypeの内部ID" },
"templateId": {
"type": "number",
@ -4267,6 +4529,26 @@
"required": ["authorId", "typists"]
},
"CreateWorkflowsResponse": { "type": "object", "properties": {} },
"UpdateWorkflowRequest": {
"type": "object",
"properties": {
"authorId": { "type": "number", "description": "Authorの内部ID" },
"worktypeId": { "type": "number", "description": "Worktypeの内部ID" },
"templateId": {
"type": "number",
"description": "テンプレートの内部ID"
},
"typists": {
"description": "ルーティング候補のタイピストユーザー/タイピストグループ",
"minItems": 1,
"type": "array",
"items": { "$ref": "#/components/schemas/WorkflowTypist" }
}
},
"required": ["authorId", "typists"]
},
"UpdateWorkflowResponse": { "type": "object", "properties": {} },
"DeleteWorkflowResponse": { "type": "object", "properties": {} },
"RegisterRequest": {
"type": "object",
"properties": {
@ -4278,7 +4560,8 @@
},
"required": ["pns", "handler"]
},
"RegisterResponse": { "type": "object", "properties": {} }
"RegisterResponse": { "type": "object", "properties": {} },
"GetTermsInfoResponse": { "type": "object", "properties": {} }
}
}
}

View File

@ -48,6 +48,8 @@ import { WorkflowsModule } from './features/workflows/workflows.module';
import { WorkflowsController } from './features/workflows/workflows.controller';
import { WorkflowsService } from './features/workflows/workflows.service';
import { validate } from './common/validators/env.validator';
import { WorkflowsRepositoryModule } from './repositories/workflows/workflows.repository.module';
import { TermsModule } from './features/terms/terms.module';
@Module({
imports: [
@ -86,6 +88,7 @@ import { validate } from './common/validators/env.validator';
CheckoutPermissionsRepositoryModule,
UserGroupsRepositoryModule,
TemplateFilesRepositoryModule,
WorkflowsRepositoryModule,
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
@ -106,6 +109,7 @@ import { validate } from './common/validators/env.validator';
AuthGuardsModule,
SortCriteriaRepositoryModule,
WorktypesRepositoryModule,
TermsModule,
],
controllers: [
HealthController,

View File

@ -56,4 +56,7 @@ export const ErrorCodes = [
'E011001', // ワークタイプ重複エラー
'E011002', // ワークタイプ登録上限超過エラー
'E011003', // ワークタイプ不在エラー
'E012001', // テンプレートファイル不在エラー
'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
'E013002', // ワークフロー不在エラー
] as const;

View File

@ -45,4 +45,7 @@ export const errors: Errors = {
E011001: 'This WorkTypeID already used Error',
E011002: 'WorkTypeID create limit exceeded Error',
E011003: 'WorkTypeID not found Error',
E012001: 'Template file not found Error',
E013001: 'AuthorId and WorktypeId pair already exists Error',
E013002: 'Workflow not found Error',
};

View File

@ -29,6 +29,7 @@ export const overrideAdB2cService = <TService>(
username: string,
) => Promise<{ sub: string } | ConflictError>;
deleteUser?: (externalId: string, context: Context) => Promise<void>;
deleteUsers?: (externalIds: string[], context: Context) => Promise<void>;
getUsers?: (
context: Context,
externalIds: string[],
@ -49,6 +50,12 @@ export const overrideAdB2cService = <TService>(
writable: true,
});
}
if (overrides.deleteUsers) {
Object.defineProperty(obj, obj.deleteUsers.name, {
value: overrides.deleteUsers,
writable: true,
});
}
if (overrides.getUsers) {
Object.defineProperty(obj, obj.getUsers.name, {
value: overrides.getUsers,
@ -229,9 +236,11 @@ export const overrideAccountsRepositoryService = <TService>(
tier: number,
adminExternalUserId: string,
adminUserRole: string,
adminUserAcceptedTermsVersion: string,
adminUserAcceptedEulaVersion: string,
adminUserAcceptedDpaVersion: string,
) => Promise<{ newAccount: Account; adminUser: User }>;
deleteAccount?: (accountId: number, userId: number) => Promise<void>;
deleteAccountAndInsertArchives?: (accountId: number) => Promise<User[]>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -248,4 +257,10 @@ export const overrideAccountsRepositoryService = <TService>(
writable: true,
});
}
if (overrides.deleteAccountAndInsertArchives) {
Object.defineProperty(obj, obj.deleteAccountAndInsertArchives.name, {
value: overrides.deleteAccountAndInsertArchives,
writable: true,
});
}
};

View File

@ -1,6 +1,6 @@
import { v4 as uuidv4 } from 'uuid';
import { DataSource } from 'typeorm';
import { User } from '../../repositories/users/entity/user.entity';
import { User, UserArchive } from '../../repositories/users/entity/user.entity';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
@ -180,7 +180,8 @@ export const makeTestAccount = async (
account_id: accountId,
role: d?.role ?? 'admin none',
author_id: d?.author_id ?? undefined,
accepted_terms_version: d?.accepted_terms_version ?? '1.0',
accepted_eula_version: d?.accepted_eula_version ?? '1.0',
accepted_dpa_version: d?.accepted_dpa_version ?? '1.0',
email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true,
license_alert: d?.license_alert ?? true,
@ -282,7 +283,8 @@ export const makeTestUser = async (
external_id: d?.external_id ?? uuidv4(),
role: d?.role ?? `${ADMIN_ROLES.STANDARD} ${USER_ROLES.NONE}`,
author_id: d?.author_id,
accepted_terms_version: d?.accepted_terms_version,
accepted_eula_version: d?.accepted_eula_version ?? '1.0',
accepted_dpa_version: d?.accepted_dpa_version ?? '1.0',
email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true,
license_alert: d?.license_alert ?? true,
@ -368,3 +370,14 @@ export const getUser = async (
export const getUsers = async (dataSource: DataSource): Promise<User[]> => {
return await dataSource.getRepository(User).find();
};
/**
* ユーティリティ: ユーザー退避テーブルの内容を取得する
* @param dataSource
* @returns 退
*/
export const getUserArchive = async (
dataSource: DataSource,
): Promise<UserArchive[]> => {
return await dataSource.getRepository(UserArchive).find();
};

View File

@ -20,18 +20,10 @@ export class EnvValidator {
@IsNumber()
DB_PORT: number;
@IsNotEmpty()
@IsNumber()
DB_EXTERNAL_PORT: number;
@IsNotEmpty()
@IsString()
DB_NAME: string;
@IsNotEmpty()
@IsString()
DB_ROOT_PASS: string;
@IsNotEmpty()
@IsString()
DB_USERNAME: string;
@ -40,21 +32,22 @@ export class EnvValidator {
@IsString()
DB_PASSWORD: string;
// .env.local
@IsOptional()
@IsString()
STAGE: string;
@IsOptional()
@IsString()
NO_COLOR: string;
@IsNotEmpty()
@IsNumber()
ACCESS_TOKEN_LIFETIME_WEB: number;
@IsOptional()
@IsString()
CORS: string;
@IsNotEmpty()
@IsOptional()
@IsNumber()
REFRESH_TOKEN_LIFETIME_WEB: number;
@IsNotEmpty()
@IsNumber()
REFRESH_TOKEN_LIFETIME_DEFAULT: number;
PORT: number;
@IsNotEmpty()
@IsString()
@ -64,43 +57,6 @@ export class EnvValidator {
@IsString()
SIGNIN_FLOW_NAME: string;
@IsNotEmpty()
@IsNumber()
EMAIL_CONFIRM_LIFETIME: number;
@IsNotEmpty()
@IsString()
APP_DOMAIN: string;
@IsNotEmpty()
@IsNumber()
STORAGE_TOKEN_EXPIRE_TIME: number;
// .env.local
@IsOptional()
@IsString()
STAGE: string;
@IsOptional()
@IsString()
CORS: string;
@IsNotEmpty()
@IsNumber()
PORT: number;
@IsNotEmpty()
@IsString()
AZURE_TENANT_ID: string;
@IsNotEmpty()
@IsString()
AZURE_CLIENT_ID: string;
@IsNotEmpty()
@IsString()
AZURE_CLIENT_SECRET: string;
@IsNotEmpty()
@IsString()
ADB2C_TENANT_ID: string;
@ -113,14 +69,10 @@ export class EnvValidator {
@IsString()
ADB2C_CLIENT_SECRET: string;
@IsNotEmpty()
@IsOptional()
@IsString()
ADB2C_ORIGIN: string;
@IsNotEmpty()
@IsString()
KEY_VAULT_NAME: string;
@IsNotEmpty()
@IsString()
JWT_PRIVATE_KEY: string;
@ -145,6 +97,14 @@ export class EnvValidator {
@IsString()
NOTIFICATION_HUB_CONNECT_STRING: string;
@IsNotEmpty()
@IsString()
APP_DOMAIN: string;
@IsNotEmpty()
@IsNumber()
STORAGE_TOKEN_EXPIRE_TIME: number;
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_NAME_US: string;
@ -180,6 +140,22 @@ export class EnvValidator {
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_ENDPOINT_EU: string;
@IsNotEmpty()
@IsNumber()
ACCESS_TOKEN_LIFETIME_WEB: number;
@IsNotEmpty()
@IsNumber()
REFRESH_TOKEN_LIFETIME_WEB: number;
@IsNotEmpty()
@IsNumber()
REFRESH_TOKEN_LIFETIME_DEFAULT: number;
@IsNotEmpty()
@IsNumber()
EMAIL_CONFIRM_LIFETIME: number;
}
export function validate(config: Record<string, unknown>) {

View File

@ -246,3 +246,9 @@ export const OPTION_ITEM_VALUE_TYPE = {
export const ADB2C_SIGN_IN_TYPE = {
EAMILADDRESS: 'emailAddress',
} as const;
/**
* MANUAL_RECOVERY_REQUIRED
* @const {string}
*/
export const MANUAL_RECOVERY_REQUIRED = '[MANUAL_RECOVERY_REQUIRED]';

View File

@ -63,6 +63,8 @@ import {
DeleteAccountRequest,
DeleteAccountResponse,
GetAuthorsResponse,
GetAccountInfoMinimalAccessRequest,
GetAccountInfoMinimalAccessResponse,
} from './types/types';
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
import { AuthGuard } from '../../common/guards/auth/authguards';
@ -107,7 +109,8 @@ export class AccountsController {
adminMail,
adminPassword,
adminName,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
} = body;
const role = USER_ROLES.NONE;
@ -122,7 +125,8 @@ export class AccountsController {
adminPassword,
adminName,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
return {};
@ -231,9 +235,9 @@ export class AccountsController {
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(userId);
console.log(context.trackingId);
const authors = await this.accountService.getAuthors(context, userId);
return { authors: [] };
return { authors };
}
@ApiResponse({
@ -1071,7 +1075,7 @@ export class AccountsController {
description: 'DBアクセスに失敗しログインできる状態で処理が終了した場合',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'deleteAccount' })
@ApiOperation({ operationId: 'deleteAccountAndData' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
@ -1079,7 +1083,7 @@ export class AccountsController {
roles: [ADMIN_ROLES.ADMIN],
}),
)
async deleteAccount(
async deleteAccountAndData(
@Req() req: Request,
@Body() body: DeleteAccountRequest,
): Promise<DeleteAccountResponse> {
@ -1088,12 +1092,35 @@ export class AccountsController {
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
/* TODO
await this.accountService.deleteAccount(
context,
accountId
);
*/
await this.accountService.deleteAccountAndData(context, userId, accountId);
return;
}
@Post('/minimal-access')
@ApiResponse({
status: HttpStatus.OK,
type: GetAccountInfoMinimalAccessResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '対象のユーザーIDが存在しない場合',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'getAccountInfoMinimalAccess' })
async getAccountInfoMinimalAccess(
@Body() body: GetAccountInfoMinimalAccessRequest,
): Promise<GetAccountInfoMinimalAccessResponse> {
const context = makeContext(uuidv4());
// TODO 仮実装。API実装タスクで本実装する。
// const idToken = await this.authService.getVerifiedIdToken(body.idToken);
// await this.accountService.getAccountInfoMinimalAccess(context, idToken);
return;
}
}

View File

@ -34,6 +34,8 @@ import {
getUsers,
makeTestUser,
makeHierarchicalAccounts,
getUser,
getUserArchive,
} from '../../common/test/utility';
import { AccountsService } from './accounts.service';
import { Context, makeContext } from '../../common/log';
@ -58,7 +60,10 @@ import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import {
createLicenseAllocationHistory,
createOrder,
getLicenseArchive,
getLicenseAllocationHistoryArchive,
selectLicense,
selectOrderLicense,
} from '../licenses/test/utility';
@ -66,6 +71,7 @@ import { WorktypesRepositoryService } from '../../repositories/worktypes/worktyp
import { AdB2cUser } from '../../gateways/adb2c/types/types';
import { Worktype } from '../../repositories/worktypes/entity/worktype.entity';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
describe('createAccount', () => {
let source: DataSource = null;
@ -97,7 +103,8 @@ describe('createAccount', () => {
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
const acceptedEulaVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
@ -128,7 +135,8 @@ describe('createAccount', () => {
password,
username,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
// 作成したアカウントのIDが返ってくるか確認
expect(accountId).toBe(1);
@ -144,7 +152,8 @@ describe('createAccount', () => {
expect(account.tier).toBe(TIERS.TIER5);
expect(account.primary_admin_user_id).toBe(user.id);
expect(account.secondary_admin_user_id).toBe(null);
expect(user.accepted_terms_version).toBe(acceptedTermsVersion);
expect(user.accepted_eula_version).toBe(acceptedEulaVersion);
expect(user.accepted_dpa_version).toBe(acceptedDpaVersion);
expect(user.account_id).toBe(accountId);
expect(user.role).toBe(role);
});
@ -175,7 +184,8 @@ describe('createAccount', () => {
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'admin none';
const acceptedTermsVersion = '1.0.0';
const acceptedEulaVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
@ -195,7 +205,8 @@ describe('createAccount', () => {
password,
username,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
} catch (e) {
if (e instanceof HttpException) {
@ -240,7 +251,8 @@ describe('createAccount', () => {
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'admin none';
const acceptedTermsVersion = '1.0.0';
const acceptedEulaVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
@ -261,7 +273,8 @@ describe('createAccount', () => {
password,
username,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
} catch (e) {
if (e instanceof HttpException) {
@ -290,7 +303,8 @@ describe('createAccount', () => {
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
const acceptedEulaVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
@ -316,7 +330,8 @@ describe('createAccount', () => {
password,
username,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
} catch (e) {
if (e instanceof HttpException) {
@ -352,7 +367,8 @@ describe('createAccount', () => {
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
const acceptedEulaVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
@ -378,7 +394,8 @@ describe('createAccount', () => {
password,
username,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
} catch (e) {
if (e instanceof HttpException) {
@ -416,7 +433,8 @@ describe('createAccount', () => {
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
const acceptedEulaVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
@ -443,7 +461,8 @@ describe('createAccount', () => {
password,
username,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
} catch (e) {
if (e instanceof HttpException) {
@ -480,7 +499,8 @@ describe('createAccount', () => {
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
const acceptedEulaVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
@ -510,7 +530,8 @@ describe('createAccount', () => {
password,
username,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
} catch (e) {
if (e instanceof HttpException) {
@ -549,7 +570,8 @@ describe('createAccount', () => {
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
const acceptedEulaVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async (
@ -596,7 +618,8 @@ describe('createAccount', () => {
password,
username,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
} catch (e) {
if (e instanceof HttpException) {
@ -641,7 +664,8 @@ describe('createAccount', () => {
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
const acceptedEulaVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
@ -685,7 +709,8 @@ describe('createAccount', () => {
password,
username,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
} catch (e) {
if (e instanceof HttpException) {
@ -5204,3 +5229,369 @@ describe('getAccountInfo', () => {
}
});
});
describe('getAuthors', () => {
let source: DataSource = 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 () => {
await source.destroy();
source = null;
});
it('アカウント内のAuthorユーザーの一覧を取得できる', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: userId1 } = await makeTestUser(source, {
account_id: account.id,
role: USER_ROLES.AUTHOR,
author_id: 'AUTHOR_ID_1',
});
const { id: userId2 } = await makeTestUser(source, {
account_id: account.id,
role: USER_ROLES.AUTHOR,
author_id: 'AUTHOR_ID_2',
});
const { id: userId3 } = await makeTestUser(source, {
account_id: account.id,
role: USER_ROLES.TYPIST,
});
// 作成したデータを確認
{
const users = await getUsers(source);
expect(users.length).toBe(4);
expect(users[1].id).toBe(userId1);
expect(users[2].id).toBe(userId2);
expect(users[3].id).toBe(userId3);
}
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const authors = await service.getAuthors(context, admin.external_id);
//実行結果を確認
{
expect(authors.length).toBe(2);
expect(authors[0].id).toBe(userId1);
expect(authors[0].authorId).toBe('AUTHOR_ID_1');
expect(authors[1].id).toBe(userId2);
expect(authors[1].authorId).toBe('AUTHOR_ID_2');
}
});
it('アカウント内のAuthorユーザーの一覧を取得できる0件', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
// 作成したデータを確認
{
const users = await getUsers(source);
expect(users.length).toBe(1);
}
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const authors = await service.getAuthors(context, admin.external_id);
//実行結果を確認
{
expect(authors.length).toBe(0);
}
});
it('DBアクセスに失敗した場合、500エラーとなる', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
//DBアクセスに失敗するようにする
const usersService = module.get<UsersRepositoryService>(
UsersRepositoryService,
);
usersService.findAuthorUsers = jest.fn().mockRejectedValue('DB failed');
//実行結果を確認
try {
await service.getAuthors(context, admin.external_id);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
});
describe('deleteAccountAndData', () => {
let source: DataSource = 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 () => {
await source.destroy();
source = null;
});
it('アカウント情報が削除されること', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
// 第五階層のアカウント作成
const tier4Accounts = await makeHierarchicalAccounts(source);
const { account: account1, admin: admin1 } = await makeTestAccount(source, {
parent_account_id: tier4Accounts.tier4Accounts[0].account.id,
});
const account = account1;
const admin = admin1;
const context = makeContext(admin.external_id);
// 第五階層のアカウント作成
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: account.id,
tier: 5,
});
// ユーザの作成
const user = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
});
// ライセンス作成
await createLicense(
source,
1,
new Date(),
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
user.id,
null,
null,
);
await createLicenseAllocationHistory(
source,
1,
user.id,
1,
tier5Accounts.account.id,
'NONE',
);
// ADB2Cユーザーの削除成功
overrideAdB2cService(service, {
deleteUsers: jest.fn(),
});
// blobstorageコンテナの削除成功
overrideBlobstorageService(service, {
deleteContainer: jest.fn(),
});
// アカウント情報の削除
await service.deleteAccountAndData(
context,
tier5Accounts.admin.external_id,
tier5Accounts.account.id,
);
// DB内が想定通りになっているか確認
const accountRecord = await getAccount(source, tier5Accounts.account.id);
expect(accountRecord).toBe(null);
const userRecord = await getUser(source, user.id);
expect(userRecord).toBe(null);
const UserArchive = await getUserArchive(source);
expect(UserArchive.length).toBe(2);
const LicenseArchive = await getLicenseArchive(source);
expect(LicenseArchive.length).toBe(1);
const LicenseAllocationHistoryArchive =
await getLicenseAllocationHistoryArchive(source);
expect(LicenseAllocationHistoryArchive.length).toBe(1);
});
it('アカウントの削除に失敗した場合はエラーを返す', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation();
// 第五階層のアカウント作成
const tier4Accounts = await makeHierarchicalAccounts(source);
const { account: account1, admin: admin1 } = await makeTestAccount(source, {
parent_account_id: tier4Accounts.tier4Accounts[0].account.id,
});
const account = account1;
const admin = admin1;
const context = makeContext(admin.external_id);
// 第五階層のアカウント作成
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: account.id,
tier: 5,
});
// ユーザの作成
const user = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
});
// アカウント情報の削除失敗
overrideAccountsRepositoryService(service, {
deleteAccountAndInsertArchives: jest.fn().mockRejectedValue(new Error()),
});
// ADB2Cユーザーの削除成功
overrideAdB2cService(service, {
deleteUsers: jest.fn(),
});
// blobstorageコンテナの削除成功
overrideBlobstorageService(service, {
deleteContainer: jest.fn(),
});
// アカウント情報の削除に失敗することを確認
await expect(
service.deleteAccountAndData(
context,
tier5Accounts.admin.external_id,
tier5Accounts.account.id,
),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
// loggerSpyがスパイしているlogger.logメソッドが出力したログを確認目視確認用
const logs = loggerSpy.mock.calls.map((call) => call[0]);
console.log(logs);
// DB内が削除されていないことを確認
const accountRecord = await getAccount(source, tier5Accounts.account.id);
expect(accountRecord.id).not.toBeNull();
const userRecord = await getUser(source, user.id);
expect(userRecord.id).not.toBeNull();
});
it('ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation();
// 第五階層のアカウント作成
const tier4Accounts = await makeHierarchicalAccounts(source);
const { account: account1, admin: admin1 } = await makeTestAccount(source, {
parent_account_id: tier4Accounts.tier4Accounts[0].account.id,
});
const account = account1;
const admin = admin1;
const context = makeContext(admin.external_id);
// 第五階層のアカウント作成
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: account.id,
tier: 5,
});
// ユーザの作成
const user = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
});
// ADB2Cユーザーの削除失敗
overrideAdB2cService(service, {
deleteUsers: jest.fn().mockRejectedValue(new Error()),
});
// blobstorageコンテナの削除成功
overrideBlobstorageService(service, {
deleteContainer: jest.fn(),
});
// 処理自体は成功することを確認
expect(
await service.deleteAccountAndData(
context,
tier5Accounts.admin.external_id,
tier5Accounts.account.id,
),
).toEqual(undefined);
// loggerSpyがスパイしているlogger.logメソッドが出力したログを確認目視確認用
const logs = loggerSpy.mock.calls.map((call) => call[0]);
console.log(logs);
// DB内が想定通りになっているか確認
const accountRecord = await getAccount(source, tier5Accounts.account.id);
expect(accountRecord).toBe(null);
const userRecord = await getUser(source, user.id);
expect(userRecord).toBe(null);
});
it('blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation();
// 第五階層のアカウント作成
const tier4Accounts = await makeHierarchicalAccounts(source);
const { account: account1, admin: admin1 } = await makeTestAccount(source, {
parent_account_id: tier4Accounts.tier4Accounts[0].account.id,
});
const account = account1;
const admin = admin1;
const context = makeContext(admin.external_id);
// 第五階層のアカウント作成
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: account.id,
tier: 5,
});
// ユーザの作成
const user = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
});
// ADB2Cユーザーの削除成功
overrideAdB2cService(service, {
deleteUsers: jest.fn(),
});
// blobstorageコンテナの削除失敗
overrideBlobstorageService(service, {
deleteContainer: jest.fn().mockRejectedValue(new Error()),
});
// 処理自体は成功することを確認
expect(
await service.deleteAccountAndData(
context,
tier5Accounts.admin.external_id,
tier5Accounts.account.id,
),
).toEqual(undefined);
// loggerSpyがスパイしているlogger.logメソッドが出力したログを確認目視確認用
const logs = loggerSpy.mock.calls.map((call) => call[0]);
console.log(logs);
// DB内が想定通りになっているか確認
const accountRecord = await getAccount(source, tier5Accounts.account.id);
expect(accountRecord).toBe(null);
const userRecord = await getUser(source, user.id);
expect(userRecord).toBe(null);
});
});

View File

@ -15,6 +15,7 @@ import {
USER_ROLES,
ADB2C_SIGN_IN_TYPE,
OPTION_ITEM_VALUE_TYPE,
MANUAL_RECOVERY_REQUIRED,
} from '../../constants';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
@ -31,6 +32,7 @@ import {
GetOptionItemsResponse,
GetPartnersResponse,
PostWorktypeOptionItem,
Author,
} from './types/types';
import {
DateWithZeroTime,
@ -157,14 +159,16 @@ export class AccountsService {
password: string,
username: string,
role: string,
acceptedTermsVersion: string,
acceptedEulaVersion: string,
acceptedDpaVersion: string,
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.createAccount.name} | params: { ` +
`country: ${country}, ` +
`dealerAccountId: ${dealerAccountId}, ` +
`role: ${role}, ` +
`acceptedTermsVersion: ${acceptedTermsVersion} };`,
`acceptedEulaVersion: ${acceptedEulaVersion} }, ` +
`acceptedDpaVersion: ${acceptedDpaVersion} };`,
);
try {
let externalUser: { sub: string } | ConflictError;
@ -207,7 +211,8 @@ export class AccountsService {
TIERS.TIER5,
externalUser.sub,
role,
acceptedTermsVersion,
acceptedEulaVersion,
acceptedDpaVersion,
);
account = newAccount;
user = adminUser;
@ -319,7 +324,7 @@ export class AccountsService {
} catch (error) {
this.logger.error(`error=${error}`);
this.logger.error(
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
);
}
}
@ -338,7 +343,7 @@ export class AccountsService {
} catch (error) {
this.logger.error(`error=${error}`);
this.logger.error(
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete account: ${accountId}, user: ${userId}`,
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete account: ${accountId}, user: ${userId}`,
);
}
}
@ -361,7 +366,7 @@ export class AccountsService {
);
} catch (error) {
this.logger.error(
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete container: ${accountId}, country: ${country}`,
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete container: ${accountId}, country: ${country}`,
);
}
}
@ -553,6 +558,64 @@ export class AccountsService {
}
}
/**
* Authorを取得する
* @param context
* @param externalId
* @returns authors
*/
async getAuthors(context: Context, externalId: string): Promise<Author[]> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.getAuthors.name} | params: { externalId: ${externalId} };`,
);
try {
const { account } = await this.usersRepository.findUserByExternalId(
externalId,
);
if (!account) {
throw new AccountNotFoundError(
`account not found. externalId: ${externalId}`,
);
}
const authorUsers = await this.usersRepository.findAuthorUsers(
account.id,
);
const authors = authorUsers.map((x) => {
return {
id: x.id,
authorId: x.author_id,
};
});
return authors;
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
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.getAuthors.name}`);
}
}
/**
*
* @param companyName
@ -638,6 +701,7 @@ export class AccountsService {
externalUser.sub,
USER_ROLES.NONE,
null,
null,
);
account = newAccount;
user = adminUser;
@ -1684,4 +1748,99 @@ export class AccountsService {
);
}
}
/**
*
* @param context
* @param externalId
* @param accountId // 削除対象のアカウントID
*/
async deleteAccountAndData(
context: Context,
externalId: string,
accountId: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.deleteAccountAndData.name} | params: { ` +
`externalId: ${externalId}, ` +
`accountId: ${accountId}, };`,
);
let country: string;
let dbUsers: User[];
try {
// パラメータとトークンから取得したアカウントIDの突き合わせ
const { account_id: myAccountId } =
await this.usersRepository.findUserByExternalId(externalId);
if (myAccountId !== accountId) {
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
);
}
// アカウント削除前に必要な情報を退避する
const targetAccount = await this.accountRepository.findAccountById(
accountId,
);
// 削除対象アカウントを削除する
dbUsers = await this.accountRepository.deleteAccountAndInsertArchives(
accountId,
);
this.logger.log(`[${context.trackingId}] delete account: ${accountId}`);
country = targetAccount.country;
} catch (e) {
// アカウントの削除に失敗した場合はエラーを返す
this.logger.log(`[${context.trackingId}] ${e}`);
this.logger.log(
`[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// 削除対象アカウント内のADB2Cユーザーをすべて削除する
await this.adB2cService.deleteUsers(
dbUsers.map((x) => x.external_id),
context,
);
this.logger.log(
`[${
context.trackingId
}] delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
(x) => x.external_id,
)}`,
);
} catch (e) {
// ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行
this.logger.log(`[${context.trackingId}] ${e}`);
this.logger.log(
`${MANUAL_RECOVERY_REQUIRED} [${
context.trackingId
}] Failed to delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
(x) => x.external_id,
)}`,
);
}
try {
// blobstorageコンテナを削除する
await this.deleteBlobContainer(accountId, country, context);
this.logger.log(
`[${context.trackingId}] delete blob container: ${accountId}-${country}`,
);
} catch (e) {
// blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了
this.logger.log(`[${context.trackingId}] ${e}`);
this.logger.log(
`${MANUAL_RECOVERY_REQUIRED}[${context.trackingId}] Failed to delete blob container: ${accountId}, country: ${country}`,
);
}
this.logger.log(
`[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`,
);
}
}

View File

@ -345,7 +345,8 @@ export const makeDefaultAccountsRepositoryMockValue =
user.account_id = 1234567890123456;
user.role = 'none admin';
user.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
user.accepted_terms_version = '1.0';
user.accepted_eula_version = '1.0';
user.accepted_dpa_version = '1.0';
user.email_verified = true;
user.auto_renew = false;
user.license_alert = false;
@ -374,7 +375,8 @@ export const makeDefaultUsersRepositoryMockValue =
user.account_id = 1234567890123456;
user.role = 'none admin';
user.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
user.accepted_terms_version = '1.0';
user.accepted_eula_version = '1.0';
user.accepted_dpa_version = '1.0';
user.email_verified = true;
user.auto_renew = false;
user.license_alert = false;
@ -422,7 +424,8 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user.account_id = 1234567890123456;
user.role = 'none admin';
user.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
user.accepted_terms_version = '1.0';
user.accepted_eula_version = '1.0';
user.accepted_dpa_version = '1.0';
user.email_verified = true;
user.auto_renew = false;
user.license_alert = false;

View File

@ -43,8 +43,10 @@ export class CreateAccountRequest {
@ApiProperty()
@IsAdminPasswordvalid()
adminPassword: string;
@ApiProperty({ description: '同意済み利用規約のバージョン' })
acceptedTermsVersion: string;
@ApiProperty({ description: '同意済み利用規約のバージョン(EULA)' })
acceptedEulaVersion: string;
@ApiProperty({ description: '同意済み利用規約のバージョン(DPA)' })
acceptedDpaVersion: string;
@ApiProperty({ description: 'reCAPTCHA Token' })
token: string;
}
@ -577,3 +579,13 @@ export class DeleteAccountRequest {
}
export class DeleteAccountResponse {}
export class GetAccountInfoMinimalAccessRequest {
@ApiProperty({ description: 'idトークン' })
idToken: string;
}
export class GetAccountInfoMinimalAccessResponse {
@ApiProperty({ description: '階層' })
tier: number;
}

View File

@ -41,7 +41,7 @@ export class AuthController {
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
description: '認証エラー/同意済み利用規約が最新でない場合',
type: ErrorResponse,
})
@ApiResponse({

View File

@ -127,7 +127,8 @@ export const makeDefaultUsersRepositoryMockValue =
account_id: 1234567890123456,
role: 'none',
author_id: '',
accepted_terms_version: '1.0',
accepted_eula_version: '1.0',
accepted_dpa_version: '1.0',
email_verified: true,
deleted_at: null,
created_by: 'test',

View File

@ -116,7 +116,8 @@ export const makeDefaultUsersRepositoryMockValue =
user1.account_id = 1234567890123456;
user1.role = 'none';
user1.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
user1.accepted_terms_version = '1.0';
user1.accepted_eula_version = '1.0';
user1.accepted_dpa_version = '1.0';
user1.email_verified = true;
user1.auto_renew = false;
user1.license_alert = false;

View File

@ -5,6 +5,8 @@ import {
CardLicenseIssue,
LicenseAllocationHistory,
LicenseOrder,
LicenseArchive,
LicenseAllocationHistoryArchive,
} from '../../../repositories/licenses/entity/license.entity';
export const createLicense = async (
@ -189,3 +191,25 @@ export const selectOrderLicense = async (
});
return { orderLicense };
};
/**
* ユーティリティ: ライセンス退避テーブルの内容を取得する
* @param dataSource
* @returns 退
*/
export const getLicenseArchive = async (
dataSource: DataSource,
): Promise<LicenseArchive[]> => {
return await dataSource.getRepository(LicenseArchive).find();
};
/**
* ユーティリティ: ライセンス割り当て履歴退避テーブルの内容を取得する
* @param dataSource
* @returns 退
*/
export const getLicenseAllocationHistoryArchive = async (
dataSource: DataSource,
): Promise<LicenseAllocationHistoryArchive[]> => {
return await dataSource.getRepository(LicenseAllocationHistoryArchive).find();
};

View File

@ -78,7 +78,8 @@ export const makeDefaultUsersRepositoryMockValue =
user.account_id = 123;
user.role = 'none';
user.author_id = undefined;
user.accepted_terms_version = '1.0';
user.accepted_eula_version = '1.0';
user.accepted_dpa_version = '1.0';
user.email_verified = true;
user.auto_renew = false;
user.license_alert = false;

View File

@ -423,7 +423,8 @@ const defaultTasksRepositoryMockValue: {
account_id: 1,
external_id: 'userId',
role: 'typist',
accepted_terms_version: '',
accepted_eula_version: '',
accepted_dpa_version: '',
email_verified: true,
auto_renew: true,
license_alert: true,

View File

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TermsController } from './terms.controller';
import { TermsService } from './terms.service';
describe('TermsController', () => {
let controller: TermsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TermsController],
providers: [TermsService],
}).compile();
controller = module.get<TermsController>(TermsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,40 @@
import { Controller, HttpStatus, Post } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { TermsService } from '../terms/terms.service';
import { ErrorResponse } from '../../common/error/types/types';
import { makeContext } from '../../common/log';
import { v4 as uuidv4 } from 'uuid';
import { GetTermsInfoResponse, TermInfo } from './types/types';
@ApiTags('terms')
@Controller('terms')
export class TermsController {
constructor(
private readonly termsService: TermsService, //private readonly cryptoService: CryptoService,
) {}
@Post()
@ApiResponse({
status: HttpStatus.OK,
type: GetTermsInfoResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'getTermsInfo' })
async getTermsInfo(): Promise<GetTermsInfoResponse> {
const context = makeContext(uuidv4());
// TODO 仮実装。API実装タスクで本実装する。
// const termInfo = await this.termsService.getTermsInfo(context);
const termsInfo = [
{ documentType: 'EULA', version: '1.0' },
{ documentType: 'DPA', version: '1.1' },
] as TermInfo[];
return { termsInfo };
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TermsController } from './terms.controller';
import { TermsService } from './terms.service';
@Module({
controllers: [TermsController],
providers: [TermsService]
})
export class TermsModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TermsService } from './terms.service';
describe('TermsService', () => {
let service: TermsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TermsService],
}).compile();
service = module.get<TermsService>(TermsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class TermsService {}

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
export class GetTermsInfoResponse {
termsInfo: TermInfo[];
}
export class TermInfo {
@ApiProperty({ description: '利用規約種別' })
documentType: string;
@ApiProperty({ description: 'バージョン' })
version: string;
}

View File

@ -356,7 +356,8 @@ export const makeDefaultUsersRepositoryMockValue =
user1.account_id = 1234567890123456;
user1.role = 'none';
user1.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
user1.accepted_terms_version = '1.0';
user1.accepted_eula_version = '1.0';
user1.accepted_dpa_version = '1.0';
user1.email_verified = true;
user1.auto_renew = false;
user1.license_alert = false;
@ -375,7 +376,8 @@ export const makeDefaultUsersRepositoryMockValue =
user2.account_id = 1234567890123456;
user2.role = 'none';
user2.author_id = '551c4077-5b55-a38c-2c55-cd1edd537aa8';
user2.accepted_terms_version = '1.0';
user2.accepted_eula_version = '1.0';
user2.accepted_dpa_version = '1.0';
user2.email_verified = true;
user2.auto_renew = false;
user2.license_alert = false;

View File

@ -255,3 +255,14 @@ export class DeallocateLicenseRequest {
}
export class DeallocateLicenseResponse {}
export class UpdateAcceptedVersionRequest {
@ApiProperty({ description: 'IDトークン' })
idToken: string;
@ApiProperty({ description: '更新バージョンEULA' })
acceptedEULAVersion: string;
@ApiProperty({ description: '更新バージョンDPA', required: false })
acceptedDPAVersion?: string | undefined;
}
export class UpdateAcceptedVersionResponse {}

View File

@ -37,6 +37,8 @@ import {
AllocateLicenseRequest,
DeallocateLicenseResponse,
DeallocateLicenseRequest,
UpdateAcceptedVersionRequest,
UpdateAcceptedVersionResponse,
} from './types/types';
import { UsersService } from './users.service';
import jwt from 'jsonwebtoken';
@ -469,4 +471,35 @@ export class UsersController {
await this.usersService.deallocateLicense(context, body.userId);
return {};
}
@ApiResponse({
status: HttpStatus.OK,
type: UpdateAcceptedVersionResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'パラメータ不正/対象のユーザidが存在しない場合',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'updateAcceptedVersion',
description: '利用規約同意バージョンを更新',
})
@Post('/accepted-version')
async updateAcceptedVersion(
@Body() body: UpdateAcceptedVersionRequest,
): Promise<UpdateAcceptedVersionResponse> {
const context = makeContext(uuidv4());
// TODO 仮実装。API実装タスクで本実装する。
// const idToken = await this.authService.getVerifiedIdToken(body.idToken);
// await this.usersService.updateAcceptedVersion(context, idToken);
return {};
}
}

View File

@ -188,7 +188,8 @@ describe('UsersService.confirmUserAndInitPassword', () => {
external_id: 'TEST9999',
account_id: 1,
role: 'None',
accepted_terms_version: 'string',
accepted_eula_version: 'string',
accepted_dpa_version: 'string',
email_verified: false,
created_by: 'string;',
created_at: new Date(),
@ -232,7 +233,8 @@ describe('UsersService.confirmUserAndInitPassword', () => {
external_id: 'TEST9999',
account_id: 1,
role: 'None',
accepted_terms_version: 'string',
accepted_eula_version: 'string',
accepted_dpa_version: 'string',
email_verified: false,
created_by: 'string;',
created_at: new Date(),
@ -272,7 +274,8 @@ describe('UsersService.confirmUserAndInitPassword', () => {
external_id: 'TEST9999',
account_id: 1,
role: 'None',
accepted_terms_version: 'string',
accepted_eula_version: 'string',
accepted_dpa_version: 'string',
email_verified: true,
created_by: 'string;',
created_at: new Date(),
@ -316,7 +319,8 @@ describe('UsersService.confirmUserAndInitPassword', () => {
external_id: 'TEST9999',
account_id: 1,
role: 'None',
accepted_terms_version: 'string',
accepted_eula_version: 'string',
accepted_dpa_version: 'string',
email_verified: false,
created_by: 'string;',
created_at: new Date(),

View File

@ -35,6 +35,7 @@ import {
import {
ADB2C_SIGN_IN_TYPE,
LICENSE_EXPIRATION_THRESHOLD_DAYS,
MANUAL_RECOVERY_REQUIRED,
USER_LICENSE_STATUS,
USER_ROLES,
} from '../../constants';
@ -300,7 +301,7 @@ export class UsersService {
} catch (error) {
this.logger.error(`error=${error}`);
this.logger.error(
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
);
}
}
@ -313,7 +314,7 @@ export class UsersService {
} catch (error) {
this.logger.error(`error=${error}`);
this.logger.error(
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete user: ${userId}`,
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete user: ${userId}`,
);
}
}

View File

@ -0,0 +1,94 @@
import { DataSource } from 'typeorm';
import { Workflow } from '../../../repositories/workflows/entity/workflow.entity';
import { WorkflowTypist } from '../../../repositories/workflows/entity/workflow_typists.entity';
// Workflowを作成する
export const createWorkflow = async (
datasource: DataSource,
accountId: number,
authorId: number,
worktypeId?: number | undefined,
templateId?: number | undefined,
): Promise<Workflow> => {
const { identifiers } = await datasource.getRepository(Workflow).insert({
account_id: accountId,
author_id: authorId,
worktype_id: worktypeId ?? undefined,
template_id: templateId ?? undefined,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const workflow = identifiers.pop() as Workflow;
return workflow;
};
// Workflow一覧を取得する
export const getWorkflows = async (
datasource: DataSource,
accountId: number,
): Promise<Workflow[]> => {
return await datasource.getRepository(Workflow).find({
where: {
account_id: accountId,
},
});
};
// Workflowを取得する
export const getWorkflow = async (
datasource: DataSource,
accountId: number,
id: number,
): Promise<Workflow> => {
return await datasource.getRepository(Workflow).findOne({
where: {
account_id: accountId,
id: id,
},
});
};
// Workflowを作成する
export const createWorkflowTypist = async (
datasource: DataSource,
workflowId: number,
typistUserId?: number | undefined,
typistGroupId?: number | undefined,
): Promise<Workflow> => {
const { identifiers } = await datasource
.getRepository(WorkflowTypist)
.insert({
workflow_id: workflowId,
typist_id: typistUserId ?? undefined,
typist_group_id: typistGroupId ?? undefined,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const workflow = identifiers.pop() as Workflow;
return workflow;
};
// WorkflowTypist一覧を取得する
export const getWorkflowTypists = async (
datasource: DataSource,
workflowId: number,
): Promise<WorkflowTypist[]> => {
return await datasource.getRepository(WorkflowTypist).find({
where: {
workflow_id: workflowId,
},
});
};
// WorkflowTypist一覧全件を取得する
export const getAllWorkflowTypists = async (
datasource: DataSource,
): Promise<WorkflowTypist[]> => {
return await datasource.getRepository(WorkflowTypist).find();
};

View File

@ -50,7 +50,7 @@ export class WorkflowTypist {
}
export class CreateWorkflowsRequest {
@ApiProperty({ description: 'Authornの内部ID' })
@ApiProperty({ description: 'Authorの内部ID' })
@Type(() => Number)
@IsInt()
@Min(0)
@ -79,3 +79,52 @@ export class CreateWorkflowsRequest {
}
export class CreateWorkflowsResponse {}
export class UpdateWorkflowRequestParam {
@ApiProperty({ description: 'ワークフローの内部ID' })
@Type(() => Number)
@IsInt()
@Min(0)
workflowId: number;
}
export class UpdateWorkflowRequest {
@ApiProperty({ description: 'Authorの内部ID' })
@Type(() => Number)
@IsInt()
@Min(0)
authorId: number;
@ApiProperty({ description: 'Worktypeの内部ID', required: false })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
worktypeId?: number | undefined;
@ApiProperty({ description: 'テンプレートの内部ID', required: false })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
templateId?: number | undefined;
@ApiProperty({
description: 'ルーティング候補のタイピストユーザー/タイピストグループ',
type: [WorkflowTypist],
minItems: 1,
})
@Type(() => WorkflowTypist)
@IsArray()
@ArrayMinSize(1)
typists: WorkflowTypist[];
}
export class UpdateWorkflowResponse {}
export class DeleteWorkflowRequestParam {
@ApiProperty({ description: 'ワークフローの内部ID' })
@Type(() => Number)
@IsInt()
@Min(0)
workflowId: number;
}
export class DeleteWorkflowResponse {}

View File

@ -3,6 +3,7 @@ import {
Controller,
Get,
HttpStatus,
Param,
Post,
Req,
UseGuards,
@ -20,6 +21,11 @@ import {
GetWorkflowsResponse,
CreateWorkflowsRequest,
CreateWorkflowsResponse,
UpdateWorkflowResponse,
UpdateWorkflowRequest,
UpdateWorkflowRequestParam,
DeleteWorkflowRequestParam,
DeleteWorkflowResponse,
} from './types/types';
import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards';
@ -62,9 +68,10 @@ export class WorkflowsController {
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
console.log(context.trackingId);
return { workflows: [] };
const workflows = await this.workflowsService.getWorkflows(context, userId);
return { workflows };
}
@ApiResponse({
@ -99,14 +106,109 @@ export class WorkflowsController {
@Req() req: Request,
@Body() body: CreateWorkflowsRequest,
): Promise<CreateWorkflowsResponse> {
const { authorId } = body;
const { authorId, worktypeId, templateId, typists } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
console.log(context.trackingId);
console.log(authorId);
await this.workflowsService.createWorkflow(
context,
userId,
authorId,
worktypeId,
templateId,
typists,
);
return {};
}
@ApiResponse({
status: HttpStatus.OK,
type: UpdateWorkflowResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'パラメータ不正エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'updateWorkflow',
description: 'アカウント内のワークフローを編集します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Post('/:workflowId')
async updateWorkflow(
@Req() req: Request,
@Param() param: UpdateWorkflowRequestParam,
@Body() body: UpdateWorkflowRequest,
): Promise<UpdateWorkflowResponse> {
const { authorId, worktypeId, templateId, typists } = body;
const { workflowId } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
await this.workflowsService.updateWorkflow(
context,
userId,
workflowId,
authorId,
worktypeId,
templateId,
typists,
);
return {};
}
@ApiResponse({
status: HttpStatus.OK,
type: DeleteWorkflowResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'deleteWorkflow',
description: 'アカウント内のワークフローを削除します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Post('/:workflowId/delete')
async deleteWorkflow(
@Req() req: Request,
@Param() param: DeleteWorkflowRequestParam,
): Promise<DeleteWorkflowResponse> {
const { workflowId } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
console.log(workflowId);
console.log(context.trackingId);
return {};
}
}

View File

@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { WorkflowsController } from './workflows.controller';
import { WorkflowsService } from './workflows.service';
import { WorkflowsRepositoryModule } from '../../repositories/workflows/workflows.repository.module';
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
@Module({
imports: [UsersRepositoryModule],
imports: [UsersRepositoryModule, WorkflowsRepositoryModule, AdB2cModule],
providers: [WorkflowsService],
controllers: [WorkflowsController],
})

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,293 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { WorkflowsRepositoryService } from '../../repositories/workflows/workflows.repository.service';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { Context } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { Workflow, WorkflowTypist } from './types/types';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import { TypistGroupNotExistError } from '../../repositories/user_groups/errors/types';
import { WorktypeIdNotFoundError } from '../../repositories/worktypes/errors/types';
import { TemplateFileNotExistError } from '../../repositories/template_files/errors/types';
import {
AuthorIdAndWorktypeIdPairAlreadyExistsError,
WorkflowIdNotFoundError,
} from '../../repositories/workflows/errors/types';
@Injectable()
export class WorkflowsService {
private readonly logger = new Logger(WorkflowsService.name);
constructor() {}
constructor(
private readonly usersRepository: UsersRepositoryService,
private readonly workflowsRepository: WorkflowsRepositoryService,
private readonly adB2cService: AdB2cService,
) {}
/**
*
* @param context
* @param externalId
* @returns workflows
*/
async getWorkflows(
context: Context,
externalId: string,
): Promise<Workflow[]> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.getWorkflows.name} | params: { externalId: ${externalId} };`,
);
try {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// DBからワークフロー一覧を取得
const workflowRecords = await this.workflowsRepository.getWorkflows(
accountId,
);
// ワークフロー一覧からtypistのexternalIdを取得
const externalIds = workflowRecords.flatMap((workflow) => {
const workflowTypists = workflow.workflowTypists.flatMap(
(workflowTypist) => {
const { typist } = workflowTypist;
return typist ? [typist?.external_id] : [];
},
);
return workflowTypists;
});
const distinctedExternalIds = [...new Set(externalIds)];
// ADB2Cからユーザー一覧を取得
const adb2cUsers = await this.adB2cService.getUsers(
context,
distinctedExternalIds,
);
// DBから取得したワークフロー一覧を整形
const workflows = workflowRecords.map((workflow) => {
const { id, author, worktype, template, workflowTypists } = workflow;
const authorId = { id: author.id, authorId: author.author_id };
const worktypeId = worktype
? { id: worktype.id, worktypeId: worktype.custom_worktype_id }
: undefined;
const templateId = template
? { id: template.id, fileName: template.file_name }
: undefined;
// ルーティング候補を整形
const typists = workflowTypists.map((workflowTypist) => {
const { typist, typistGroup } = workflowTypist;
// typistがユーザーの場合はADB2Cからユーザー名を取得
const typistName = typist
? adb2cUsers.find(
(adb2cUser) => adb2cUser.id === typist.external_id,
).displayName
: typistGroup.name;
return {
typistUserId: typist?.id,
typistGroupId: typistGroup?.id,
typistName,
};
});
return {
id,
author: authorId,
worktype: worktypeId,
template: templateId,
typists,
};
});
return workflows;
} 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.getWorkflows.name}`,
);
}
}
/**
*
* @param context
* @param externalId
* @param authorId
* @param worktypeId
* @param templateId
* @param typists
* @returns workflow
*/
async createWorkflow(
context: Context,
externalId: string,
authorId: number,
worktypeId?: number | undefined,
templateId?: number | undefined,
typists?: WorkflowTypist[],
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.createWorkflow.name} | | params: { ` +
`externalId: ${externalId}, ` +
`authorId: ${authorId}, ` +
`worktypeId: ${worktypeId}, ` +
`templateId: ${templateId}, ` +
`typists: ${JSON.stringify(typists)} };`,
);
try {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
await this.workflowsRepository.createtWorkflows(
accountId,
authorId,
worktypeId,
templateId,
typists,
);
} 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 TypistGroupNotExistError:
throw new HttpException(
makeErrorResponse('E010908'),
HttpStatus.BAD_REQUEST,
);
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
HttpStatus.BAD_REQUEST,
);
case TemplateFileNotExistError:
throw new HttpException(
makeErrorResponse('E012001'),
HttpStatus.BAD_REQUEST,
);
case AuthorIdAndWorktypeIdPairAlreadyExistsError:
throw new HttpException(
makeErrorResponse('E013001'),
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.createWorkflow.name}`,
);
}
}
/**
*
* @param context
* @param externalId
* @param workflowId
* @param authorId
* @param [worktypeId]
* @param [templateId]
* @param [typists]
* @returns workflow
*/
async updateWorkflow(
context: Context,
externalId: string,
workflowId: number,
authorId: number,
worktypeId?: number | undefined,
templateId?: number | undefined,
typists?: WorkflowTypist[],
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateWorkflow.name} | params: { ` +
`externalId: ${externalId}, ` +
`workflowId: ${workflowId}, ` +
`authorId: ${authorId}, ` +
`worktypeId: ${worktypeId}, ` +
`templateId: ${templateId}, ` +
`typists: ${JSON.stringify(typists)} };`,
);
try {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
await this.workflowsRepository.updatetWorkflow(
accountId,
workflowId,
authorId,
worktypeId,
templateId,
typists,
);
} catch (e) {
this.logger.error(`[${context.trackingId}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case WorkflowIdNotFoundError:
throw new HttpException(
makeErrorResponse('E013002'),
HttpStatus.BAD_REQUEST,
);
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case TypistGroupNotExistError:
throw new HttpException(
makeErrorResponse('E010908'),
HttpStatus.BAD_REQUEST,
);
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
HttpStatus.BAD_REQUEST,
);
case TemplateFileNotExistError:
throw new HttpException(
makeErrorResponse('E012001'),
HttpStatus.BAD_REQUEST,
);
case AuthorIdAndWorktypeIdPairAlreadyExistsError:
throw new HttpException(
makeErrorResponse('E013001'),
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.updateWorkflow.name}`,
);
}
}
}

View File

@ -254,6 +254,31 @@ export class AdB2cService {
this.logger.log(`[OUT] [${context.trackingId}] ${this.deleteUser.name}`);
}
}
/**
* Azure AD B2Cからユーザ情報を削除する
* @param externalIds ID
* @param context
*/
async deleteUsers(externalIds: string[], context: Context): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.deleteUsers.name} | params: { externalIds: ${externalIds} };`,
);
try {
// 複数ユーザーを一括削除する方法が不明なため、rate limitの懸念があるのを承知のうえ単一削除の繰り返しで実装
// TODO 一括削除する方法が判明したら修正する
// https://learn.microsoft.com/en-us/graph/api/user-delete?view=graph-rest-1.0&tabs=javascript#example
externalIds.map(
async (x) => await this.graphClient.api(`users/${x}`).delete(),
);
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.deleteUsers.name}`);
}
}
}
// TODO [Task2002] 文字列の配列を15要素ずつ区切る(この処理も別タスクで削除予定)

View File

@ -51,14 +51,9 @@ export class BlobstorageService {
this.configService.get('STORAGE_ACCOUNT_ENDPOINT_EU'),
this.sharedKeyCredentialEU,
);
const expireTime = Number(
this.sasTokenExpireHour = Number(
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
);
if (Number.isNaN(expireTime)) {
throw new Error(`STORAGE_TOKEN_EXPIRE_TIME is invalid value NaN`);
}
this.sasTokenExpireHour = expireTime;
}
/**

View File

@ -10,9 +10,15 @@ import {
UpdateResult,
EntityManager,
} from 'typeorm';
import { User } from '../users/entity/user.entity';
import { User, UserArchive } from '../users/entity/user.entity';
import { Account } from './entity/account.entity';
import { License, LicenseOrder } from '../licenses/entity/license.entity';
import {
License,
LicenseAllocationHistory,
LicenseAllocationHistoryArchive,
LicenseArchive,
LicenseOrder,
} from '../licenses/entity/license.entity';
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
import {
getDirection,
@ -100,7 +106,8 @@ export class AccountsRepositoryService {
* @param tier
* @param adminExternalUserId
* @param adminUserRole
* @param adminUserAcceptedTermsVersion
* @param adminUserAcceptedEulaVersion
* @param adminUserAcceptedDpaVersion
* @returns account/admin user
*/
async createAccount(
@ -110,7 +117,8 @@ export class AccountsRepositoryService {
tier: number,
adminExternalUserId: string,
adminUserRole: string,
adminUserAcceptedTermsVersion: string,
adminUserAcceptedEulaVersion: string,
adminUserAcceptedDpaVersion: string,
): Promise<{ newAccount: Account; adminUser: User }> {
return await this.dataSource.transaction(async (entityManager) => {
const account = new Account();
@ -130,7 +138,8 @@ export class AccountsRepositoryService {
user.account_id = persistedAccount.id;
user.external_id = adminExternalUserId;
user.role = adminUserRole;
user.accepted_terms_version = adminUserAcceptedTermsVersion;
user.accepted_eula_version = adminUserAcceptedEulaVersion;
user.accepted_dpa_version = adminUserAcceptedDpaVersion;
}
const usersRepo = entityManager.getRepository(User);
const newUser = usersRepo.create(user);
@ -902,4 +911,65 @@ export class AccountsRepositoryService {
);
});
}
/**
*
* @param accountId
* @returns users
*/
async deleteAccountAndInsertArchives(accountId: number): Promise<User[]> {
return await this.dataSource.transaction(async (entityManager) => {
// 削除対象のユーザーを退避テーブルに退避
const users = await this.dataSource.getRepository(User).find({
where: {
account_id: accountId,
},
});
const userArchiveRepo = entityManager.getRepository(UserArchive);
await userArchiveRepo
.createQueryBuilder()
.insert()
.into(UserArchive)
.values(users)
.execute();
// 削除対象のライセンスを退避テーブルに退避
const licenses = await this.dataSource.getRepository(License).find({
where: {
account_id: accountId,
},
});
const licenseArchiveRepo = entityManager.getRepository(LicenseArchive);
await licenseArchiveRepo
.createQueryBuilder()
.insert()
.into(LicenseArchive)
.values(licenses)
.execute();
// 削除対象のライセンス割り当て履歴を退避テーブルに退避
const licenseHistories = await this.dataSource
.getRepository(LicenseAllocationHistory)
.find({
where: {
account_id: accountId,
},
});
const licenseHistoryArchiveRepo = entityManager.getRepository(
LicenseAllocationHistoryArchive,
);
await licenseHistoryArchiveRepo
.createQueryBuilder()
.insert()
.into(LicenseAllocationHistoryArchive)
.values(licenseHistories)
.execute();
// アカウントを削除
// アカウントを削除することで、外部キー制約がで紐づいている関連テーブルのデータも削除される
const accountRepo = entityManager.getRepository(Account);
await accountRepo.delete({ id: accountId });
return users;
});
}
}

View File

@ -7,6 +7,7 @@ import {
OneToOne,
JoinColumn,
ManyToOne,
PrimaryColumn,
} from 'typeorm';
import { User } from '../../users/entity/user.entity';
@ -188,3 +189,90 @@ export class LicenseAllocationHistory {
@JoinColumn({ name: 'license_id' })
license?: License;
}
@Entity({ name: 'licenses_archive' })
export class LicenseArchive {
@PrimaryColumn()
id: number;
@Column({ nullable: true })
expiry_date: Date;
@Column()
account_id: number;
@Column()
type: string;
@Column()
status: string;
@Column({ nullable: true })
allocated_user_id: number;
@Column({ nullable: true })
order_id: number;
@Column({ nullable: true })
deleted_at: Date;
@Column({ nullable: true })
delete_order_id: number;
@Column({ nullable: true })
created_by: string;
@Column()
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@Column()
updated_at: Date;
@CreateDateColumn()
archived_at: Date;
}
@Entity({ name: 'license_allocation_history_archive' })
export class LicenseAllocationHistoryArchive {
@PrimaryColumn()
id: number;
@Column()
user_id: number;
@Column()
license_id: number;
@Column()
is_allocated: boolean;
@Column()
account_id: number;
@Column()
executed_at: Date;
@Column()
switch_from_type: string;
@Column({ nullable: true })
deleted_at: Date;
@Column({ nullable: true })
created_by: string;
@Column()
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@Column()
updated_at: Date;
@CreateDateColumn()
archived_at: Date;
}

View File

@ -6,6 +6,8 @@ import {
License,
LicenseOrder,
LicenseAllocationHistory,
LicenseArchive,
LicenseAllocationHistoryArchive,
} from './entity/license.entity';
import { LicensesRepositoryService } from './licenses.repository.service';
@ -17,6 +19,8 @@ import { LicensesRepositoryService } from './licenses.repository.service';
CardLicense,
CardLicenseIssue,
LicenseAllocationHistory,
LicenseArchive,
LicenseAllocationHistoryArchive,
]),
],
providers: [LicensesRepositoryService],

View File

@ -0,0 +1,2 @@
// テンプレートファイルが存在しないエラー
export class TemplateFileNotExistError extends Error {}

View File

@ -9,6 +9,7 @@ import {
JoinColumn,
OneToOne,
OneToMany,
PrimaryColumn,
} from 'typeorm';
import { License } from '../../licenses/entity/license.entity';
import { UserGroupMember } from '../../user_groups/entity/user_group_member.entity';
@ -31,7 +32,10 @@ export class User {
author_id?: string;
@Column({ nullable: true })
accepted_terms_version?: string;
accepted_eula_version?: string;
@Column({ nullable: true })
accepted_dpa_version?: string;
@Column({ default: false })
email_verified: boolean;
@ -69,7 +73,7 @@ export class User {
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@ManyToOne(() => Account, (account) => account.user)
@ManyToOne(() => Account, (account) => account.user, { onDelete: 'CASCADE' }) // onDeleteはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: 'account_id' })
account?: Account;
@ -80,6 +84,66 @@ export class User {
userGroupMembers?: UserGroupMember[];
}
@Entity({ name: 'users_archive' })
export class UserArchive {
@PrimaryColumn()
id: number;
@Column()
external_id: string;
@Column()
account_id: number;
@Column()
role: string;
@Column({ nullable: true })
author_id?: string;
@Column({ nullable: true })
accepted_eula_version?: string;
@Column({ nullable: true })
accepted_dpa_version?: string;
@Column()
email_verified: boolean;
@Column()
auto_renew: boolean;
@Column()
license_alert: boolean;
@Column()
notification: boolean;
@Column()
encryption: boolean;
@Column()
prompt: boolean;
@Column({ nullable: true })
deleted_at?: Date;
@Column({ nullable: true })
created_by: string;
@Column()
created_at: Date;
@Column({ nullable: true })
updated_by?: string;
@Column()
updated_at: Date;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
archived_at: Date;
}
export type newUser = Omit<
User,
| 'id'

View File

@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entity/user.entity';
import { User, UserArchive } from './entity/user.entity';
import { UsersRepositoryService } from './users.repository.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
imports: [TypeOrmModule.forFeature([User, UserArchive])],
providers: [UsersRepositoryService],
exports: [UsersRepositoryService],
})

View File

@ -40,7 +40,8 @@ export class UsersRepositoryService {
license_alert,
notification,
author_id,
accepted_terms_version,
accepted_eula_version,
accepted_dpa_version,
encryption,
encryption_password: encryptionPassword,
prompt,
@ -54,7 +55,8 @@ export class UsersRepositoryService {
userEntity.license_alert = license_alert;
userEntity.notification = notification;
userEntity.author_id = author_id;
userEntity.accepted_terms_version = accepted_terms_version;
userEntity.accepted_eula_version = accepted_eula_version;
userEntity.accepted_dpa_version = accepted_dpa_version;
userEntity.encryption = encryption;
userEntity.encryption_password = encryptionPassword;
userEntity.prompt = prompt;
@ -375,6 +377,25 @@ export class UsersRepositoryService {
});
}
/**
* Authorユーザーを取得する
* @param accountId
* @returns author users
*/
async findAuthorUsers(accountId: number): Promise<User[]> {
return await this.dataSource.transaction(async (entityManager) => {
const repo = entityManager.getRepository(User);
const authors = await repo.find({
where: {
account_id: accountId,
role: USER_ROLES.AUTHOR,
deleted_at: IsNull(),
},
});
return authors;
});
}
/**
* UserID指定のユーザーとソート条件を同時に削除する
* @param userId

View File

@ -0,0 +1,59 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
JoinColumn,
ManyToOne,
} from 'typeorm';
import { WorkflowTypist } from './workflow_typists.entity';
import { Worktype } from '../../worktypes/entity/worktype.entity';
import { TemplateFile } from '../../template_files/entity/template_file.entity';
import { User } from '../../users/entity/user.entity';
@Entity({ name: 'workflows' })
export class Workflow {
@PrimaryGeneratedColumn()
id: number;
@Column()
account_id: number;
@Column()
author_id: number;
@Column({ nullable: true })
worktype_id?: number;
@Column({ nullable: true })
template_id?: number;
@Column({ nullable: true })
created_by: string;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true })
updated_by?: string;
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@ManyToOne(() => User, (user) => user.id)
@JoinColumn({ name: 'author_id' })
author?: User;
@ManyToOne(() => Worktype, (worktype) => worktype.id)
@JoinColumn({ name: 'worktype_id' })
worktype?: Worktype;
@ManyToOne(() => TemplateFile, (templateFile) => templateFile.id)
@JoinColumn({ name: 'template_id' })
template?: TemplateFile;
@OneToMany(() => WorkflowTypist, (workflowTypist) => workflowTypist.workflow)
workflowTypists?: WorkflowTypist[];
}

View File

@ -0,0 +1,51 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Workflow } from './workflow.entity';
import { User } from '../../users/entity/user.entity';
import { UserGroup } from '../../user_groups/entity/user_group.entity';
@Entity({ name: 'workflow_typists' })
export class WorkflowTypist {
@PrimaryGeneratedColumn()
id: number;
@Column()
workflow_id: number;
@Column({ nullable: true })
typist_id?: number;
@Column({ nullable: true })
typist_group_id?: number;
@Column({ nullable: true })
created_by: string;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true })
updated_by?: string;
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@ManyToOne(() => Workflow, (workflow) => workflow.id)
@JoinColumn({ name: 'workflow_id' })
workflow?: Workflow;
@ManyToOne(() => User, (user) => user.id)
@JoinColumn({ name: 'typist_id' })
typist?: User;
@ManyToOne(() => UserGroup, (userGroup) => userGroup.id)
@JoinColumn({ name: 'typist_group_id' })
typistGroup?: UserGroup;
}

View File

@ -0,0 +1,4 @@
// AuthorIDとWorktypeIDのペア重複エラー
export class AuthorIdAndWorktypeIdPairAlreadyExistsError extends Error {}
// WorkflowID存在エラー
export class WorkflowIdNotFoundError extends Error {}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkflowTypist } from './entity/workflow_typists.entity';
import { Workflow } from './entity/workflow.entity';
import { WorkflowsRepositoryService } from './workflows.repository.service';
@Module({
imports: [TypeOrmModule.forFeature([Workflow, WorkflowTypist])],
providers: [WorkflowsRepositoryService],
exports: [WorkflowsRepositoryService],
})
export class WorkflowsRepositoryModule {}

View File

@ -0,0 +1,345 @@
import { Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
import { Workflow } from './entity/workflow.entity';
import { WorkflowTypist as DbWorkflowTypist } from './entity/workflow_typists.entity';
import { User } from '../users/entity/user.entity';
import { WorkflowTypist } from '../../features/workflows/types/types';
import { Worktype } from '../worktypes/entity/worktype.entity';
import { TemplateFile } from '../template_files/entity/template_file.entity';
import { UserGroup } from '../user_groups/entity/user_group.entity';
import { TypistGroupNotExistError } from '../user_groups/errors/types';
import { UserNotFoundError } from '../users/errors/types';
import { WorktypeIdNotFoundError } from '../worktypes/errors/types';
import { TemplateFileNotExistError } from '../template_files/errors/types';
import {
AuthorIdAndWorktypeIdPairAlreadyExistsError,
WorkflowIdNotFoundError,
} from './errors/types';
@Injectable()
export class WorkflowsRepositoryService {
constructor(private dataSource: DataSource) {}
/**
*
* @param externalId
* @returns worktypes and active worktype id
*/
async getWorkflows(accountId: number): Promise<Workflow[]> {
return await this.dataSource.transaction(async (entityManager) => {
const workflowRepo = entityManager.getRepository(Workflow);
const workflows = await workflowRepo.find({
where: { account_id: accountId },
relations: {
author: true,
worktype: true,
template: true,
workflowTypists: {
typist: true,
typistGroup: true,
},
},
order: {
id: 'ASC',
},
});
return workflows;
});
}
/**
*
* @param accountId
* @param authorId
* @param worktypeId
* @param templateId
* @param typists
* @returns workflows
*/
async createtWorkflows(
accountId: number,
authorId: number,
worktypeId?: number | undefined,
templateId?: number | undefined,
typists?: WorkflowTypist[],
): Promise<void> {
return await this.dataSource.transaction(async (entityManager) => {
// authorの存在確認
const userRepo = entityManager.getRepository(User);
const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId },
});
if (!author) {
throw new UserNotFoundError(`author not found. id: ${authorId}`);
}
// worktypeの存在確認
if (worktypeId !== undefined) {
const worktypeRepo = entityManager.getRepository(Worktype);
const worktypes = await worktypeRepo.find({
where: { account_id: accountId, id: worktypeId },
});
if (worktypes.length === 0) {
throw new WorktypeIdNotFoundError(
`worktype not found. id: ${worktypeId}`,
);
}
}
// templateの存在確認
if (templateId !== undefined) {
const templateRepo = entityManager.getRepository(TemplateFile);
const template = await templateRepo.findOne({
where: { account_id: accountId, id: templateId },
});
if (!template) {
throw new TemplateFileNotExistError('template not found');
}
}
// ルーティング候補ユーザーの存在確認
const typistIds = typists.flatMap((typist) =>
typist.typistId ? [typist.typistId] : [],
);
const typistUsers = await userRepo.find({
where: { account_id: accountId, id: In(typistIds) },
});
if (typistUsers.length !== typistIds.length) {
throw new UserNotFoundError(`typist not found. ids: ${typistIds}`);
}
// ルーティング候補ユーザーグループの存在確認
const groupIds = typists.flatMap((typist) => {
return typist.typistGroupId ? [typist.typistGroupId] : [];
});
const userGroupRepo = entityManager.getRepository(UserGroup);
const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) },
});
if (typistGroups.length !== groupIds.length) {
throw new TypistGroupNotExistError(
`typist group not found. ids: ${groupIds}`,
);
}
const workflowRepo = entityManager.getRepository(Workflow);
// ワークフローの重複確認
const workflow = await workflowRepo.find({
where: {
account_id: accountId,
author_id: authorId,
worktype_id: worktypeId !== undefined ? worktypeId : IsNull(),
},
});
if (workflow.length !== 0) {
throw new AuthorIdAndWorktypeIdPairAlreadyExistsError(
'workflow already exists',
);
}
// ワークフローのデータ作成
const newWorkflow = this.makeWorkflow(
accountId,
authorId,
worktypeId,
templateId,
);
await workflowRepo.save(newWorkflow);
// ルーティング候補のデータ作成
const workflowTypists = typists.map((typist) =>
this.makeWorkflowTypist(
newWorkflow.id,
typist.typistId,
typist.typistGroupId,
),
);
const workflowTypistsRepo = entityManager.getRepository(DbWorkflowTypist);
await workflowTypistsRepo.save(workflowTypists);
});
}
/**
*
* @param accountId
* @param workflowId
* @param authorId
* @param [worktypeId]
* @param [templateId]
* @param [typists]
* @returns workflow
*/
async updatetWorkflow(
accountId: number,
workflowId: number,
authorId: number,
worktypeId?: number | undefined,
templateId?: number | undefined,
typists?: WorkflowTypist[],
): Promise<void> {
return await this.dataSource.transaction(async (entityManager) => {
const workflowRepo = entityManager.getRepository(Workflow);
// ワークフローの存在確認
const targetWorkflow = await workflowRepo.findOne({
where: { account_id: accountId, id: workflowId },
});
if (!targetWorkflow) {
throw new WorkflowIdNotFoundError(
`workflow not found. id: ${workflowId}`,
);
}
// authorの存在確認
const userRepo = entityManager.getRepository(User);
const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId },
});
if (!author) {
throw new UserNotFoundError(`author not found. id: ${authorId}`);
}
// worktypeの存在確認
if (worktypeId !== undefined) {
const worktypeRepo = entityManager.getRepository(Worktype);
const worktypes = await worktypeRepo.find({
where: { account_id: accountId, id: worktypeId },
});
if (worktypes.length === 0) {
throw new WorktypeIdNotFoundError(
`worktype not found. id: ${worktypeId}`,
);
}
}
// templateの存在確認
if (templateId !== undefined) {
const templateRepo = entityManager.getRepository(TemplateFile);
const template = await templateRepo.findOne({
where: { account_id: accountId, id: templateId },
});
if (!template) {
throw new TemplateFileNotExistError(
`template not found. id: ${templateId}`,
);
}
}
// ルーティング候補ユーザーの存在確認
const typistIds = typists.flatMap((typist) =>
typist.typistId ? [typist.typistId] : [],
);
const typistUsers = await userRepo.find({
where: { account_id: accountId, id: In(typistIds) },
});
if (typistUsers.length !== typistIds.length) {
throw new UserNotFoundError(`typist not found. ids: ${typistIds}`);
}
// ルーティング候補ユーザーグループの存在確認
const groupIds = typists.flatMap((typist) => {
return typist.typistGroupId ? [typist.typistGroupId] : [];
});
const userGroupRepo = entityManager.getRepository(UserGroup);
const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) },
});
if (typistGroups.length !== groupIds.length) {
throw new TypistGroupNotExistError(
`typist group not found. ids: ${groupIds}`,
);
}
const workflowTypistsRepo = entityManager.getRepository(DbWorkflowTypist);
// 既存データの削除
await workflowTypistsRepo.delete({ workflow_id: workflowId });
await workflowRepo.delete(workflowId);
{
// ワークフローの重複確認
const duplicateWorkflow = await workflowRepo.find({
where: {
account_id: accountId,
author_id: authorId,
worktype_id: worktypeId !== undefined ? worktypeId : IsNull(),
},
});
if (duplicateWorkflow.length !== 0) {
throw new AuthorIdAndWorktypeIdPairAlreadyExistsError(
'workflow already exists',
);
}
}
// ワークフローのデータ作成
const newWorkflow = this.makeWorkflow(
accountId,
authorId,
worktypeId,
templateId,
);
await workflowRepo.save(newWorkflow);
// ルーティング候補のデータ作成
const workflowTypists = typists.map((typist) =>
this.makeWorkflowTypist(
newWorkflow.id,
typist.typistId,
typist.typistGroupId,
),
);
await workflowTypistsRepo.save(workflowTypists);
});
}
/**
* DBに保存するワークフローデータを作成する
* @param accountId
* @param authorId
* @param worktypeId
* @param templateId
* @returns workflow
*/
private makeWorkflow(
accountId: number,
authorId: number,
worktypeId?: number | undefined,
templateId?: number | undefined,
): Workflow {
const workflow = new Workflow();
workflow.account_id = accountId;
workflow.author_id = authorId;
workflow.worktype_id = worktypeId;
workflow.template_id = templateId;
return workflow;
}
/**
* DBに保存するルーティング候補データを作成する
* @param workflowId
* @param typistId
* @param typistGroupId
* @returns workflow typist
*/
private makeWorkflowTypist(
workflowId: number,
typistId: number,
typistGroupId: number,
): DbWorkflowTypist {
const workflowTypist = new DbWorkflowTypist();
workflowTypist.workflow_id = workflowId;
workflowTypist.typist_id = typistId;
workflowTypist.typist_group_id = typistGroupId;
return workflowTypist;
}
}