From 9739942bdf38a95ee6fd430722b5fc7cb89817db Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Tue, 18 Jul 2023 05:01:17 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20238:=20Revert=20'Revert=20'API?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88=E3=83=91=E3=83=BC=E3=83=88=E3=83=8A?= =?UTF-8?q?=E3=83=BC=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0API=EF=BC=89''?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2157: API実装(パートナーアカウント追加API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2157) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど パートナーアカウント追加APIを実装しました。 - このPull Requestでの対象/対象外 認証メール送信後のフローは既存機能のため対象外 - 影響範囲(他の機能にも影響があるか) 既存のaccounts.service.spec.tsのテスト ## レビューポイント - 特にレビューしてほしい箇所 エラー判定に過不足ないか ## UIの変更 なし ## 動作確認状況 - ローカルで確認 Azureに管理者ユーザが追加されたこと、認証メールが送信されてくることを確認。 (対象外だが、認証後に追加されたアカウントでログインできることを確認) ## 補足 - 相談、参考資料などがあれば 一度間違えてCompleteにしてしまったので、 Reverts !225 差分を戻すプルリクをCompleteにして出し直させていただいております。 Reverts !237 --- .../features/accounts/accounts.controller.ts | 21 ++-- .../accounts/accounts.service.spec.ts | 86 ++++++++++++++ .../src/features/accounts/accounts.service.ts | 105 +++++++++++++++++- .../accounts/test/accounts.service.mock.ts | 72 +++++++++++- .../src/features/accounts/types/types.ts | 2 +- 5 files changed, 273 insertions(+), 13 deletions(-) diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index b7d80c6..503a77e 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -261,21 +261,28 @@ export class AccountsController { @ApiOperation({ operationId: 'createPartnerAccount' }) @ApiBearerAuth() @UseGuards(AuthGuard) - @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + tiers: [TIERS.TIER1,TIERS.TIER2,TIERS.TIER3], + }), + ) async createPartnerAccount( @Req() req: Request, @Body() body: CreatePartnerAccountRequest, ): Promise { - console.log(req.header('Authorization')); - console.log(body); - const { companyName, country, eMail, adminName } = body; + const { companyName, country, email, adminName } = body; + const accessToken = retrieveAuthorizationToken(req); + const payload = jwt.decode(accessToken, { json: true }) as AccessToken; - /*await this.accountService.createPartnerAccount( + await this.accountService.createPartnerAccount( companyName, country, - eMail, + email, adminName, - );*/ + payload.userId, + payload.tier, + ); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index c315cc8..f3e5dc0 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -8,6 +8,7 @@ import { makeDefaultUserGroupsRepositoryMockValue, makeDefaultUsersRepositoryMockValue, } from './test/accounts.service.mock'; +import { makeDefaultConfigValue } from '../users/test/users.service.mock'; describe('AccountsService', () => { it('アカウントに紐づくライセンス情報を取得する', async () => { @@ -18,12 +19,14 @@ describe('AccountsService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const accountsRepositoryMockValue = makeDefaultAccountsRepositoryMockValue(); + const configMockValue = makeDefaultConfigValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeAccountsServiceMock( accountsRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cParam, + configMockValue, sendGridMockValue, ); expect(await service.getLicenseSummary(accountId)).toEqual( @@ -40,12 +43,14 @@ describe('AccountsService', () => { const accountsRepositoryMockValue = makeDefaultAccountsRepositoryMockValue(); accountsRepositoryMockValue.getLicenseSummaryInfo = null; + const configMockValue = makeDefaultConfigValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeAccountsServiceMock( accountsRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cParam, + configMockValue, sendGridMockValue, ); await expect(service.getLicenseSummary(accountId)).rejects.toEqual( @@ -64,12 +69,14 @@ describe('AccountsService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const accountsRepositoryMockValue = makeDefaultAccountsRepositoryMockValue(); + const configMockValue = makeDefaultConfigValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeAccountsServiceMock( accountsRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cParam, + configMockValue, sendGridMockValue, ); expect(await service.getTypists(externalId)).toEqual([ @@ -87,12 +94,14 @@ describe('AccountsService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const accountsRepositoryMockValue = makeDefaultAccountsRepositoryMockValue(); + const configMockValue = makeDefaultConfigValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeAccountsServiceMock( accountsRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cParam, + configMockValue, sendGridMockValue, ); await expect(service.getTypists(externalId)).rejects.toEqual( @@ -111,12 +120,14 @@ describe('AccountsService', () => { adb2cParam.getUsers = new Error(); const accountsRepositoryMockValue = makeDefaultAccountsRepositoryMockValue(); + const configMockValue = makeDefaultConfigValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeAccountsServiceMock( accountsRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cParam, + configMockValue, sendGridMockValue, ); await expect(service.getTypists(externalId)).rejects.toEqual( @@ -135,12 +146,14 @@ describe('AccountsService', () => { makeDefaultAccountsRepositoryMockValue(); const userGroupsRepositoryMockValue = makeDefaultUserGroupsRepositoryMockValue(); + const configMockValue = makeDefaultConfigValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeAccountsServiceMock( accountsRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cParam, + configMockValue, sendGridMockValue, ); @@ -158,12 +171,14 @@ describe('AccountsService', () => { makeDefaultAccountsRepositoryMockValue(); const userGroupsRepositoryMockValue = makeDefaultUserGroupsRepositoryMockValue(); + const configMockValue = makeDefaultConfigValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeAccountsServiceMock( accountsRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cParam, + configMockValue, sendGridMockValue, ); @@ -183,12 +198,14 @@ describe('AccountsService', () => { const userGroupsRepositoryMockValue = makeDefaultUserGroupsRepositoryMockValue(); userGroupsRepositoryMockValue.getUserGroups = new Error('DB failed'); + const configMockValue = makeDefaultConfigValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeAccountsServiceMock( accountsRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cParam, + configMockValue, sendGridMockValue, ); @@ -199,6 +216,75 @@ describe('AccountsService', () => { ), ); }); + it('パートナーを追加できる', async () => { + const companyName = "TEST_COMPANY"; + const country = "US"; + const email = "xxx@example.com"; + const adminName = "ADMIN"; + const userId = "100"; + const tier = 3; + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const userGroupsRepositoryMockValue = + makeDefaultUserGroupsRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + const accountsRepositoryMockValue = + makeDefaultAccountsRepositoryMockValue(); + const configMockValue = makeDefaultConfigValue(); + const sendGridMockValue = makeDefaultSendGridlValue(); + + const service = await makeAccountsServiceMock( + accountsRepositoryMockValue, + usersRepositoryMockValue, + userGroupsRepositoryMockValue, + adb2cParam, + configMockValue, + sendGridMockValue, + ); + expect(await service.createPartnerAccount(companyName, country,email,adminName,userId,tier +1)).toEqual( + undefined, + ); + }); + it('アカウントの追加に失敗した場合、エラーとなる', async () => { + const companyName = 'TEST_COMPANY'; + const country = 'US'; + const email = 'xxx@example.com'; + const adminName = 'ADMIN'; + const userId = '100'; + const tier = 3; + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const userGroupsRepositoryMockValue = + makeDefaultUserGroupsRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + const accountsRepositoryMockValue = + makeDefaultAccountsRepositoryMockValue(); + accountsRepositoryMockValue.createAccount = new Error('DB failed'); + const configMockValue = makeDefaultConfigValue(); + const sendGridMockValue = makeDefaultSendGridlValue(); + + const service = await makeAccountsServiceMock( + accountsRepositoryMockValue, + usersRepositoryMockValue, + userGroupsRepositoryMockValue, + adb2cParam, + configMockValue, + sendGridMockValue, + ); + await expect( + service.createPartnerAccount( + companyName, + country, + email, + adminName, + userId, + tier + 1, + ), + ).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); + }); }); const expectedAccountLisenceCounts = { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index bbf92cb..63c67bb 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -10,13 +10,14 @@ import { } from '../../gateways/adb2c/adb2c.service'; import { Account } from '../../repositories/accounts/entity/account.entity'; import { User } from '../../repositories/users/entity/user.entity'; -import { LICENSE_EXPIRATION_THRESHOLD_DAYS, TIERS } from '../../constants'; +import { LICENSE_EXPIRATION_THRESHOLD_DAYS, TIERS, USER_ROLES } from '../../constants'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { TypistGroup } from './types/types'; import { GetLicenseSummaryResponse, Typist } from './types/types'; import { AccessToken } from '../../common/token'; import { UserNotFoundError } from '../../repositories/users/errors/types'; import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service'; +import { makePassword } from '../../common/password'; @Injectable() export class AccountsService { @@ -288,4 +289,106 @@ export class AccountsService { this.logger.log(`[OUT] ${this.getTypists.name}`); } } + + /** + * パートナーを追加する + * @param companyName + * @param country + * @param email + * @param adminName + * @param userId + * @param tier + */ + async createPartnerAccount( + companyName: string, + country: string, + email: string, + adminName: string, + userId: string, + tier: number, + ): Promise { + this.logger.log(`[IN] ${this.createPartnerAccount.name}`); + + let myAccountId: number; + + try { + // アクセストークンからユーザーIDを取得する + myAccountId = (await this.usersRepository.findUserByExternalId(userId)).account_id; + } catch (e) { + this.logger.error(`error=${e}`); + if (e instanceof UserNotFoundError) { + throw new HttpException( + makeErrorResponse('E010204'), + HttpStatus.BAD_REQUEST, + ); + } else { + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + const ramdomPassword = makePassword(); + + let externalUser: { sub: string } | ConflictError; + + try { + // 管理者ユーザを作成し、AzureADB2C IDを取得する + externalUser = await this.adB2cService.createUser( + email, + ramdomPassword, + adminName, + ); + } catch (e) { + this.logger.error(`error=${e}`); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + // メールアドレスが重複していた場合はエラーを返す + if (isConflictError(externalUser)) { + throw new HttpException( + makeErrorResponse('E010301'), + HttpStatus.BAD_REQUEST, + ); + } + + try { + // アカウントと管理者をセットで作成 + const { newAccount, adminUser } = + await this.accountRepository.createAccount( + companyName, + country, + myAccountId, + tier + 1, + externalUser.sub, + USER_ROLES.NONE, + null, + ); + + const from = this.configService.get('MAIL_FROM') || ''; + const { subject, text, html } = + await this.sendgridService.createMailContentFromEmailConfirmForNormalUser( + newAccount.id, + adminUser.id, + email, + ); + await this.sendgridService.sendMail( + email, + from, + subject, + text, + html, + ); + } catch (e) { + this.logger.error(`error=${e}`); + this.logger.error('create partner account failed'); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/dictation_server/src/features/accounts/test/accounts.service.mock.ts b/dictation_server/src/features/accounts/test/accounts.service.mock.ts index 4100f78..b543f93 100644 --- a/dictation_server/src/features/accounts/test/accounts.service.mock.ts +++ b/dictation_server/src/features/accounts/test/accounts.service.mock.ts @@ -1,4 +1,4 @@ -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { User } from '../../../repositories/users/entity/user.entity'; import { UsersRepositoryService } from '../../../repositories/users/users.repository.service'; @@ -9,10 +9,11 @@ import { ConflictError, } from '../../../gateways/adb2c/adb2c.service'; import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service'; -import { LicenseSummaryInfo } from '../types/types'; +import { Account, LicenseSummaryInfo } from '../types/types'; import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity'; import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service'; import { AdB2cUser } from '../../../gateways/adb2c/types/types'; +import { AccountSASPermissions } from '@azure/storage-blob'; export type UsersRepositoryMockValue = { findUserById: User | Error; findUserByExternalId: User | Error; @@ -31,19 +32,29 @@ export type SendGridMockValue = { text: string; html: string; }; + createMailContentFromEmailConfirmForNormalUser: { + subject: string; + text: string; + html: string; + }; sendMail: undefined | Error; }; +export type ConfigMockValue = { + get: string | Error; +}; export type AccountsRepositoryMockValue = { getLicenseSummaryInfo: { licenseSummary: LicenseSummaryInfo; isStorageAvailable: boolean; }; + createAccount: { newAccount: Account; adminUser: User } | Error; }; export const makeAccountsServiceMock = async ( accountsRepositoryMockValue: AccountsRepositoryMockValue, usersRepositoryMockValue: UsersRepositoryMockValue, userGroupsRepositoryMockValue: UserGroupsRepositoryMockValue, adB2cMockValue: AdB2cMockValue, + configMockValue: ConfigMockValue, sendGridMockValue: SendGridMockValue, ): Promise => { const module: TestingModule = await Test.createTestingModule({ @@ -65,6 +76,8 @@ export const makeAccountsServiceMock = async ( return makeUserGroupsRepositoryMock(userGroupsRepositoryMockValue); case AdB2cService: return makeAdB2cServiceMock(adB2cMockValue); + case ConfigService: + return makeConfigMock(configMockValue); case SendGridService: return makeSendGridServiceMock(sendGridMockValue); } @@ -77,7 +90,7 @@ export const makeAccountsServiceMock = async ( export const makeAccountsRepositoryMock = ( value: AccountsRepositoryMockValue, ) => { - const { getLicenseSummaryInfo } = value; + const { getLicenseSummaryInfo, createAccount } = value; return { getLicenseSummaryInfo: getLicenseSummaryInfo instanceof Error @@ -91,6 +104,12 @@ export const makeAccountsRepositoryMock = ( [] >() .mockResolvedValue(getLicenseSummaryInfo), + createAccount: + createAccount instanceof Error + ? jest.fn, []>().mockRejectedValue(createAccount) + : jest + .fn, []>() + .mockResolvedValue(createAccount), }; }; export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => { @@ -139,8 +158,18 @@ export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { : jest.fn, []>().mockResolvedValue(getUsers), }; }; +export const makeConfigMock = (value: ConfigMockValue) => { + const { get } = value; + + return { + get: + get instanceof Error + ? jest.fn, []>().mockRejectedValue(get) + : jest.fn, []>().mockResolvedValue(get), + }; +}; export const makeSendGridServiceMock = (value: SendGridMockValue) => { - const { createMailContentFromEmailConfirm, sendMail } = value; + const { createMailContentFromEmailConfirm,createMailContentFromEmailConfirmForNormalUser, sendMail } = value; return { createMailContentFromEmailConfirm: createMailContentFromEmailConfirm instanceof Error @@ -150,6 +179,14 @@ export const makeSendGridServiceMock = (value: SendGridMockValue) => { : jest .fn, []>() .mockResolvedValue(createMailContentFromEmailConfirm), + createMailContentFromEmailConfirmForNormalUser: + createMailContentFromEmailConfirmForNormalUser instanceof Error + ? jest + .fn, []>() + .mockRejectedValue(createMailContentFromEmailConfirmForNormalUser) + : jest + .fn, []>() + .mockResolvedValue(createMailContentFromEmailConfirmForNormalUser), sendMail: sendMail instanceof Error ? jest.fn, []>().mockRejectedValue(sendMail) @@ -170,11 +207,33 @@ export const makeDefaultAccountsRepositoryMockValue = numberOfRequesting: 7, allocatableLicenseWithMargin: 8, }; + const account = new Account(); + account.accountId = 1; + const user = new User(); + user.id = 1; + user.external_id = 'ede66c43-9b9d-4222-93ed-5f11c96e08e2'; + user.account_id = 1234567890123456; + user.role = 'none admin'; + user.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9'; + user.accepted_terms_version = '1.0'; + user.email_verified = true; + user.auto_renew = false; + user.license_alert = false; + user.notification = false; + user.deleted_at = null; + user.created_by = 'test'; + user.created_at = new Date(); + user.updated_by = null; + user.updated_at = null; return { getLicenseSummaryInfo: { licenseSummary: licenseSummaryInfo, isStorageAvailable: false, }, + createAccount: { + newAccount: account, + adminUser: user, + }, }; }; export const makeDefaultUsersRepositoryMockValue = @@ -286,5 +345,10 @@ export const makeDefaultSendGridlValue = (): SendGridMockValue => { return { sendMail: undefined, createMailContentFromEmailConfirm: { subject: '', text: '', html: '' }, + createMailContentFromEmailConfirmForNormalUser: { + subject: 'Verify your new account', + text: `The verification URL.`, + html: `

The verification URL.

`, + }, }; }; diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 416d73a..f33d0a7 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -131,7 +131,7 @@ export class CreatePartnerAccountRequest { adminName: string; @ApiProperty() @IsEmail() - eMail: string; + email: string; } export class CreatePartnerAccountResponse {} \ No newline at end of file