From 273ba588cea224cc056f6ea1dfb66c049aad3258 Mon Sep 17 00:00:00 2001 From: "oura.a" Date: Mon, 16 Oct 2023 01:31:30 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20495:=20API=E4=BD=9C=E6=88=90?= =?UTF-8?q?=EF=BC=88=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7=E3=83=B3=E6=9B=B4?= =?UTF-8?q?=E6=96=B0API=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2804: API作成(バージョン更新API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2804) 同意済み利用規約バージョン更新APIを実装しました。 ## レビューポイント なし ## UIの変更 なし ## 動作確認状況 UT,ローカルで動作確認済み ## 補足 なし --- .../features/users/users.controller.spec.ts | 6 +- .../src/features/users/users.controller.ts | 27 ++++-- .../src/features/users/users.module.ts | 3 +- .../src/features/users/users.service.spec.ts | 89 +++++++++++++++++++ .../src/features/users/users.service.ts | 57 +++++++++++- .../src/repositories/users/errors/types.ts | 2 + .../users/users.repository.service.ts | 46 ++++++++++ 7 files changed, 222 insertions(+), 8 deletions(-) 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/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); + }); + } }