diff --git a/dictation_client/src/App.tsx b/dictation_client/src/App.tsx index 4be155a..d9717a9 100644 --- a/dictation_client/src/App.tsx +++ b/dictation_client/src/App.tsx @@ -22,11 +22,11 @@ const App = (): JSX.Element => { useEffect(() => { const id = globalAxios.interceptors.response.use( (response: AxiosResponse) => response, - (e: AxiosError) => { + (e: AxiosError<{ code?: string }>) => { if ( e?.response?.status === 401 && - e.code && - !UNAUTHORIZED_TO_CONTINUE_ERROR_CODES.includes(e.code) + e?.response?.data?.code && + !UNAUTHORIZED_TO_CONTINUE_ERROR_CODES.includes(e.response.data.code) ) { dispatch(clearToken()); instance.logoutRedirect({ diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 41368a5..8c3236f 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -56,4 +56,5 @@ export const errorCodes = [ "E011002", // ワークタイプ登録上限超過エラー "E011003", // ワークタイプ不在エラー "E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー + "E013002", // ワークフロー不在エラー ] as const; diff --git a/dictation_client/src/features/workflow/operations.ts b/dictation_client/src/features/workflow/operations.ts index 7b11e04..01a9815 100644 --- a/dictation_client/src/features/workflow/operations.ts +++ b/dictation_client/src/features/workflow/operations.ts @@ -346,6 +346,17 @@ export const deleteWorkflowAsync = createAsyncThunk< // e ⇒ errorObjectに変換" const error = createErrorObject(e); + // ワークフローが削除済みの場合は成功扱いとする + if (error.code === "E013002") { + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + thunkApi.dispatch( openSnackbar({ level: "error", diff --git a/dictation_server/src/common/test/modules.ts b/dictation_server/src/common/test/modules.ts index 05f8e77..aec0d61 100644 --- a/dictation_server/src/common/test/modules.ts +++ b/dictation_server/src/common/test/modules.ts @@ -34,10 +34,13 @@ import { TemplatesService } from '../../features/templates/templates.service'; import { TemplatesModule } from '../../features/templates/templates.module'; import { WorkflowsService } from '../../features/workflows/workflows.service'; import { WorkflowsModule } from '../../features/workflows/workflows.module'; +import { TermsService } from '../../features/terms/terms.service'; +import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module'; +import { TermsModule } from '../../features/terms/terms.module'; export const makeTestingModule = async ( datasource: DataSource, -): Promise => { +): Promise => { try { const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -56,6 +59,7 @@ export const makeTestingModule = async ( LicensesModule, TemplatesModule, WorkflowsModule, + TermsModule, AccountsRepositoryModule, UsersRepositoryModule, LicensesRepositoryModule, @@ -71,6 +75,7 @@ export const makeTestingModule = async ( AuthGuardsModule, SortCriteriaRepositoryModule, WorktypesRepositoryModule, + TermsRepositoryModule, ], providers: [ AuthService, @@ -82,6 +87,7 @@ export const makeTestingModule = async ( LicensesService, TemplatesService, WorkflowsService, + TermsService, ], }) .useMocker(async (token) => { diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts index b2bbf73..2761b0e 100644 --- a/dictation_server/src/features/accounts/accounts.controller.spec.ts +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -2,10 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AccountsController } from './accounts.controller'; import { AccountsService } from './accounts.service'; import { ConfigModule } from '@nestjs/config'; +import { AuthService } from '../auth/auth.service'; describe('AccountsController', () => { let controller: AccountsController; const mockAccountService = {}; + const mockAuthService = {}; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -16,10 +18,12 @@ describe('AccountsController', () => { }), ], controllers: [AccountsController], - providers: [AccountsService], + providers: [AccountsService, AuthService], }) .overrideProvider(AccountsService) .useValue(mockAccountService) + .overrideProvider(AuthService) + .useValue(mockAuthService) .compile(); controller = module.get(AccountsController); diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 92206af..bb98487 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -8,6 +8,7 @@ import { UseGuards, Param, Query, + HttpException, } from '@nestjs/common'; import { ApiOperation, @@ -74,12 +75,15 @@ import { AccessToken } from '../../common/token'; import jwt from 'jsonwebtoken'; import { makeContext } from '../../common/log'; import { v4 as uuidv4 } from 'uuid'; +import { AuthService } from '../auth/auth.service'; +import { makeErrorResponse } from '../../common/error/makeErrorResponse'; @ApiTags('accounts') @Controller('accounts') export class AccountsController { constructor( private readonly accountService: AccountsService, //private readonly cryptoService: CryptoService, + private readonly authService: AuthService, ) {} @Post() @@ -1116,11 +1120,22 @@ export class AccountsController { async getAccountInfoMinimalAccess( @Body() body: GetAccountInfoMinimalAccessRequest, ): Promise { - const context = makeContext(uuidv4()); + // IDトークンの検証 + const idToken = await this.authService.getVerifiedIdToken(body.idToken); + const isVerified = await this.authService.isVerifiedUser(idToken); + if (!isVerified) { + throw new HttpException( + makeErrorResponse('E010201'), + HttpStatus.BAD_REQUEST, + ); + } - // TODO 仮実装。API実装タスクで本実装する。 - // const idToken = await this.authService.getVerifiedIdToken(body.idToken); - // await this.accountService.getAccountInfoMinimalAccess(context, idToken); - return; + const context = makeContext(idToken.sub); + + const tier = await this.accountService.getAccountInfoMinimalAccess( + context, + idToken.sub, + ); + return { tier }; } } diff --git a/dictation_server/src/features/accounts/accounts.module.ts b/dictation_server/src/features/accounts/accounts.module.ts index 4b43415..23cf65e 100644 --- a/dictation_server/src/features/accounts/accounts.module.ts +++ b/dictation_server/src/features/accounts/accounts.module.ts @@ -9,6 +9,7 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module'; import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktypes.repository.module'; +import { AuthService } from '../auth/auth.service'; @Module({ imports: [ @@ -22,6 +23,6 @@ import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktype BlobstorageModule, ], controllers: [AccountsController], - providers: [AccountsService], + providers: [AccountsService, AuthService], }) export class AccountsModule {} diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index c7786d5..e5ec12d 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -5595,3 +5595,127 @@ describe('deleteAccountAndData', () => { expect(userRecord).toBe(null); }); }); +describe('getAccountInfoMinimalAccess', () => { + 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('IDトークンのsub情報からアカウントの階層情報を取得できること(第五階層)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const context = makeContext(admin.external_id); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account.tier).toBe(5); + } + + const tier = await service.getAccountInfoMinimalAccess( + context, + admin.external_id, + ); + + //実行結果を確認 + expect(tier).toBe(5); + }); + it('IDトークンのSub情報からアカウントの階層情報を取得できること(第四階層)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + // 第四階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 4, + }); + const context = makeContext(admin.external_id); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account.tier).toBe(4); + } + + const tier = await service.getAccountInfoMinimalAccess( + context, + admin.external_id, + ); + + //実行結果を確認 + expect(tier).toBe(4); + }); + it('対象のユーザーが存在しない場合、400エラーとなること', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + // 第四階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 4, + }); + const context = makeContext(admin.external_id); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account.tier).toBe(4); + } + + try { + await service.getAccountInfoMinimalAccess(context, 'fail_external_id'); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010204')); + } else { + fail(); + } + } + }); + it('DBアクセスに失敗した場合、500エラーとなること', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + // 第四階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 4, + }); + const context = makeContext(admin.external_id); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account.tier).toBe(4); + } + + //DBアクセスに失敗するようにする + const usersRepositoryService = module.get( + UsersRepositoryService, + ); + usersRepositoryService.findUserByExternalId = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.getAccountInfoMinimalAccess(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(); + } + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 0587575..c2dacad 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -1843,4 +1843,61 @@ export class AccountsService { `[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`, ); } + + /** + * IDトークンのsubからアカウントの階層情報を取得します + * @param context + * @param externalId + * @returns account info minimal access + */ + async getAccountInfoMinimalAccess( + context: Context, + externalId: string, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.getAccountInfoMinimalAccess.name} | params: { externalId: ${externalId} };`, + ); + + try { + const { account } = await this.usersRepository.findUserByExternalId( + externalId, + ); + if (!account) { + throw new AccountNotFoundError( + `Account not found. externalId: ${externalId}`, + ); + } + + return account.tier; + } catch (e) { + this.logger.error(`[${context.trackingId}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case UserNotFoundError: + throw new HttpException( + makeErrorResponse('E010204'), + HttpStatus.BAD_REQUEST, + ); + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E010501'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.getAccountInfoMinimalAccess.name}`, + ); + } + } } diff --git a/dictation_server/src/features/auth/auth.service.spec.ts b/dictation_server/src/features/auth/auth.service.spec.ts index deb63bd..41a7ba4 100644 --- a/dictation_server/src/features/auth/auth.service.spec.ts +++ b/dictation_server/src/features/auth/auth.service.spec.ts @@ -166,7 +166,7 @@ describe('checkIsAcceptedLatestVersion', () => { await createTermInfo(source, 'EULA', '1.0'); await createTermInfo(source, 'DPA', '1.0'); - const result = await service.checkIsAcceptedLatestVersion(context, idToken); + const result = await service.isAcceptedLatestVersion(context, idToken); expect(result).toBe(true); }); @@ -187,7 +187,7 @@ describe('checkIsAcceptedLatestVersion', () => { await createTermInfo(source, 'EULA', '1.0'); await createTermInfo(source, 'DPA', '1.0'); - const result = await service.checkIsAcceptedLatestVersion(context, idToken); + const result = await service.isAcceptedLatestVersion(context, idToken); expect(result).toBe(true); }); @@ -208,7 +208,7 @@ describe('checkIsAcceptedLatestVersion', () => { await createTermInfo(source, 'EULA', '1.1'); await createTermInfo(source, 'DPA', '1.0'); - const result = await service.checkIsAcceptedLatestVersion(context, idToken); + const result = await service.isAcceptedLatestVersion(context, idToken); expect(result).toBe(false); }); @@ -229,7 +229,7 @@ describe('checkIsAcceptedLatestVersion', () => { await createTermInfo(source, 'EULA', '1.1'); await createTermInfo(source, 'DPA', '1.0'); - const result = await service.checkIsAcceptedLatestVersion(context, idToken); + const result = await service.isAcceptedLatestVersion(context, idToken); expect(result).toBe(false); }); @@ -250,7 +250,7 @@ describe('checkIsAcceptedLatestVersion', () => { await createTermInfo(source, 'EULA', '1.0'); await createTermInfo(source, 'DPA', '1.1'); - const result = await service.checkIsAcceptedLatestVersion(context, idToken); + const result = await service.isAcceptedLatestVersion(context, idToken); expect(result).toBe(false); }); }); diff --git a/dictation_server/src/features/notification/notification.controller.ts b/dictation_server/src/features/notification/notification.controller.ts index c234163..0a640be 100644 --- a/dictation_server/src/features/notification/notification.controller.ts +++ b/dictation_server/src/features/notification/notification.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + HttpException, HttpStatus, Post, Req, @@ -21,6 +22,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper'; import { AccessToken } from '../../common/token'; import jwt from 'jsonwebtoken'; import { makeContext } from '../../common/log'; +import { makeErrorResponse } from '../../common/error/makeErrorResponse'; @ApiTags('notification') @Controller('notification') @@ -57,7 +59,20 @@ export class NotificationController { const { handler, pns } = body; const accessToken = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); diff --git a/dictation_server/src/features/templates/templates.controller.ts b/dictation_server/src/features/templates/templates.controller.ts index 6d85968..ef972cf 100644 --- a/dictation_server/src/features/templates/templates.controller.ts +++ b/dictation_server/src/features/templates/templates.controller.ts @@ -1,4 +1,11 @@ -import { Controller, Get, HttpStatus, Req, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + HttpException, + HttpStatus, + Req, + UseGuards, +} from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, @@ -16,6 +23,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper'; import { Request } from 'express'; import { makeContext } from '../../common/log'; import { TemplatesService } from './templates.service'; +import { makeErrorResponse } from '../../common/error/makeErrorResponse'; @ApiTags('templates') @Controller('templates') @@ -46,8 +54,21 @@ export class TemplatesController { @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) @Get() async getTemplates(@Req() req: Request): Promise { - const token = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(token, { json: true }) as AccessToken; + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); const templates = await this.templatesService.getTemplates(context, userId); diff --git a/dictation_server/src/features/templates/templates.service.spec.ts b/dictation_server/src/features/templates/templates.service.spec.ts index c29b168..8401db7 100644 --- a/dictation_server/src/features/templates/templates.service.spec.ts +++ b/dictation_server/src/features/templates/templates.service.spec.ts @@ -9,7 +9,7 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; describe('getTemplates', () => { - let source: DataSource = null; + let source: DataSource | undefined = undefined; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -22,12 +22,16 @@ describe('getTemplates', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); - source = null; + source = undefined; }); it('テンプレートファイル一覧を取得できる', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(TemplatesService); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); @@ -62,10 +66,13 @@ describe('getTemplates', () => { expect(templates[1].id).toBe(template2.id); expect(templates[1].name).toBe(template2.file_name); } - }); + }, 6000000); it('テンプレートファイル一覧を取得できる(0件)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(TemplatesService); // 第五階層のアカウント作成 const { admin } = await makeTestAccount(source, { tier: 5 }); @@ -80,7 +87,10 @@ describe('getTemplates', () => { }); it('テンプレートファイル一覧の取得に失敗した場合、エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(TemplatesService); // 第五階層のアカウント作成 const { admin } = await makeTestAccount(source, { tier: 5 }); diff --git a/dictation_server/src/features/terms/terms.controller.spec.ts b/dictation_server/src/features/terms/terms.controller.spec.ts index 7473f05..b57a498 100644 --- a/dictation_server/src/features/terms/terms.controller.spec.ts +++ b/dictation_server/src/features/terms/terms.controller.spec.ts @@ -6,11 +6,14 @@ describe('TermsController', () => { let controller: TermsController; beforeEach(async () => { + const mockTermsService = {}; const module: TestingModule = await Test.createTestingModule({ controllers: [TermsController], providers: [TermsService], - }).compile(); - + }) + .overrideProvider(TermsService) + .useValue(mockTermsService) + .compile(); controller = module.get(TermsController); }); diff --git a/dictation_server/src/features/terms/terms.controller.ts b/dictation_server/src/features/terms/terms.controller.ts index 5155587..1d855ba 100644 --- a/dictation_server/src/features/terms/terms.controller.ts +++ b/dictation_server/src/features/terms/terms.controller.ts @@ -28,12 +28,7 @@ export class TermsController { async getTermsInfo(): Promise { 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[]; + const termsInfo = await this.termsService.getTermsInfo(context); return { termsInfo }; } diff --git a/dictation_server/src/features/terms/terms.module.ts b/dictation_server/src/features/terms/terms.module.ts index e314518..704e003 100644 --- a/dictation_server/src/features/terms/terms.module.ts +++ b/dictation_server/src/features/terms/terms.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; import { TermsController } from './terms.controller'; import { TermsService } from './terms.service'; +import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module'; @Module({ + imports: [TermsRepositoryModule], controllers: [TermsController], - providers: [TermsService] + providers: [TermsService], }) export class TermsModule {} diff --git a/dictation_server/src/features/terms/terms.service.spec.ts b/dictation_server/src/features/terms/terms.service.spec.ts index 6e8839b..33fc59a 100644 --- a/dictation_server/src/features/terms/terms.service.spec.ts +++ b/dictation_server/src/features/terms/terms.service.spec.ts @@ -1,18 +1,83 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { TermsService } from './terms.service'; +import { DataSource } from 'typeorm'; +import { makeTestingModule } from '../../common/test/modules'; +import { createTermInfo } from '../auth/test/utility'; +import { makeContext } from '../../common/log'; +import { v4 as uuidv4 } from 'uuid'; +import { HttpException, HttpStatus } from '@nestjs/common'; +import { makeErrorResponse } from '../../common/error/makeErrorResponse'; -describe('TermsService', () => { - let service: TermsService; - +describe('利用規約取得', () => { + let source: DataSource = null; beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [TermsService], - }).compile(); - - service = module.get(TermsService); + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); }); - it('should be defined', () => { - expect(service).toBeDefined(); + afterEach(async () => { + await source.destroy(); + source = null; + }); + + it('最新の利用規約情報が取得できる', async () => { + const module = await makeTestingModule(source); + const service = module.get(TermsService); + + await createTermInfo(source, 'EULA', 'v1.0'); + await createTermInfo(source, 'EULA', 'v1.1'); + await createTermInfo(source, 'DPA', 'v1.0'); + await createTermInfo(source, 'DPA', 'v1.2'); + + const context = makeContext(uuidv4()); + const result = await service.getTermsInfo(context); + + expect(result[0].documentType).toBe('EULA'); + expect(result[0].version).toBe('v1.1'); + expect(result[1].documentType).toBe('DPA'); + expect(result[1].version).toBe('v1.2'); + }); + + it('利用規約情報(EULA、DPA両方)が存在しない場合エラーとなる', async () => { + const module = await makeTestingModule(source); + const service = module.get(TermsService); + const context = makeContext(uuidv4()); + await expect(service.getTermsInfo(context)).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); + }); + + it('利用規約情報(EULAのみ)が存在しない場合エラーとなる', async () => { + const module = await makeTestingModule(source); + const service = module.get(TermsService); + await createTermInfo(source, 'DPA', 'v1.0'); + const context = makeContext(uuidv4()); + await expect(service.getTermsInfo(context)).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); + }); + + it('利用規約情報(DPAのみ)が存在しない場合エラーとなる', async () => { + const module = await makeTestingModule(source); + const service = module.get(TermsService); + await createTermInfo(source, 'EULA', 'v1.0'); + const context = makeContext(uuidv4()); + await expect(service.getTermsInfo(context)).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); }); }); diff --git a/dictation_server/src/features/terms/terms.service.ts b/dictation_server/src/features/terms/terms.service.ts index 51ba395..526bde6 100644 --- a/dictation_server/src/features/terms/terms.service.ts +++ b/dictation_server/src/features/terms/terms.service.ts @@ -1,4 +1,44 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { Context } from '../../common/log'; +import { makeErrorResponse } from '../../common/error/makeErrorResponse'; +import { TermInfo } from './types/types'; +import { TermsRepositoryService } from '../../repositories/terms/terms.repository.service'; +import { TERM_TYPE } from '../../constants'; @Injectable() -export class TermsService {} +export class TermsService { + constructor(private readonly termsRepository: TermsRepositoryService) {} + private readonly logger = new Logger(TermsService.name); + + /** + * 利用規約情報を取得する + * return termsInfo + */ + async getTermsInfo(context: Context): Promise { + this.logger.log(`[IN] [${context.trackingId}] ${this.getTermsInfo.name}`); + try { + const { eulaVersion, dpaVersion } = + await this.termsRepository.getLatestTermsInfo(); + return [ + { + documentType: TERM_TYPE.EULA, + version: eulaVersion, + }, + { + documentType: TERM_TYPE.DPA, + version: dpaVersion, + }, + ]; + } catch (e) { + this.logger.error(`error=${e}`); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.getTermsInfo.name}`, + ); + } + } +} diff --git a/dictation_server/src/features/terms/types/types.ts b/dictation_server/src/features/terms/types/types.ts index afea3f0..6a45eae 100644 --- a/dictation_server/src/features/terms/types/types.ts +++ b/dictation_server/src/features/terms/types/types.ts @@ -10,3 +10,8 @@ export class GetTermsInfoResponse { @ApiProperty({ type: [TermInfo] }) termsInfo: TermInfo[]; } + +export type TermsVersion = { + eulaVersion: string; + dpaVersion: string; +}; diff --git a/dictation_server/src/features/users/users.controller.spec.ts b/dictation_server/src/features/users/users.controller.spec.ts index 6193160..b64a9fc 100644 --- a/dictation_server/src/features/users/users.controller.spec.ts +++ b/dictation_server/src/features/users/users.controller.spec.ts @@ -2,10 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { ConfigModule } from '@nestjs/config'; +import { AuthService } from '../auth/auth.service'; describe('UsersController', () => { let controller: UsersController; const mockUserService = {}; + const mockAuthService = {}; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -16,10 +18,12 @@ describe('UsersController', () => { }), ], controllers: [UsersController], - providers: [UsersService], + providers: [UsersService, AuthService], }) .overrideProvider(UsersService) .useValue(mockUserService) + .overrideProvider(AuthService) + .useValue(mockAuthService) .compile(); controller = module.get(UsersController); diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 81306ec..9ee6370 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -41,6 +41,7 @@ import { UpdateAcceptedVersionResponse, } from './types/types'; import { UsersService } from './users.service'; +import { AuthService } from '../auth/auth.service'; import jwt from 'jsonwebtoken'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { @@ -56,7 +57,10 @@ import { v4 as uuidv4 } from 'uuid'; @ApiTags('users') @Controller('users') export class UsersController { - constructor(private readonly usersService: UsersService) {} + constructor( + private readonly usersService: UsersService, + private readonly authService: AuthService, + ) {} @ApiResponse({ status: HttpStatus.OK, @@ -495,11 +499,24 @@ export class UsersController { async updateAcceptedVersion( @Body() body: UpdateAcceptedVersionRequest, ): Promise { - const context = makeContext(uuidv4()); + const { idToken, acceptedEULAVersion, acceptedDPAVersion } = body; - // TODO 仮実装。API実装タスクで本実装する。 - // const idToken = await this.authService.getVerifiedIdToken(body.idToken); - // await this.usersService.updateAcceptedVersion(context, idToken); + const verifiedIdToken = await this.authService.getVerifiedIdToken(idToken); + const context = makeContext(verifiedIdToken.sub); + + const isVerified = await this.authService.isVerifiedUser(verifiedIdToken); + if (!isVerified) { + throw new HttpException( + makeErrorResponse('E010201'), + HttpStatus.BAD_REQUEST, + ); + } + await this.usersService.updateAcceptedVersion( + context, + verifiedIdToken.sub, + acceptedEULAVersion, + acceptedDPAVersion, + ); return {}; } } diff --git a/dictation_server/src/features/users/users.module.ts b/dictation_server/src/features/users/users.module.ts index e4ae006..f349a95 100644 --- a/dictation_server/src/features/users/users.module.ts +++ b/dictation_server/src/features/users/users.module.ts @@ -7,6 +7,7 @@ import { UsersRepositoryModule } from '../../repositories/users/users.repository import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; +import { AuthService } from '../auth/auth.service'; @Module({ imports: [ @@ -18,6 +19,6 @@ import { UsersService } from './users.service'; ConfigModule, ], controllers: [UsersController], - providers: [UsersService], + providers: [UsersService, AuthService], }) export class UsersModule {} diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 7bc2eae..79114d1 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -43,6 +43,7 @@ import { makeTestSimpleAccount, makeTestUser, } from '../../common/test/utility'; +import { v4 as uuidv4 } from 'uuid'; describe('UsersService.confirmUser', () => { let source: DataSource = null; @@ -2480,3 +2481,91 @@ describe('UsersService.updateUser', () => { ); }); }); + +describe('UsersService.updateAcceptedVersion', () => { + 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 { admin } = await makeTestAccount(source, { + tier: 5, + }); + const context = makeContext(uuidv4()); + + const service = module.get(UsersService); + await service.updateAcceptedVersion(context, admin.external_id, 'v2.0'); + const user = await getUser(source, admin.id); + + expect(user.accepted_eula_version).toBe('v2.0'); + }); + + it('同意済み利用規約バージョンを更新できる(第一~第四)', async () => { + const module = await makeTestingModule(source); + const { admin } = await makeTestAccount(source, { + tier: 4, + }); + const context = makeContext(uuidv4()); + + const service = module.get(UsersService); + await service.updateAcceptedVersion( + context, + admin.external_id, + 'v2.0', + 'v3.0', + ); + const user = await getUser(source, admin.id); + + expect(user.accepted_eula_version).toBe('v2.0'); + expect(user.accepted_dpa_version).toBe('v3.0'); + }); + + it('パラメータが不在のときエラーとなる(第五)', async () => { + const module = await makeTestingModule(source); + const { admin } = await makeTestAccount(source, { + tier: 5, + }); + const context = makeContext(uuidv4()); + + const service = module.get(UsersService); + await expect( + service.updateAcceptedVersion(context, admin.external_id, undefined), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST), + ); + }); + + it('パラメータが不在のときエラーとなる(第一~第四)', async () => { + const module = await makeTestingModule(source); + const { admin } = await makeTestAccount(source, { + tier: 4, + }); + const context = makeContext(uuidv4()); + + const service = module.get(UsersService); + await expect( + service.updateAcceptedVersion( + context, + admin.external_id, + 'v2.0', + undefined, + ), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST), + ); + }); +}); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index a3e9521..142c165 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -4,7 +4,7 @@ import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { isVerifyError, verify } from '../../common/jwt'; import { getPublicKey } from '../../common/jwt/jwt'; import { makePassword } from '../../common/password/password'; -import { AccessToken } from '../../common/token'; +import { AccessToken, IDToken } from '../../common/token'; import { SortDirection, TaskListSortableAttribute, @@ -30,6 +30,7 @@ import { EmailAlreadyVerifiedError, EncryptionPasswordNeedError, InvalidRoleChangeError, + UpdateTermsVersionNotSetError, UserNotFoundError, } from '../../repositories/users/errors/types'; import { @@ -967,4 +968,58 @@ export class UsersService { ); } } + + /** + * 同意済み利用規約バージョンを更新する + * @param context + * @param idToken + * @param eulaVersion + * @param dpaVersion + */ + async updateAcceptedVersion( + context: Context, + externalId: string, + eulaVersion: string, + dpaVersion?: string, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.updateAcceptedVersion.name} | params: { ` + + `externalId: ${externalId}, ` + + `eulaVersion: ${eulaVersion}, ` + + `dpaVersion: ${dpaVersion}, };`, + ); + + try { + await this.usersRepository.updateAcceptedTermsVersion( + externalId, + eulaVersion, + dpaVersion, + ); + } 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 UpdateTermsVersionNotSetError: + throw new HttpException( + makeErrorResponse('E010001'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.updateAcceptedVersion.name}`, + ); + } + } } diff --git a/dictation_server/src/features/workflows/workflows.controller.ts b/dictation_server/src/features/workflows/workflows.controller.ts index 46b0fa0..bf96f2a 100644 --- a/dictation_server/src/features/workflows/workflows.controller.ts +++ b/dictation_server/src/features/workflows/workflows.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Get, + HttpException, HttpStatus, Param, Post, @@ -34,6 +35,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper'; import { Request } from 'express'; import { makeContext } from '../../common/log'; import { WorkflowsService } from './workflows.service'; +import { makeErrorResponse } from '../../common/error/makeErrorResponse'; @ApiTags('workflows') @Controller('workflows') @@ -64,8 +66,22 @@ export class WorkflowsController { @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) @Get() async getWorkflows(@Req() req: Request): Promise { - const token = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(token, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); @@ -107,17 +123,31 @@ export class WorkflowsController { @Body() body: CreateWorkflowsRequest, ): Promise { const { authorId, worktypeId, templateId, typists } = body; - const token = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(token, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); await this.workflowsService.createWorkflow( context, userId, authorId, + typists, worktypeId, templateId, - typists, ); return {}; @@ -158,8 +188,22 @@ export class WorkflowsController { ): Promise { const { authorId, worktypeId, templateId, typists } = body; const { workflowId } = param; - const token = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(token, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); await this.workflowsService.updateWorkflow( @@ -167,9 +211,9 @@ export class WorkflowsController { userId, workflowId, authorId, + typists, worktypeId, templateId, - typists, ); return {}; @@ -208,8 +252,22 @@ export class WorkflowsController { @Param() param: DeleteWorkflowRequestParam, ): Promise { const { workflowId } = param; - const token = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(token, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); await this.workflowsService.deleteWorkflow(context, userId, workflowId); diff --git a/dictation_server/src/features/workflows/workflows.service.spec.ts b/dictation_server/src/features/workflows/workflows.service.spec.ts index 51a3cb4..047e870 100644 --- a/dictation_server/src/features/workflows/workflows.service.spec.ts +++ b/dictation_server/src/features/workflows/workflows.service.spec.ts @@ -21,7 +21,7 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; describe('getWorkflows', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -34,12 +34,15 @@ describe('getWorkflows', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('アカウント内のWorkflow一覧を取得できる', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -149,10 +152,10 @@ describe('getWorkflows', () => { expect(resWorkflows[0].id).toBe(workflow1.id); expect(resWorkflows[0].author.id).toBe(authorId1); expect(resWorkflows[0].author.authorId).toBe('AUTHOR1'); - expect(resWorkflows[0].worktype.id).toBe(worktypeId1); - expect(resWorkflows[0].worktype.worktypeId).toBe('worktype1'); - expect(resWorkflows[0].template.id).toBe(templateId1); - expect(resWorkflows[0].template.fileName).toBe('fileName1'); + expect(resWorkflows[0].worktype?.id).toBe(worktypeId1); + expect(resWorkflows[0].worktype?.worktypeId).toBe('worktype1'); + expect(resWorkflows[0].template?.id).toBe(templateId1); + expect(resWorkflows[0].template?.fileName).toBe('fileName1'); expect(resWorkflows[0].typists.length).toBe(1); expect(resWorkflows[0].typists[0].typistUserId).toBe(typistId); expect(resWorkflows[0].typists[0].typistName).toBe('typist1'); @@ -161,8 +164,8 @@ describe('getWorkflows', () => { expect(resWorkflows[1].author.id).toBe(authorId2); expect(resWorkflows[1].author.authorId).toBe('AUTHOR2'); expect(resWorkflows[1].worktype).toBe(undefined); - expect(resWorkflows[1].template.id).toBe(templateId1); - expect(resWorkflows[1].template.fileName).toBe('fileName1'); + expect(resWorkflows[1].template?.id).toBe(templateId1); + expect(resWorkflows[1].template?.fileName).toBe('fileName1'); expect(resWorkflows[1].typists.length).toBe(1); expect(resWorkflows[1].typists[0].typistGroupId).toBe(userGroupId); expect(resWorkflows[1].typists[0].typistName).toBe('group1'); @@ -170,8 +173,8 @@ describe('getWorkflows', () => { expect(resWorkflows[2].id).toBe(workflow3.id); expect(resWorkflows[2].author.id).toBe(authorId3); expect(resWorkflows[2].author.authorId).toBe('AUTHOR3'); - expect(resWorkflows[2].worktype.id).toBe(worktypeId1); - expect(resWorkflows[2].worktype.worktypeId).toBe('worktype1'); + expect(resWorkflows[2].worktype?.id).toBe(worktypeId1); + expect(resWorkflows[2].worktype?.worktypeId).toBe('worktype1'); expect(resWorkflows[2].template).toBe(undefined); expect(resWorkflows[2].typists.length).toBe(1); expect(resWorkflows[2].typists[0].typistGroupId).toBe(userGroupId); @@ -180,7 +183,9 @@ describe('getWorkflows', () => { }); it('アカウント内のWorkflow一覧を取得できる(0件)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { admin } = await makeTestAccount(source, { tier: 5 }); @@ -200,7 +205,9 @@ describe('getWorkflows', () => { }); it('DBアクセスに失敗した場合、500エラーを返却する', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); @@ -228,7 +235,7 @@ describe('getWorkflows', () => { }); describe('createWorkflows', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -240,12 +247,15 @@ describe('createWorkflows', () => { return source.initialize(); }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('アカウント内にWorkflowを作成できる(WorktypeIDあり、テンプレートファイルあり)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -288,13 +298,13 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - worktypeId, - templateId, [ { typistId: typistId, }, ], + worktypeId, + templateId, ); //実行結果を確認 @@ -314,7 +324,9 @@ describe('createWorkflows', () => { }); it('アカウント内にWorkflowを作成できる(WorktypeIDなし、テンプレートファイルあり)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -351,13 +363,13 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - undefined, - templateId, [ { typistId: typistId, }, ], + undefined, + templateId, ); //実行結果を確認 @@ -377,7 +389,9 @@ describe('createWorkflows', () => { }); it('アカウント内にWorkflowを作成できる(WorktypeIDあり、テンプレートファイルなし)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -413,13 +427,13 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - worktypeId, - undefined, [ { typistId: typistId, }, ], + worktypeId, + undefined, ); //実行結果を確認 @@ -439,7 +453,9 @@ describe('createWorkflows', () => { }); it('アカウント内にWorkflowを作成できる(WorktypeIDなし、テンプレートファイルなし)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -469,13 +485,13 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - undefined, - undefined, [ { typistId: typistId, }, ], + undefined, + undefined, ); //実行結果を確認 @@ -495,7 +511,9 @@ describe('createWorkflows', () => { }); it('アカウント内にWorkflowを作成できる(WorktypeIDなし、テンプレートファイルなし、同一AuthorIDのワークフローあり)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -532,26 +550,26 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - worktypeId, - undefined, [ { typistId: typistId, }, ], + worktypeId, + undefined, ); await service.createWorkflow( context, admin.external_id, authorId, - undefined, - undefined, [ { typistId: typistId, }, ], + undefined, + undefined, ); //実行結果を確認 @@ -571,7 +589,9 @@ describe('createWorkflows', () => { }); it('DBにAuthorが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); @@ -611,13 +631,13 @@ describe('createWorkflows', () => { context, admin.external_id, 0, - worktypeId, - templateId, [ { typistId: typistId, }, ], + worktypeId, + templateId, ); } catch (e) { if (e instanceof HttpException) { @@ -630,7 +650,9 @@ describe('createWorkflows', () => { }); it('DBにWorktypeIDが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -669,13 +691,13 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - 9999, - templateId, [ { typistId: typistId, }, ], + 9999, + templateId, ); } catch (e) { if (e instanceof HttpException) { @@ -688,7 +710,9 @@ describe('createWorkflows', () => { }); it('DBにテンプレートファイルが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -726,13 +750,13 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - worktypeId, - 9999, [ { typistId: typistId, }, ], + worktypeId, + 9999, ); } catch (e) { if (e instanceof HttpException) { @@ -745,7 +769,9 @@ describe('createWorkflows', () => { }); it('DBにルーティング候補ユーザーが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -785,13 +811,13 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - worktypeId, - templateId, [ { typistId: 9999, }, ], + worktypeId, + templateId, ); } catch (e) { if (e instanceof HttpException) { @@ -804,7 +830,9 @@ describe('createWorkflows', () => { }); it('DBにルーティング候補グループが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -844,13 +872,13 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - worktypeId, - templateId, [ { typistGroupId: 9999, }, ], + worktypeId, + templateId, ); } catch (e) { if (e instanceof HttpException) { @@ -863,7 +891,9 @@ describe('createWorkflows', () => { }); it('DBにAuthorIDとWorktypeIDのペアがすでに存在する場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -912,13 +942,13 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - worktypeId, - templateId, [ { typistId: typistId, }, ], + worktypeId, + templateId, ); } catch (e) { if (e instanceof HttpException) { @@ -931,7 +961,9 @@ describe('createWorkflows', () => { }); it('DBアクセスに失敗した場合、500エラーを返却する', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -984,13 +1016,13 @@ describe('createWorkflows', () => { context, admin.external_id, authorId, - worktypeId, - templateId, [ { typistId: typistId, }, ], + worktypeId, + templateId, ); } catch (e) { if (e instanceof HttpException) { @@ -1004,7 +1036,7 @@ describe('createWorkflows', () => { }); describe('updateWorkflow', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -1016,12 +1048,15 @@ describe('updateWorkflow', () => { return source.initialize(); }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('アカウント内のWorkflowを更新できる(WorktypeIDあり、テンプレートファイルあり)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1090,13 +1125,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, authorId2, - worktypeId, - templateId, [ { typistId: typistId2, }, ], + worktypeId, + templateId, ); //実行結果を確認 @@ -1115,7 +1150,9 @@ describe('updateWorkflow', () => { }); it('アカウント内にWorkflowを作成できる(WorktypeIDなし、テンプレートファイルあり)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1178,13 +1215,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, authorId2, - undefined, - templateId, [ { typistId: typistId2, }, ], + undefined, + templateId, ); //実行結果を確認 @@ -1203,7 +1240,9 @@ describe('updateWorkflow', () => { }); it('アカウント内にWorkflowを作成できる(WorktypeIDあり、テンプレートファイルなし)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1265,13 +1304,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, authorId2, - worktypeId, - undefined, [ { typistId: typistId2, }, ], + worktypeId, + undefined, ); //実行結果を確認 @@ -1290,7 +1329,9 @@ describe('updateWorkflow', () => { }); it('アカウント内にWorkflowを作成できる(WorktypeIDなし、テンプレートファイルなし)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1346,13 +1387,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, authorId2, - undefined, - undefined, [ { typistId: typistId2, }, ], + undefined, + undefined, ); //実行結果を確認 @@ -1371,7 +1412,9 @@ describe('updateWorkflow', () => { }); it('アカウント内にWorkflowを作成できる(WorktypeIDなし、テンプレートファイルなし、同一AuthorIDのワークフローあり)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1447,13 +1490,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow1.id, authorId2, - undefined, - undefined, [ { typistId: typistId2, }, ], + undefined, + undefined, ); //実行結果を確認 @@ -1472,7 +1515,9 @@ describe('updateWorkflow', () => { }); it('DBにWorkflowが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1497,13 +1542,13 @@ describe('updateWorkflow', () => { admin.external_id, 9999, authorId1, - undefined, - undefined, [ { typistId: typistId1, }, ], + undefined, + undefined, ); } catch (e) { if (e instanceof HttpException) { @@ -1515,7 +1560,9 @@ describe('updateWorkflow', () => { } }); it('DBにAuthorが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1568,13 +1615,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, 9999, - worktypeId, - templateId, [ { typistId: typistId1, }, ], + worktypeId, + templateId, ); } catch (e) { if (e instanceof HttpException) { @@ -1587,7 +1634,9 @@ describe('updateWorkflow', () => { }); it('DBにWorktypeIDが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1634,13 +1683,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, authorId1, - 9999, - templateId, [ { typistId: typistId1, }, ], + 9999, + templateId, ); } catch (e) { if (e instanceof HttpException) { @@ -1653,7 +1702,9 @@ describe('updateWorkflow', () => { }); it('DBにテンプレートファイルが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1699,13 +1750,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, authorId1, - worktypeId, - 9999, [ { typistId: typistId1, }, ], + worktypeId, + 9999, ); } catch (e) { if (e instanceof HttpException) { @@ -1718,7 +1769,9 @@ describe('updateWorkflow', () => { }); it('DBにルーティング候補ユーザーが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1771,13 +1824,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, authorId1, - worktypeId, - templateId, [ { typistId: 9999, }, ], + worktypeId, + templateId, ); } catch (e) { if (e instanceof HttpException) { @@ -1790,7 +1843,9 @@ describe('updateWorkflow', () => { }); it('DBにルーティング候補グループが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1843,13 +1898,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, authorId1, - worktypeId, - templateId, [ { typistGroupId: 9999, }, ], + worktypeId, + templateId, ); } catch (e) { if (e instanceof HttpException) { @@ -1862,7 +1917,9 @@ describe('updateWorkflow', () => { }); it('DBにAuthorIDとWorktypeIDのペアがすでに存在する場合、400エラーとなること', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1909,13 +1966,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, authorId1, - worktypeId1, - undefined, [ { typistId: typistId1, }, ], + worktypeId1, + undefined, ); } catch (e) { if (e instanceof HttpException) { @@ -1928,7 +1985,9 @@ describe('updateWorkflow', () => { }); it('DBアクセスに失敗した場合、500エラーを返却する', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -1983,13 +2042,13 @@ describe('updateWorkflow', () => { admin.external_id, preWorkflow.id, authorId1, - undefined, - undefined, [ { typistId: typistId1, }, ], + undefined, + undefined, ); } catch (e) { if (e instanceof HttpException) { diff --git a/dictation_server/src/features/workflows/workflows.service.ts b/dictation_server/src/features/workflows/workflows.service.ts index de23f5f..b8be4e6 100644 --- a/dictation_server/src/features/workflows/workflows.service.ts +++ b/dictation_server/src/features/workflows/workflows.service.ts @@ -14,6 +14,7 @@ import { WorkflowNotFoundError, } from '../../repositories/workflows/errors/types'; import { AccountNotFoundError } from '../../repositories/accounts/errors/types'; +import { Assignee } from '../tasks/types/types'; @Injectable() export class WorkflowsService { @@ -47,15 +48,20 @@ export class WorkflowsService { // ワークフロー一覧からtypistのexternalIdを取得 const externalIds = workflowRecords.flatMap((workflow) => { - const workflowTypists = workflow.workflowTypists.flatMap( + const workflowTypists = workflow.workflowTypists?.flatMap( (workflowTypist) => { const { typist } = workflowTypist; - return typist ? [typist?.external_id] : []; + return typist ? [typist.external_id] : []; }, ); return workflowTypists; }); - const distinctedExternalIds = [...new Set(externalIds)]; + // externalIdsからundefinedを除外 + const filteredExternalIds = externalIds.filter( + (externalId): externalId is string => externalId !== undefined, + ); + // externalIdsから重複を除外 + const distinctedExternalIds = [...new Set(filteredExternalIds)]; // ADB2Cからユーザー一覧を取得 const adb2cUsers = await this.adB2cService.getUsers( @@ -64,8 +70,11 @@ export class WorkflowsService { ); // DBから取得したワークフロー一覧を整形 - const workflows = workflowRecords.map((workflow) => { + const workflows = workflowRecords.map((workflow): Workflow => { const { id, author, worktype, template, workflowTypists } = workflow; + if (!author || !author.id || !author.author_id) { + throw new Error('author is undefined'); + } const authorId = { id: author.id, authorId: author.author_id }; const worktypeId = worktype @@ -75,16 +84,24 @@ export class WorkflowsService { ? { id: template.id, fileName: template.file_name } : undefined; + if (!workflowTypists) { + throw new Error('workflowTypists is undefined'); + } + // ルーティング候補を整形 - const typists = workflowTypists.map((workflowTypist) => { + const typists = workflowTypists.map((workflowTypist): Assignee => { const { typist, typistGroup } = workflowTypist; // typistがユーザーの場合はADB2Cからユーザー名を取得 const typistName = typist ? adb2cUsers.find( (adb2cUser) => adb2cUser.id === typist.external_id, - ).displayName - : typistGroup.name; + )?.displayName + : typistGroup?.name; + + if (!typistName) { + throw new Error('typistName is undefined'); + } return { typistUserId: typist?.id, @@ -130,9 +147,9 @@ export class WorkflowsService { context: Context, externalId: string, authorId: number, + typists: WorkflowTypist[], worktypeId?: number | undefined, templateId?: number | undefined, - typists?: WorkflowTypist[], ): Promise { this.logger.log( `[IN] [${context.trackingId}] ${this.createWorkflow.name} | | params: { ` + @@ -149,9 +166,9 @@ export class WorkflowsService { await this.workflowsRepository.createtWorkflows( accountId, authorId, + typists, worktypeId, templateId, - typists, ); } catch (e) { this.logger.error(`[${context.trackingId}] error=${e}`); @@ -215,9 +232,9 @@ export class WorkflowsService { externalId: string, workflowId: number, authorId: number, + typists: WorkflowTypist[], worktypeId?: number | undefined, templateId?: number | undefined, - typists?: WorkflowTypist[], ): Promise { this.logger.log( `[IN] [${context.trackingId}] ${this.updateWorkflow.name} | params: { ` + @@ -236,9 +253,9 @@ export class WorkflowsService { accountId, workflowId, authorId, + typists, worktypeId, templateId, - typists, ); } catch (e) { this.logger.error(`[${context.trackingId}] error=${e}`); diff --git a/dictation_server/src/gateways/adb2c/adb2c.service.ts b/dictation_server/src/gateways/adb2c/adb2c.service.ts index 254acea..9f22a17 100644 --- a/dictation_server/src/gateways/adb2c/adb2c.service.ts +++ b/dictation_server/src/gateways/adb2c/adb2c.service.ts @@ -30,17 +30,18 @@ export const isConflictError = (arg: unknown): arg is ConflictError => { @Injectable() export class AdB2cService { private readonly logger = new Logger(AdB2cService.name); - private readonly tenantName = this.configService.get('TENANT_NAME'); + private readonly tenantName = + this.configService.getOrThrow('TENANT_NAME'); private readonly flowName = - this.configService.get('SIGNIN_FLOW_NAME'); + this.configService.getOrThrow('SIGNIN_FLOW_NAME'); private graphClient: Client; constructor(private readonly configService: ConfigService) { // ADB2Cへの認証情報 const credential = new ClientSecretCredential( - this.configService.get('ADB2C_TENANT_ID'), - this.configService.get('ADB2C_CLIENT_ID'), - this.configService.get('ADB2C_CLIENT_SECRET'), + this.configService.getOrThrow('ADB2C_TENANT_ID'), + this.configService.getOrThrow('ADB2C_CLIENT_ID'), + this.configService.getOrThrow('ADB2C_CLIENT_SECRET'), ); const authProvider = new TokenCredentialAuthenticationProvider(credential, { scopes: ['https://graph.microsoft.com/.default'], diff --git a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts index 57dd908..8d8b91e 100644 --- a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts +++ b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts @@ -28,31 +28,31 @@ export class BlobstorageService { private readonly sasTokenExpireHour: number; constructor(private readonly configService: ConfigService) { this.sharedKeyCredentialUS = new StorageSharedKeyCredential( - this.configService.get('STORAGE_ACCOUNT_NAME_US'), - this.configService.get('STORAGE_ACCOUNT_KEY_US'), + this.configService.getOrThrow('STORAGE_ACCOUNT_NAME_US'), + this.configService.getOrThrow('STORAGE_ACCOUNT_KEY_US'), ); this.sharedKeyCredentialAU = new StorageSharedKeyCredential( - this.configService.get('STORAGE_ACCOUNT_NAME_AU'), - this.configService.get('STORAGE_ACCOUNT_KEY_AU'), + this.configService.getOrThrow('STORAGE_ACCOUNT_NAME_AU'), + this.configService.getOrThrow('STORAGE_ACCOUNT_KEY_AU'), ); this.sharedKeyCredentialEU = new StorageSharedKeyCredential( - this.configService.get('STORAGE_ACCOUNT_NAME_EU'), - this.configService.get('STORAGE_ACCOUNT_KEY_EU'), + this.configService.getOrThrow('STORAGE_ACCOUNT_NAME_EU'), + this.configService.getOrThrow('STORAGE_ACCOUNT_KEY_EU'), ); this.blobServiceClientUS = new BlobServiceClient( - this.configService.get('STORAGE_ACCOUNT_ENDPOINT_US'), + this.configService.getOrThrow('STORAGE_ACCOUNT_ENDPOINT_US'), this.sharedKeyCredentialUS, ); this.blobServiceClientAU = new BlobServiceClient( - this.configService.get('STORAGE_ACCOUNT_ENDPOINT_AU'), + this.configService.getOrThrow('STORAGE_ACCOUNT_ENDPOINT_AU'), this.sharedKeyCredentialAU, ); this.blobServiceClientEU = new BlobServiceClient( - this.configService.get('STORAGE_ACCOUNT_ENDPOINT_EU'), + this.configService.getOrThrow('STORAGE_ACCOUNT_ENDPOINT_EU'), this.sharedKeyCredentialEU, ); - this.sasTokenExpireHour = Number( - this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'), + this.sasTokenExpireHour = this.configService.getOrThrow( + 'STORAGE_TOKEN_EXPIRE_TIME', ); } diff --git a/dictation_server/src/gateways/notificationhub/notificationhub.service.ts b/dictation_server/src/gateways/notificationhub/notificationhub.service.ts index 2f8fe90..22038cc 100644 --- a/dictation_server/src/gateways/notificationhub/notificationhub.service.ts +++ b/dictation_server/src/gateways/notificationhub/notificationhub.service.ts @@ -18,8 +18,8 @@ export class NotificationhubService { private readonly client: NotificationHubsClient; constructor(private readonly configService: ConfigService) { this.client = new NotificationHubsClient( - this.configService.get('NOTIFICATION_HUB_CONNECT_STRING'), - this.configService.get('NOTIFICATION_HUB_NAME'), + this.configService.getOrThrow('NOTIFICATION_HUB_CONNECT_STRING'), + this.configService.getOrThrow('NOTIFICATION_HUB_NAME'), ); } diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index 3fb3e66..1b29705 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -9,7 +9,7 @@ import { Context } from '../../common/log'; export class SendGridService { private readonly logger = new Logger(SendGridService.name); constructor(private readonly configService: ConfigService) { - const key = this.configService.get('SENDGRID_API_KEY'); + const key = this.configService.getOrThrow('SENDGRID_API_KEY'); sendgrid.setApiKey(key); } diff --git a/dictation_server/src/main.ts b/dictation_server/src/main.ts index b4ad979..eec7370 100644 --- a/dictation_server/src/main.ts +++ b/dictation_server/src/main.ts @@ -5,14 +5,15 @@ import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import helmet from 'helmet'; const helmetDirectives = helmet.contentSecurityPolicy.getDefaultDirectives(); + helmetDirectives['connect-src'] = process.env.STAGE === 'local' ? [ "'self'", - process.env.ADB2C_ORIGIN, - process.env.STORAGE_ACCOUNT_ENDPOINT_US, - process.env.STORAGE_ACCOUNT_ENDPOINT_AU, - process.env.STORAGE_ACCOUNT_ENDPOINT_EU, + process.env.ADB2C_ORIGIN ?? '', + process.env.STORAGE_ACCOUNT_ENDPOINT_US ?? '', + process.env.STORAGE_ACCOUNT_ENDPOINT_AU ?? '', + process.env.STORAGE_ACCOUNT_ENDPOINT_EU ?? '', ] : ["'self'"]; diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 1e207ca..3436d1c 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -336,7 +336,7 @@ export class TasksRepositoryService { await taskRepo.update( { audio_file_id: audio_file_id }, { - typist_user: null, + typist_user_id: null, status: TASK_STATUS.UPLOADED, }, ); diff --git a/dictation_server/src/repositories/template_files/entity/template_file.entity.ts b/dictation_server/src/repositories/template_files/entity/template_file.entity.ts index f5f9064..d772869 100644 --- a/dictation_server/src/repositories/template_files/entity/template_file.entity.ts +++ b/dictation_server/src/repositories/template_files/entity/template_file.entity.ts @@ -19,13 +19,13 @@ export class TemplateFile { @Column() file_name: string; @Column({ nullable: true }) - created_by?: string; + created_by: string | null; @CreateDateColumn() created_at: Date; @Column({ nullable: true }) - updated_by?: string; + updated_by: string | null; @UpdateDateColumn() updated_at: Date; @OneToMany(() => Task, (task) => task.template_file) - tasks?: Task[]; + tasks: Task[] | null; } diff --git a/dictation_server/src/repositories/terms/terms.repository.module.ts b/dictation_server/src/repositories/terms/terms.repository.module.ts index 9edd181..f88c52c 100644 --- a/dictation_server/src/repositories/terms/terms.repository.module.ts +++ b/dictation_server/src/repositories/terms/terms.repository.module.ts @@ -1,8 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Term } from './entity/term.entity'; +import { TermsRepositoryService } from './terms.repository.service'; @Module({ imports: [TypeOrmModule.forFeature([Term])], + providers: [TermsRepositoryService], + exports: [TermsRepositoryService], }) export class TermsRepositoryModule {} diff --git a/dictation_server/src/repositories/terms/terms.repository.service.ts b/dictation_server/src/repositories/terms/terms.repository.service.ts new file mode 100644 index 0000000..7c79f24 --- /dev/null +++ b/dictation_server/src/repositories/terms/terms.repository.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { TermsVersion } from '../../features/terms/types/types'; +import { Term } from './entity/term.entity'; +import { TERM_TYPE } from '../../constants'; +import { TermInfoNotFoundError } from '../users/errors/types'; + +@Injectable() +export class TermsRepositoryService { + constructor(private dataSource: DataSource) {} + + /* + * 利用規約の最新バージョンを取得する + * @returns Term[] + */ + async getLatestTermsInfo(): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const termRepo = entityManager.getRepository(Term); + const latestEulaInfo = await termRepo.findOne({ + where: { + document_type: TERM_TYPE.EULA, + }, + order: { + id: 'DESC', + }, + }); + const latestDpaInfo = await termRepo.findOne({ + where: { + document_type: TERM_TYPE.DPA, + }, + order: { + id: 'DESC', + }, + }); + + if (!latestEulaInfo || !latestDpaInfo) { + throw new TermInfoNotFoundError( + `Terms info is not found. latestEulaInfo: ${latestEulaInfo}, latestDpaInfo: ${latestDpaInfo}`, + ); + } + return { + eulaVersion: latestEulaInfo.version, + dpaVersion: latestDpaInfo.version, + }; + }); + } +} diff --git a/dictation_server/src/repositories/users/errors/types.ts b/dictation_server/src/repositories/users/errors/types.ts index 808e93c..faee0b1 100644 --- a/dictation_server/src/repositories/users/errors/types.ts +++ b/dictation_server/src/repositories/users/errors/types.ts @@ -10,3 +10,5 @@ export class InvalidRoleChangeError extends Error {} export class EncryptionPasswordNeedError extends Error {} // 利用規約バージョン情報不在エラー export class TermInfoNotFoundError extends Error {} +// 利用規約バージョンパラメータ不在エラー +export class UpdateTermsVersionNotSetError extends Error {} diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 31f62bf..4bd4d02 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -13,6 +13,7 @@ import { InvalidRoleChangeError, EncryptionPasswordNeedError, TermInfoNotFoundError, + UpdateTermsVersionNotSetError, } from './errors/types'; import { LICENSE_ALLOCATED_STATUS, @@ -475,4 +476,49 @@ export class UsersRepositoryService { }; }); } + + /** + * 同意済み利用規約のバージョンを更新する + * @param externalId + * @param eulaVersion + * @param dpaVersion + * @returns update + */ + async updateAcceptedTermsVersion( + externalId: string, + eulaVersion: string, + dpaVersion: string | undefined, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const userRepo = entityManager.getRepository(User); + const user = await userRepo.findOne({ + where: { + external_id: externalId, + }, + relations: { + account: true, + }, + }); + + if (!user) { + throw new UserNotFoundError( + `User not found. externalId: ${externalId}`, + ); + } + + // パラメータが不在の場合はエラーを返却 + if (!eulaVersion) { + throw new UpdateTermsVersionNotSetError(`EULA version param not set.`); + } + if (user.account.tier !== TIERS.TIER5 && !dpaVersion) { + throw new UpdateTermsVersionNotSetError( + `DPA version param not set. User's tier: ${user.account.tier}`, + ); + } + + user.accepted_eula_version = eulaVersion; + user.accepted_dpa_version = dpaVersion ?? user.accepted_dpa_version; + await userRepo.update({ id: user.id }, user); + }); + } } diff --git a/dictation_server/src/repositories/workflows/entity/workflow.entity.ts b/dictation_server/src/repositories/workflows/entity/workflow.entity.ts index e3bac8e..806311a 100644 --- a/dictation_server/src/repositories/workflows/entity/workflow.entity.ts +++ b/dictation_server/src/repositories/workflows/entity/workflow.entity.ts @@ -25,35 +25,35 @@ export class Workflow { author_id: number; @Column({ nullable: true }) - worktype_id?: number; + worktype_id: number | null; @Column({ nullable: true }) - template_id?: number; + template_id: number | null; @Column({ nullable: true }) - created_by: string; + created_by: string | null; @CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定 created_at: Date; @Column({ nullable: true }) - updated_by?: string; + updated_by: string | null; @UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定 updated_at: Date; @ManyToOne(() => User, (user) => user.id) @JoinColumn({ name: 'author_id' }) - author?: User; + author: User | null; @ManyToOne(() => Worktype, (worktype) => worktype.id) @JoinColumn({ name: 'worktype_id' }) - worktype?: Worktype; + worktype: Worktype | null; @ManyToOne(() => TemplateFile, (templateFile) => templateFile.id) @JoinColumn({ name: 'template_id' }) - template?: TemplateFile; + template: TemplateFile | null; @OneToMany(() => WorkflowTypist, (workflowTypist) => workflowTypist.workflow) - workflowTypists?: WorkflowTypist[]; + workflowTypists: WorkflowTypist[] | null; } diff --git a/dictation_server/src/repositories/workflows/workflows.repository.service.ts b/dictation_server/src/repositories/workflows/workflows.repository.service.ts index f658dcd..6ec186d 100644 --- a/dictation_server/src/repositories/workflows/workflows.repository.service.ts +++ b/dictation_server/src/repositories/workflows/workflows.repository.service.ts @@ -61,9 +61,9 @@ export class WorkflowsRepositoryService { async createtWorkflows( accountId: number, authorId: number, + typists: WorkflowTypist[], worktypeId?: number | undefined, templateId?: number | undefined, - typists?: WorkflowTypist[], ): Promise { return await this.dataSource.transaction(async (entityManager) => { // authorの存在確認 @@ -178,9 +178,9 @@ export class WorkflowsRepositoryService { accountId: number, workflowId: number, authorId: number, + typists: WorkflowTypist[], worktypeId?: number | undefined, templateId?: number | undefined, - typists?: WorkflowTypist[], ): Promise { return await this.dataSource.transaction(async (entityManager) => { const workflowRepo = entityManager.getRepository(Workflow); @@ -343,8 +343,8 @@ export class WorkflowsRepositoryService { const workflow = new Workflow(); workflow.account_id = accountId; workflow.author_id = authorId; - workflow.worktype_id = worktypeId; - workflow.template_id = templateId; + workflow.worktype_id = worktypeId ?? null; + workflow.template_id = templateId ?? null; return workflow; } @@ -358,8 +358,8 @@ export class WorkflowsRepositoryService { */ private makeWorkflowTypist( workflowId: number, - typistId: number, - typistGroupId: number, + typistId?: number, + typistGroupId?: number, ): DbWorkflowTypist { const workflowTypist = new DbWorkflowTypist(); workflowTypist.workflow_id = workflowId;