From 338d6b88a9c745d0ce94419312ade39d95a4d2db Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 4 Aug 2023 05:39:33 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20299:=20=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E7=B7=A8=E9=9B=86API=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2317: ユーザー編集API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2317) - ユーザー編集APIとテストを実装しました。 ## レビューポイント - リポジトリでのチェックは適切か - バリデータの適用は適切か - テストケースは十分か ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_server/src/common/error/code.ts | 2 + dictation_server/src/common/error/message.ts | 2 + .../encryptionPassword.validator.ts | 32 ++ .../common/validators/roleAuthor.validator.ts | 39 ++ .../src/features/users/test/utility.ts | 20 +- .../src/features/users/types/types.ts | 6 + .../src/features/users/users.controller.ts | 33 +- .../src/features/users/users.service.spec.ts | 539 ++++++++++++++++++ .../src/features/users/users.service.ts | 107 +++- .../repositories/users/entity/user.entity.ts | 3 + .../src/repositories/users/errors/types.ts | 6 + .../users/users.repository.service.ts | 91 ++- 12 files changed, 869 insertions(+), 11 deletions(-) create mode 100644 dictation_server/src/common/validators/encryptionPassword.validator.ts create mode 100644 dictation_server/src/common/validators/roleAuthor.validator.ts diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 4bee6e5..94fa41c 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -30,6 +30,8 @@ export const ErrorCodes = [ 'E010204', // ユーザ不在エラー 'E010205', // DBのRoleが想定外の値エラー 'E010206', // DBのTierが想定外の値エラー + 'E010207', // ユーザーのRole変更不可エラー + 'E010208', // ユーザーの暗号化パスワード不足エラー 'E010301', // メールアドレス登録済みエラー 'E010302', // authorId重複エラー 'E010401', // PONumber重複エラー diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 30ab3c2..bbc1b40 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -19,6 +19,8 @@ export const errors: Errors = { E010204: 'User not Found Error.', E010205: 'Role from DB is unexpected value Error.', E010206: 'Tier from DB is unexpected value Error.', + E010207: 'User role change not allowed Error.', + E010208: 'User encryption password not found Error.', E010301: 'This email user already created Error', E010302: 'This AuthorId already used Error', E010401: 'This PoNumber already used Error', diff --git a/dictation_server/src/common/validators/encryptionPassword.validator.ts b/dictation_server/src/common/validators/encryptionPassword.validator.ts new file mode 100644 index 0000000..9cae899 --- /dev/null +++ b/dictation_server/src/common/validators/encryptionPassword.validator.ts @@ -0,0 +1,32 @@ +import { registerDecorator, ValidationOptions } from 'class-validator'; + +export const IsPasswordvalid = (validationOptions?: ValidationOptions) => { + return (object: any, propertyName: string) => { + registerDecorator({ + name: 'IsPasswordvalid', + target: object.constructor, + propertyName: propertyName, + constraints: [], + options: validationOptions, + validator: { + validate: (value: string | undefined) => { + // passwordが設定されていない場合はチェックしない + if (value === undefined) { + return true; + } + // 正規表現でパスワードのチェックを行う + // 4~16文字の半角英数字と記号のみ + const regex = /^[!-~]{4,16}$/; + if (!regex.test(value)) { + return false; + } + + return true; + }, + defaultMessage: () => { + return 'EncryptionPassword rule not satisfied'; + }, + }, + }); + }; +}; diff --git a/dictation_server/src/common/validators/roleAuthor.validator.ts b/dictation_server/src/common/validators/roleAuthor.validator.ts new file mode 100644 index 0000000..02bb06e --- /dev/null +++ b/dictation_server/src/common/validators/roleAuthor.validator.ts @@ -0,0 +1,39 @@ +import { + registerDecorator, + ValidationOptions, + ValidationArguments, +} from 'class-validator'; +import { USER_ROLES } from '../../constants'; +import { + SignupRequest, + PostUpdateUserRequest, +} from '../../features/users/types/types'; + +export const IsRoleAuthorDataValid = < + T extends SignupRequest | PostUpdateUserRequest, +>( + validationOptions?: ValidationOptions, +) => { + return (object: T, propertyName: string) => { + registerDecorator({ + name: 'IsRoleAuthorDataValid', + target: object.constructor, + propertyName: propertyName, + constraints: [], + options: validationOptions, + validator: { + validate: (value: boolean | undefined, args: ValidationArguments) => { + const request = args.object as T; + const { role } = request; + if (role === USER_ROLES.AUTHOR && value === undefined) { + return false; + } + return true; + }, + defaultMessage: () => { + return `When role is author, ${propertyName} cannot be undefined`; + }, + }, + }); + }; +}; diff --git a/dictation_server/src/features/users/test/utility.ts b/dictation_server/src/features/users/test/utility.ts index 6d2251a..31a8a34 100644 --- a/dictation_server/src/features/users/test/utility.ts +++ b/dictation_server/src/features/users/test/utility.ts @@ -65,6 +65,9 @@ export const createUser = async ( role: string, author_id?: string | undefined, auto_renew?: boolean, + encryption?: boolean | undefined, + encryption_password?: string | undefined, + prompt?: boolean | undefined, ): Promise<{ userId: number; externalId: string }> => { const { identifiers } = await datasource.getRepository(User).insert({ account_id: accountId, @@ -76,8 +79,9 @@ export const createUser = async ( auto_renew: auto_renew, license_alert: true, notification: true, - encryption: false, - prompt: false, + encryption: encryption ?? false, + encryption_password: encryption_password, + prompt: prompt ?? false, created_by: 'test_runner', created_at: new Date(), updated_by: 'updater', @@ -87,6 +91,18 @@ export const createUser = async ( return { userId: user.id, externalId: external_id }; }; +export const getUser = async ( + datasource: DataSource, + id: number, +): Promise => { + const user = await datasource.getRepository(User).findOne({ + where: { + id: id, + }, + }); + return user; +}; + /** * * @param datasource diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index cee0495..d836739 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -5,6 +5,8 @@ import { USER_LICENSE_STATUS, } from '../../../constants'; import { USER_ROLES } from '../../../constants'; +import { IsRoleAuthorDataValid } from '../../../common/validators/roleAuthor.validator'; +import { IsPasswordvalid } from '../../../common/validators/encryptionPassword.validator'; export class ConfirmRequest { @ApiProperty() @@ -197,6 +199,7 @@ export class PostUpdateUserRequest { role: string; @ApiProperty({ required: false }) + @IsRoleAuthorDataValid() authorId?: string | undefined; @ApiProperty() @@ -209,12 +212,15 @@ export class PostUpdateUserRequest { notification: boolean; @ApiProperty({ required: false }) + @IsRoleAuthorDataValid() encryption?: boolean | undefined; @ApiProperty({ required: false }) + @IsPasswordvalid() encryptionPassword?: string | undefined; @ApiProperty({ required: false }) + @IsRoleAuthorDataValid() prompt?: boolean | undefined; } diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 527e1f8..09f051e 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -335,11 +335,36 @@ export class UsersController { @Body() body: PostUpdateUserRequest, @Req() req: Request, ): Promise { - const accessToken = retrieveAuthorizationToken(req); - const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken; + const { + id, + role, + authorId, + autoRenew, + licenseAlart, + notification, + encryption, + encryptionPassword, + prompt, + } = body; - console.log(body); - console.log(decodedToken); + const accessToken = retrieveAuthorizationToken(req); + const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken; + + const context = makeContext(userId); + + await this.usersService.updateUser( + context, + userId, + id, + role, + authorId, + autoRenew, + licenseAlart, + notification, + encryption, + encryptionPassword, + prompt, + ); return {}; } } diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 66b7e45..dec1174 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -15,6 +15,7 @@ import { createLicense, createUser, createUserGroup, + getUser, makeTestingModuleWithAdb2c, } from './test/utility'; import { DataSource } from 'typeorm'; @@ -22,7 +23,9 @@ import { UsersService } from './users.service'; import { LICENSE_EXPIRATION_THRESHOLD_DAYS, USER_LICENSE_STATUS, + USER_ROLES, } from '../../constants'; +import { makeTestingModule } from '../../common/test/modules'; describe('UsersService.confirmUser', () => { it('ユーザの仮登録時に払い出されるトークンにより、未認証のユーザが認証済みになる', async () => { @@ -1016,3 +1019,539 @@ describe('UsersService.getSortCriteria', () => { ); }); }); + +describe('UsersService.updateUser', () => { + 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('ユーザー情報を更新できる(None)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId: external_id } = await createUser( + source, + accountId, + 'external_id', + USER_ROLES.NONE, + undefined, + true, + ); + const { userId: user1 } = await createUser( + source, + accountId, + 'external_id1', + USER_ROLES.NONE, + undefined, + true, + ); + + const service = module.get(UsersService); + + expect( + await service.updateUser( + { trackingId: 'trackingId' }, + external_id, + user1, + USER_ROLES.NONE, + undefined, + false, + false, + false, + undefined, + undefined, + undefined, + ), + ).toEqual(undefined); + + const createdUser = await getUser(source, user1); + + expect(createdUser.id).toBe(user1); + expect(createdUser.role).toBe(USER_ROLES.NONE); + expect(createdUser.author_id).toBeNull(); + expect(createdUser.auto_renew).toBe(false); + expect(createdUser.license_alert).toBe(false); + expect(createdUser.notification).toBe(false); + expect(createdUser.encryption).toBe(false); + expect(createdUser.encryption_password).toBeNull(); + expect(createdUser.prompt).toBe(false); + }); + + it('ユーザー情報を更新できる(Typist)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId: external_id } = await createUser( + source, + accountId, + 'external_id', + USER_ROLES.NONE, + undefined, + true, + ); + const { userId: user1 } = await createUser( + source, + accountId, + 'external_id1', + USER_ROLES.TYPIST, + undefined, + true, + ); + + const service = module.get(UsersService); + + expect( + await service.updateUser( + { trackingId: 'trackingId' }, + external_id, + user1, + USER_ROLES.TYPIST, + undefined, + false, + false, + false, + undefined, + undefined, + undefined, + ), + ).toEqual(undefined); + + const createdUser = await getUser(source, user1); + + expect(createdUser.id).toBe(user1); + expect(createdUser.role).toBe(USER_ROLES.TYPIST); + expect(createdUser.author_id).toBeNull(); + expect(createdUser.auto_renew).toBe(false); + expect(createdUser.license_alert).toBe(false); + expect(createdUser.notification).toBe(false); + expect(createdUser.encryption).toBe(false); + expect(createdUser.encryption_password).toBeNull(); + expect(createdUser.prompt).toBe(false); + }); + + it('ユーザー情報を更新できる(Author)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId: external_id } = await createUser( + source, + accountId, + 'external_id', + USER_ROLES.NONE, + undefined, + true, + ); + const { userId: user1 } = await createUser( + source, + accountId, + 'external_id1', + USER_ROLES.AUTHOR, + undefined, + true, + true, + 'password', + true, + ); + + const service = module.get(UsersService); + + expect( + await service.updateUser( + { trackingId: 'trackingId' }, + external_id, + user1, + USER_ROLES.AUTHOR, + 'AUTHOR_ID', + false, + false, + false, + true, + 'new_password', + true, + ), + ).toEqual(undefined); + + const createdUser = await getUser(source, user1); + + expect(createdUser.id).toBe(user1); + expect(createdUser.role).toBe(USER_ROLES.AUTHOR); + expect(createdUser.author_id).toBe('AUTHOR_ID'); + expect(createdUser.auto_renew).toBe(false); + expect(createdUser.license_alert).toBe(false); + expect(createdUser.notification).toBe(false); + expect(createdUser.encryption).toBe(true); + expect(createdUser.encryption_password).toBe('new_password'); + expect(createdUser.prompt).toBe(true); + }); + + it('ユーザーのRoleを更新できる(None⇒Typist)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId: external_id } = await createUser( + source, + accountId, + 'external_id', + USER_ROLES.NONE, + undefined, + true, + ); + const { userId: user1 } = await createUser( + source, + accountId, + 'external_id1', + USER_ROLES.NONE, + undefined, + true, + ); + + const service = module.get(UsersService); + + expect( + await service.updateUser( + { trackingId: 'trackingId' }, + external_id, + user1, + USER_ROLES.TYPIST, + undefined, + false, + false, + false, + undefined, + undefined, + undefined, + ), + ).toEqual(undefined); + + const createdUser = await getUser(source, user1); + + expect(createdUser.id).toBe(user1); + expect(createdUser.role).toBe(USER_ROLES.TYPIST); + expect(createdUser.author_id).toBeNull(); + expect(createdUser.auto_renew).toBe(false); + expect(createdUser.license_alert).toBe(false); + expect(createdUser.notification).toBe(false); + expect(createdUser.encryption).toBe(false); + expect(createdUser.encryption_password).toBeNull(); + expect(createdUser.prompt).toBe(false); + }); + + it('ユーザーのRoleを更新できる(None⇒Author)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId: external_id } = await createUser( + source, + accountId, + 'external_id', + USER_ROLES.NONE, + undefined, + true, + ); + const { userId: user1 } = await createUser( + source, + accountId, + 'external_id1', + USER_ROLES.NONE, + undefined, + true, + ); + + const service = module.get(UsersService); + + expect( + await service.updateUser( + { trackingId: 'trackingId' }, + external_id, + user1, + USER_ROLES.AUTHOR, + 'AUTHOR_ID', + false, + false, + false, + false, + undefined, + false, + ), + ).toEqual(undefined); + + const createdUser = await getUser(source, user1); + + expect(createdUser.id).toBe(user1); + expect(createdUser.role).toBe(USER_ROLES.AUTHOR); + expect(createdUser.author_id).toBe('AUTHOR_ID'); + expect(createdUser.auto_renew).toBe(false); + expect(createdUser.license_alert).toBe(false); + expect(createdUser.notification).toBe(false); + expect(createdUser.encryption).toBe(false); + expect(createdUser.encryption_password).toBeNull(); + expect(createdUser.prompt).toBe(false); + }); + + it('None以外からRoleを変更した場合、エラーとなる(Typist⇒None)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId: external_id } = await createUser( + source, + accountId, + 'external_id', + USER_ROLES.NONE, + undefined, + true, + ); + const { userId: user1 } = await createUser( + source, + accountId, + 'external_id1', + USER_ROLES.TYPIST, + undefined, + true, + ); + + const service = module.get(UsersService); + + await expect( + service.updateUser( + { trackingId: 'trackingId' }, + external_id, + user1, + USER_ROLES.NONE, + undefined, + false, + false, + false, + undefined, + undefined, + undefined, + ), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010207'), HttpStatus.BAD_REQUEST), + ); + }); + + it('Authorがパスワードundefinedで渡されたとき、元のパスワードを維持する(Encryptionがtrue)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId: external_id } = await createUser( + source, + accountId, + 'external_id', + USER_ROLES.NONE, + undefined, + true, + ); + const { userId: user1 } = await createUser( + source, + accountId, + 'external_id1', + USER_ROLES.AUTHOR, + 'AUTHOR_ID', + true, + true, + 'password', + true, + ); + + const service = module.get(UsersService); + + expect( + await service.updateUser( + { trackingId: 'trackingId' }, + external_id, + user1, + USER_ROLES.AUTHOR, + 'AUTHOR_ID', + false, + false, + false, + true, + undefined, + true, + ), + ).toEqual(undefined); + + const createdUser = await getUser(source, user1); + + expect(createdUser.id).toBe(user1); + expect(createdUser.role).toBe(USER_ROLES.AUTHOR); + expect(createdUser.author_id).toBe('AUTHOR_ID'); + expect(createdUser.auto_renew).toBe(false); + expect(createdUser.license_alert).toBe(false); + expect(createdUser.notification).toBe(false); + expect(createdUser.encryption).toBe(true); + expect(createdUser.encryption_password).toBe('password'); + expect(createdUser.prompt).toBe(true); + }); + + it('Authorが暗号化なしで更新した場合、パスワードをNULLにする(Encryptionがfalse)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId: external_id } = await createUser( + source, + accountId, + 'external_id', + USER_ROLES.NONE, + undefined, + true, + ); + const { userId: user1 } = await createUser( + source, + accountId, + 'external_id1', + USER_ROLES.AUTHOR, + 'AUTHOR_ID', + true, + false, + 'password', + true, + ); + + const service = module.get(UsersService); + + expect( + await service.updateUser( + { trackingId: 'trackingId' }, + external_id, + user1, + USER_ROLES.AUTHOR, + 'AUTHOR_ID', + false, + false, + false, + false, + 'password', + true, + ), + ).toEqual(undefined); + + const createdUser = await getUser(source, user1); + + expect(createdUser.id).toBe(user1); + expect(createdUser.role).toBe(USER_ROLES.AUTHOR); + expect(createdUser.author_id).toBe('AUTHOR_ID'); + expect(createdUser.auto_renew).toBe(false); + expect(createdUser.license_alert).toBe(false); + expect(createdUser.notification).toBe(false); + expect(createdUser.encryption).toBe(false); + expect(createdUser.encryption_password).toBeNull(); + expect(createdUser.prompt).toBe(true); + }); + + it('AuthorのDBにパスワードが設定されていない場合、パスワードundefinedでわたすとエラーとなる(Encryptionがtrue)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId: external_id } = await createUser( + source, + accountId, + 'external_id', + USER_ROLES.NONE, + undefined, + true, + ); + const { userId: user1 } = await createUser( + source, + accountId, + 'external_id1', + USER_ROLES.AUTHOR, + 'AUTHOR_ID', + true, + true, + undefined, + true, + ); + + const service = module.get(UsersService); + + await expect( + service.updateUser( + { trackingId: 'trackingId' }, + external_id, + user1, + USER_ROLES.AUTHOR, + 'AUTHOR_ID', + false, + false, + false, + true, + undefined, + true, + ), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010208'), HttpStatus.BAD_REQUEST), + ); + }); + + it('AuthorIdが既存のユーザーと重複した場合、エラーとなる', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId: external_id } = await createUser( + source, + accountId, + 'external_id', + USER_ROLES.NONE, + undefined, + true, + ); + const { userId: user1 } = await createUser( + source, + accountId, + 'external_id1', + USER_ROLES.AUTHOR, + 'AUTHOR_ID1', + true, + true, + 'password', + true, + ); + + const { userId: user2 } = await createUser( + source, + accountId, + 'external_id2', + USER_ROLES.AUTHOR, + 'AUTHOR_ID2', + true, + true, + 'password', + true, + ); + + const service = module.get(UsersService); + + await expect( + service.updateUser( + { trackingId: 'trackingId' }, + external_id, + user1, + USER_ROLES.AUTHOR, + 'AUTHOR_ID2', + false, + false, + false, + true, + undefined, + true, + ), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010302'), HttpStatus.BAD_REQUEST), + ); + }); +}); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 6dcbeee..9ed982d 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -21,7 +21,13 @@ import { SortCriteriaRepositoryService } from '../../repositories/sort_criteria/ import { User as EntityUser } from '../../repositories/users/entity/user.entity'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; import { GetRelationsResponse, User } from './types/types'; -import { EmailAlreadyVerifiedError } from '../../repositories/users/errors/types'; +import { + AuthorIdAlreadyExistsError, + EmailAlreadyVerifiedError, + EncryptionPasswordNeedError, + InvalidRoleChangeError, + UserNotFoundError, +} from '../../repositories/users/errors/types'; import { LICENSE_EXPIRATION_THRESHOLD_DAYS, USER_LICENSE_STATUS, @@ -628,4 +634,103 @@ export class UsersService { ); } } + + /** + * 指定したユーザーの情報を更新します + * @param context + * @param extarnalId + * @param id + * @param role + * @param authorId + * @param autoRenew + * @param licenseAlart + * @param notification + * @param encryption + * @param encryptionPassword + * @param prompt + * @returns user + */ + async updateUser( + context: Context, + extarnalId: string, + id: number, + role: string, + authorId: string | undefined, + autoRenew: boolean, + licenseAlart: boolean, + notification: boolean, + encryption: boolean | undefined, + encryptionPassword: string | undefined, + prompt: boolean | undefined, + ): Promise { + try { + this.logger.log( + `[IN] [${context.trackingId}] ${this.updateUser.name} | params: { ` + + `role: ${role}, ` + + `authorId: ${authorId}, ` + + `autoRenew: ${autoRenew}, ` + + `licenseAlart: ${licenseAlart}, ` + + `notification: ${notification}, ` + + `encryption: ${encryption}, ` + + `encryptionPassword: ********, ` + + `prompt: ${prompt} }`, + ); + + // 実行ユーザーのアカウントIDを取得 + const accountId = ( + await this.usersRepository.findUserByExternalId(extarnalId) + ).account_id; + + // ユーザー情報を更新 + await this.usersRepository.update( + accountId, + id, + role, + authorId, + autoRenew, + licenseAlart, + notification, + encryption, + encryptionPassword, + prompt, + ); + } catch (e) { + this.logger.error(`error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case UserNotFoundError: + throw new HttpException( + makeErrorResponse('E010204'), + HttpStatus.BAD_REQUEST, + ); + case AuthorIdAlreadyExistsError: + throw new HttpException( + makeErrorResponse('E010302'), + HttpStatus.BAD_REQUEST, + ); + case InvalidRoleChangeError: + throw new HttpException( + makeErrorResponse('E010207'), + HttpStatus.BAD_REQUEST, + ); + case EncryptionPasswordNeedError: + throw new HttpException( + makeErrorResponse('E010208'), + 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.updateUser.name}`); + } + } } diff --git a/dictation_server/src/repositories/users/entity/user.entity.ts b/dictation_server/src/repositories/users/entity/user.entity.ts index f42123e..a08926e 100644 --- a/dictation_server/src/repositories/users/entity/user.entity.ts +++ b/dictation_server/src/repositories/users/entity/user.entity.ts @@ -48,6 +48,9 @@ export class User { @Column() encryption: boolean; + @Column({ nullable: true }) + encryption_password?: string; + @Column() prompt: boolean; diff --git a/dictation_server/src/repositories/users/errors/types.ts b/dictation_server/src/repositories/users/errors/types.ts index ddfdb91..6f78b9a 100644 --- a/dictation_server/src/repositories/users/errors/types.ts +++ b/dictation_server/src/repositories/users/errors/types.ts @@ -2,3 +2,9 @@ export class EmailAlreadyVerifiedError extends Error {} // ユーザー未発見エラー export class UserNotFoundError extends Error {} +// AuthorID重複エラー +export class AuthorIdAlreadyExistsError extends Error {} +// 不正なRole変更エラー +export class InvalidRoleChangeError extends Error {} +// 暗号化パスワード不足エラー +export class EncryptionPasswordNeedError 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 b37feb5..ef1d971 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -1,12 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { DataSource, IsNull, UpdateResult } from 'typeorm'; +import { DataSource, IsNull, Not, UpdateResult } from 'typeorm'; import { User } from './entity/user.entity'; import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity'; import { getDirection, getTaskListSortableAttribute, } from '../../common/types/sort/util'; -import { UserNotFoundError, EmailAlreadyVerifiedError } from './errors/types'; +import { + UserNotFoundError, + EmailAlreadyVerifiedError, + AuthorIdAlreadyExistsError, + InvalidRoleChangeError, + EncryptionPasswordNeedError, +} from './errors/types'; import { USER_ROLES } from '../../constants'; @Injectable() @@ -165,10 +171,87 @@ export class UsersRepositoryService { * @param user * @returns update */ - async update(user: User): Promise { + async update( + accountId: number, + id: number, + role: string, + authorId: string | undefined, + autoRenew: boolean, + licenseAlart: boolean, + notification: boolean, + encryption: boolean | undefined, + encryptionPassword: string | undefined, + prompt: boolean | undefined, + ): Promise { return await this.dataSource.transaction(async (entityManager) => { const repo = entityManager.getRepository(User); - return await repo.update({ id: user.id }, user); + + // 変更対象のユーザーを取得 + const targetUser = await repo.findOne({ + where: { id: id, account_id: accountId }, + }); + + // 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!targetUser) { + throw new UserNotFoundError(); + } + + // ユーザーのロールがNoneの場合以外はロールを変更できない + if (targetUser.role !== USER_ROLES.NONE && targetUser.role !== role) { + throw new InvalidRoleChangeError('Not None user cannot change role.'); + } + + if (role === USER_ROLES.AUTHOR) { + // ユーザーのロールがAuthorの場合はAuthorIDの重複チェックを行う + const authorIdDuplicatedUser = await repo.findOne({ + where: { account_id: accountId, id: Not(id), author_id: authorId }, + }); + + // 重複したAuthorIDがあった場合はエラー + if (authorIdDuplicatedUser) { + throw new AuthorIdAlreadyExistsError('authorId already exists.'); + } + + // 暗号化を有効にする場合はパスワードを設定する + if (encryption) { + // 暗号化パスワードが設定されている場合は更新する(undefinedの場合は変更なしとして元のパスワードを維持) + if (encryptionPassword) { + targetUser.encryption_password = encryptionPassword; + } else if (!targetUser.encryption_password) { + // DBにパスワードが設定されていない場合にはパスワードを設定しないとエラー + throw new EncryptionPasswordNeedError( + 'encryption_password need to set value.', + ); + } + } else { + targetUser.encryption_password = null; + } + + // Author用項目を更新 + targetUser.author_id = authorId; + targetUser.encryption = encryption; + targetUser.prompt = prompt; + } else { + // ユーザーのロールがAuthor以外の場合はAuthor用項目はundefinedにする + targetUser.author_id = null; + targetUser.encryption = false; + targetUser.encryption_password = null; + targetUser.prompt = false; + } + + // 共通項目を更新 + targetUser.role = role; + targetUser.auto_renew = autoRenew; + targetUser.license_alert = licenseAlart; + targetUser.notification = notification; + + const result = await repo.update({ id: id }, targetUser); + + // 想定外の更新が行われた場合はロールバックを行った上でエラー送出 + if (result.affected !== 1) { + throw new Error(`invalid update. result.affected=${result.affected}`); + } + return result; }); }