Merged PR 323: アカウント登録APIを修正

## 概要
[Task2353: アカウント登録APIを修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2353)

- アカウント登録時にコンテナを作成するように修正
- ログ追加
- リクエストのバリデータを追加

## レビューポイント
- 処理の流れに問題はないか
- テストケースに不足はないか
- バリデータに問題はないか

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-08-16 09:12:56 +00:00
parent ba81bc5cb8
commit ad969bd2cf
17 changed files with 483 additions and 109 deletions

View File

@ -1,7 +1,7 @@
import { Context } from './types';
export const makeContext = (externaiId: string): Context => {
export const makeContext = (externalId: string): Context => {
return {
trackingId: externaiId,
trackingId: externalId,
};
};

View File

@ -1,3 +1,4 @@
import { Context } from '../log';
import {
AdB2cService,
ConflictError,
@ -5,6 +6,7 @@ import {
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { User, newUser } from '../../repositories/users/entity/user.entity';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
// ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ###
@ -18,6 +20,7 @@ export const overrideAdB2cService = <TService>(
service: TService,
overrides: {
createUser?: (
context: Context,
email: string,
password: string,
username: string,
@ -44,6 +47,7 @@ export const overrideSendgridService = <TService>(
service: TService,
overrides: {
createMailContentFromEmailConfirm?: (
context: Context,
accountId: number,
userId: number,
email: string,
@ -54,6 +58,7 @@ export const overrideSendgridService = <TService>(
email: string,
) => Promise<{ subject: string; text: string; html: string }>;
sendMail?: (
context: Context,
to: string,
from: string,
subject: string,
@ -121,3 +126,29 @@ export const overrideUsersRepositoryService = <TService>(
});
}
};
/**
* blobStorageServiceのモックを作成してTServiceが依存するサービス(blobStorageService)
* serviceに指定するオブジェクトは`blobstorageService: blobStorageService`
* @param service TService
* @param overrides blobStorageServiceの各種メソッドのモックが返す値
*/
export const overrideBlobstorageService = <TService>(
service: TService,
overrides: {
createContainer?: (
context: Context,
accountId: number,
country: string,
) => Promise<void>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
const obj = (service as any).blobStorageService as BlobstorageService;
if (overrides.createContainer) {
Object.defineProperty(obj, obj.createContainer.name, {
value: overrides.createContainer,
writable: true,
});
}
};

View File

@ -0,0 +1,31 @@
import { registerDecorator, ValidationOptions } from 'class-validator';
export const IsAdminPasswordvalid = (validationOptions?: ValidationOptions) => {
return (object: any, propertyName: string) => {
registerDecorator({
name: 'IsAdminPasswordvalid',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
validate: (value: string) => {
// 8文字64文字でなければ早期に不合格
const minLength = 8;
const maxLength = 64;
if (value.length < minLength || value.length > maxLength) {
return false;
}
// 英字の大文字、英字の小文字、アラビア数字、記号(@#$%^&*\-_+=[]{}|\:',.?/`~"();!から2種類以上組み合わせ
const charaTypePattern =
/^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[\d])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]))[a-zA-Z\d@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]/;
return new RegExp(charaTypePattern).test(value);
},
defaultMessage: () => {
return 'Admin password rule not satisfied';
},
},
});
};
};

View File

@ -41,6 +41,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper';
import { AccessToken } from '../../common/token';
import jwt from 'jsonwebtoken';
import { makeContext } from '../../common/log';
import { v4 as uuidv4 } from 'uuid';
@ApiTags('accounts')
@Controller('accounts')
@ -80,7 +81,10 @@ export class AccountsController {
} = body;
const role = USER_ROLES.NONE;
const context = makeContext(uuidv4());
await this.accountService.createAccount(
context,
companyName,
country,
dealerAccountId,
@ -277,7 +281,10 @@ export class AccountsController {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(payload.userId);
await this.accountService.createPartnerAccount(
context,
companyName,
country,
email,

View File

@ -7,6 +7,7 @@ import { AccountsController } from './accounts.controller';
import { AccountsService } from './accounts.service';
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module';
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
@Module({
imports: [
@ -16,6 +17,7 @@ import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_
UserGroupsRepositoryModule,
SendGridModule,
AdB2cModule,
BlobstorageModule,
],
controllers: [AccountsController],
providers: [AccountsService],

View File

@ -2,6 +2,7 @@ import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
makeAccountsServiceMock,
makeBlobStorageServiceMockValue,
makeDefaultAccountsRepositoryMockValue,
makeDefaultAdB2cMockValue,
makeDefaultLicensesRepositoryMockValue,
@ -29,6 +30,7 @@ import { TIERS } from '../../constants';
import { License } from '../../repositories/licenses/entity/license.entity';
import {
overrideAdB2cService,
overrideBlobstorageService,
overrideSendgridService,
} from '../../common/test/overrides';
@ -61,7 +63,7 @@ describe('createAccount', () => {
const email = 'dummy@dummy.dummy';
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'admin none';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
overrideAdB2cService(service, {
@ -69,6 +71,7 @@ describe('createAccount', () => {
return { sub: externalId };
},
});
overrideSendgridService(service, {
sendMail: async () => {
return;
@ -77,8 +80,12 @@ describe('createAccount', () => {
return { html: '', text: '', subject: '' };
},
});
overrideBlobstorageService(service, {
createContainer: async () => {},
});
const { accountId, externalUserId, userId } = await service.createAccount(
makeContext('uuid'),
companyName,
country,
dealerAccountId,
@ -135,8 +142,17 @@ describe('createAccount', () => {
const role = 'admin none';
const acceptedTermsVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
throw new Error();
},
});
overrideSendgridService(service, {});
overrideBlobstorageService(service, {});
try {
await service.createAccount(
makeContext('uuid'),
companyName,
country,
dealerAccountId,
@ -191,8 +207,18 @@ describe('createAccount', () => {
const role = 'admin none';
const acceptedTermsVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
// EmailのConflictエラーを返す
return { reason: 'email', message: 'dummy' };
},
});
overrideSendgridService(service, {});
overrideBlobstorageService(service, {});
try {
await service.createAccount(
makeContext('uuid'),
companyName,
country,
dealerAccountId,
@ -217,6 +243,54 @@ describe('createAccount', () => {
const users = await getUsers(source);
expect(users.length).toBe(0);
});
it('アカウントを作成がBlobStorageへの通信失敗によって失敗すると500エラーが発生する', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const externalId = 'test_external_id';
const companyName = 'test_company_name';
const country = 'US';
const dealerAccountId = 1;
const email = 'dummy@dummy.dummy';
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
return { sub: externalId };
},
});
overrideSendgridService(service, {});
overrideBlobstorageService(service, {
createContainer: async () => {
throw new Error();
},
});
try {
await service.createAccount(
makeContext('uuid'),
companyName,
country,
dealerAccountId,
email,
password,
username,
role,
acceptedTermsVersion,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
expect(true).toBe(false); // ここには来てはいけない
}
}
});
});
describe('AccountsService', () => {
@ -230,6 +304,7 @@ describe('AccountsService', () => {
makeDefaultAccountsRepositoryMockValue();
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeAccountsServiceMock(
@ -239,6 +314,7 @@ describe('AccountsService', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
expect(await service.getLicenseSummary(accountId)).toEqual(
@ -257,6 +333,7 @@ describe('AccountsService', () => {
accountsRepositoryMockValue.getLicenseSummaryInfo = null;
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeAccountsServiceMock(
@ -266,6 +343,7 @@ describe('AccountsService', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
await expect(service.getLicenseSummary(accountId)).rejects.toEqual(
@ -286,6 +364,7 @@ describe('AccountsService', () => {
makeDefaultAccountsRepositoryMockValue();
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeAccountsServiceMock(
@ -295,6 +374,7 @@ describe('AccountsService', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
expect(await service.getTypists(externalId)).toEqual([
@ -314,6 +394,7 @@ describe('AccountsService', () => {
makeDefaultAccountsRepositoryMockValue();
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeAccountsServiceMock(
@ -323,6 +404,7 @@ describe('AccountsService', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
await expect(service.getTypists(externalId)).rejects.toEqual(
@ -343,6 +425,7 @@ describe('AccountsService', () => {
makeDefaultAccountsRepositoryMockValue();
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeAccountsServiceMock(
@ -352,6 +435,7 @@ describe('AccountsService', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
await expect(service.getTypists(externalId)).rejects.toEqual(
@ -372,6 +456,7 @@ describe('AccountsService', () => {
makeDefaultUserGroupsRepositoryMockValue();
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeAccountsServiceMock(
@ -381,6 +466,7 @@ describe('AccountsService', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
@ -400,6 +486,7 @@ describe('AccountsService', () => {
makeDefaultUserGroupsRepositoryMockValue();
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeAccountsServiceMock(
@ -409,6 +496,7 @@ describe('AccountsService', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
@ -430,6 +518,7 @@ describe('AccountsService', () => {
userGroupsRepositoryMockValue.getUserGroups = new Error('DB failed');
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeAccountsServiceMock(
@ -439,6 +528,7 @@ describe('AccountsService', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
@ -464,6 +554,7 @@ describe('AccountsService', () => {
makeDefaultAccountsRepositoryMockValue();
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeAccountsServiceMock(
@ -473,10 +564,12 @@ describe('AccountsService', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
expect(
await service.createPartnerAccount(
makeContext('uuid'),
companyName,
country,
email,
@ -502,6 +595,7 @@ describe('AccountsService', () => {
accountsRepositoryMockValue.createAccount = new Error('DB failed');
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeAccountsServiceMock(
@ -511,10 +605,12 @@ describe('AccountsService', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
await expect(
service.createPartnerAccount(
makeContext('external_id'),
companyName,
country,
email,
@ -867,6 +963,7 @@ describe('getOrderHistories', () => {
makeDefaultAccountsRepositoryMockValue();
const configMockValue = makeDefaultConfigValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const blobStorageMockValue = makeBlobStorageServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
licensesRepositoryMockValue.getLicenseOrderHistoryInfo = new Error(
@ -879,6 +976,7 @@ describe('getOrderHistories', () => {
adb2cParam,
configMockValue,
sendGridMockValue,
blobStorageMockValue,
licensesRepositoryMockValue,
);
await expect(
@ -1145,3 +1243,9 @@ describe('getDealers', () => {
});
});
});
describe('createAccount', () => {
it('新規にアカウントを作成できる', async () => {
expect(1).toBe(1);
});
});

View File

@ -39,6 +39,8 @@ import {
AlreadyIssuedError,
OrderNotFoundError,
} from '../../repositories/licenses/errors/types';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
@Injectable()
export class AccountsService {
constructor(
@ -48,6 +50,7 @@ export class AccountsService {
private readonly userGroupsRepository: UserGroupsRepositoryService,
private readonly adB2cService: AdB2cService,
private readonly sendgridService: SendGridService,
private readonly blobStorageService: BlobstorageService,
private readonly configService: ConfigService,
) {}
private readonly logger = new Logger(AccountsService.name);
@ -119,6 +122,7 @@ export class AccountsService {
* @returns account
*/
async createAccount(
context: Context,
companyName: string,
country: string,
dealerAccountId: number | null,
@ -128,91 +132,131 @@ export class AccountsService {
role: string,
acceptedTermsVersion: string,
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
let externalUser: { sub: string } | ConflictError;
this.logger.log(`[IN] [${context.trackingId}] ${this.createAccount.name}`);
try {
// idpにユーザーを作成
externalUser = await this.adB2cService.createUser(
email,
password,
username,
);
} catch (e) {
console.log(e);
console.log('create externalUser failed');
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// メールアドレス重複エラー
if (isConflictError(externalUser)) {
throw new HttpException(
makeErrorResponse('E010301'),
HttpStatus.BAD_REQUEST,
);
}
let account: Account;
let user: User;
try {
// アカウントと管理者をセットで作成
const { newAccount, adminUser } =
await this.accountRepository.createAccount(
companyName,
country,
dealerAccountId,
TIERS.TIER5,
externalUser.sub,
role,
acceptedTermsVersion,
);
account = newAccount;
user = adminUser;
} catch (e) {
console.log('create account failed');
console.log(
`[NOT IMPLEMENT] [RECOVER] delete account: ${externalUser.sub}`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// メールの送信元を取得
const from = this.configService.get<string>('MAIL_FROM') ?? '';
// メールの内容を構成
const { subject, text, html } =
await this.sendgridService.createMailContentFromEmailConfirm(
account.id,
user.id,
let externalUser: { sub: string } | ConflictError;
try {
// idpにユーザーを作成
externalUser = await this.adB2cService.createUser(
context,
email,
password,
username,
);
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('create externalUser failed');
// メールを送信
await this.sendgridService.sendMail(email, from, subject, text, html);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// メールアドレス重複エラー
if (isConflictError(externalUser)) {
this.logger.error(`email conflict. externalUser: ${externalUser}`);
throw new HttpException(
makeErrorResponse('E010301'),
HttpStatus.BAD_REQUEST,
);
}
let account: Account;
let user: User;
try {
// アカウントと管理者をセットで作成
const { newAccount, adminUser } =
await this.accountRepository.createAccount(
companyName,
country,
dealerAccountId,
TIERS.TIER5,
externalUser.sub,
role,
acceptedTermsVersion,
);
account = newAccount;
user = adminUser;
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('create account failed');
this.logger.error(
`[NOT IMPLEMENT] [RECOVER] delete account: ${externalUser.sub}`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// 新規作成アカウント用のBlobコンテナを作成
try {
await this.blobStorageService.createContainer(
context,
account.id,
country,
);
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('create container failed');
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// メールの送信元を取得
const from = this.configService.get<string>('MAIL_FROM') ?? '';
// メールの内容を構成
const { subject, text, html } =
await this.sendgridService.createMailContentFromEmailConfirm(
context,
account.id,
user.id,
email,
);
// メールを送信
await this.sendgridService.sendMail(
context,
email,
from,
subject,
text,
html,
);
} catch (e) {
console.log(e);
this.logger.error(`error=${e}`);
this.logger.error('create user failed');
this.logger.error(
`[NOT IMPLEMENT] [RECOVER] delete account: ${account.id}`,
);
this.logger.error(
`[NOT IMPLEMENT] [RECOVER] delete externalUser: ${externalUser.sub}`,
);
this.logger.error(`[NOT IMPLEMENT] [RECOVER] delete user: ${user.id}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
return {
accountId: account.id,
userId: user.id,
externalUserId: user.external_id,
};
} catch (e) {
console.log('create user failed');
console.log(`[NOT IMPLEMENT] [RECOVER] delete account: ${account.id}`);
console.log(
`[NOT IMPLEMENT] [RECOVER] delete externalUser: ${externalUser.sub}`,
);
console.log(`[NOT IMPLEMENT] [RECOVER] delete user: ${user.id}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
throw e;
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.createAccount.name}`,
);
}
return {
accountId: account.id,
userId: user.id,
externalUserId: user.external_id,
};
}
/**
@ -344,6 +388,7 @@ export class AccountsService {
* @param tier
*/
async createPartnerAccount(
context: Context,
companyName: string,
country: string,
email: string,
@ -351,7 +396,9 @@ export class AccountsService {
userId: string,
tier: number,
): Promise<void> {
this.logger.log(`[IN] ${this.createPartnerAccount.name}`);
this.logger.log(
`[IN] [${context.trackingId}] ${this.createPartnerAccount.name}`,
);
let myAccountId: number;
@ -381,6 +428,7 @@ export class AccountsService {
try {
// 管理者ユーザを作成し、AzureADB2C IDを取得する
externalUser = await this.adB2cService.createUser(
context,
email,
ramdomPassword,
adminName,
@ -420,7 +468,14 @@ export class AccountsService {
adminUser.id,
email,
);
await this.sendgridService.sendMail(email, from, subject, text, html);
await this.sendgridService.sendMail(
context,
email,
from,
subject,
text,
html,
);
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('create partner account failed');

View File

@ -15,6 +15,7 @@ import { UserGroupsRepositoryService } from '../../../repositories/user_groups/u
import { AdB2cUser } from '../../../gateways/adb2c/types/types';
import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service';
import { Context } from '../../../common/log';
import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service';
export type LicensesRepositoryMockValue = {
getLicenseOrderHistoryInfo:
@ -60,6 +61,15 @@ export type AccountsRepositoryMockValue = {
};
createAccount: { newAccount: Account; adminUser: User } | Error;
};
export type BlobStorageServiceMockValue = {
createContainer: void | Error;
containerExists: boolean | Error;
fileExists: boolean | Error;
publishUploadSas: string | Error;
publishDownloadSas: string | Error;
};
export const makeAccountsServiceMock = async (
accountsRepositoryMockValue: AccountsRepositoryMockValue,
usersRepositoryMockValue: UsersRepositoryMockValue,
@ -67,6 +77,7 @@ export const makeAccountsServiceMock = async (
adB2cMockValue: AdB2cMockValue,
configMockValue: ConfigMockValue,
sendGridMockValue: SendGridMockValue,
blobStorageMockValue: BlobStorageServiceMockValue,
licensesRepositoryMockValue: LicensesRepositoryMockValue,
): Promise<AccountsService> => {
const module: TestingModule = await Test.createTestingModule({
@ -92,6 +103,8 @@ export const makeAccountsServiceMock = async (
return makeConfigMock(configMockValue);
case SendGridService:
return makeSendGridServiceMock(sendGridMockValue);
case BlobstorageService:
return makeBlobStorageServiceMock(blobStorageMockValue);
case LicensesRepositoryService:
return makeLicensesRepositoryMock(licensesRepositoryMockValue);
}
@ -245,6 +258,42 @@ export const makeSendGridServiceMock = (value: SendGridMockValue) => {
: jest.fn<Promise<void>, []>().mockResolvedValue(sendMail),
};
};
export const makeBlobStorageServiceMock = (
value: BlobStorageServiceMockValue,
) => {
const {
containerExists,
fileExists,
createContainer,
publishUploadSas,
publishDownloadSas,
} = value;
return {
containerExists:
containerExists instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(containerExists)
: jest.fn<Promise<boolean>, []>().mockResolvedValue(containerExists),
fileExists:
fileExists instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(fileExists)
: jest.fn<Promise<boolean>, []>().mockResolvedValue(fileExists),
createContainer:
createContainer instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(createContainer)
: jest.fn<Promise<void>, []>().mockResolvedValue(createContainer),
publishUploadSas:
publishUploadSas instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(publishUploadSas)
: jest.fn<Promise<string>, []>().mockResolvedValue(publishUploadSas),
publishDownloadSas:
publishDownloadSas instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(publishDownloadSas)
: jest.fn<Promise<string>, []>().mockResolvedValue(publishDownloadSas),
};
};
// 個別のテストケースに対応してそれぞれのMockを用意するのは無駄が多いのでテストケース内で個別の値を設定する
export const makeDefaultAccountsRepositoryMockValue =
(): AccountsRepositoryMockValue => {
@ -422,3 +471,14 @@ export const makeDefaultLicensesRepositoryMockValue =
issueLicense: undefined,
};
};
export const makeBlobStorageServiceMockValue =
(): BlobStorageServiceMockValue => {
return {
containerExists: true,
fileExists: true,
publishUploadSas: 'https://blob-storage?sas-token',
publishDownloadSas: 'https://blob-storage?sas-token',
createContainer: undefined,
};
};

View File

@ -1,10 +1,10 @@
import { DataSource } from 'typeorm';
import { User } from '../../../repositories/users/entity/user.entity';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import {
License,
LicenseOrder,
} from '../../../repositories/licenses/entity/license.entity';
import { User } from '../../../repositories/users/entity/user.entity';
export const createAccount = async (
datasource: DataSource,

View File

@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsInt, IsOptional, Matches, Min } from 'class-validator';
import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator';
export class CreateAccountRequest {
@ApiProperty()
@ -20,6 +21,7 @@ export class CreateAccountRequest {
@IsEmail()
adminMail: string;
@ApiProperty()
@IsAdminPasswordvalid()
adminPassword: string;
@ApiProperty({ description: '同意済み利用規約のバージョン' })
acceptedTermsVersion: string;

View File

@ -237,10 +237,6 @@ export class FilesService {
accountId,
country,
);
//TODO [Task2241] コンテナが無ければ作成しているが、アカウント登録時に作成するので本処理は削除予定。
if (!isContainerExist) {
await this.blobStorageService.createContainer(accountId, country);
}
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.log(

View File

@ -47,6 +47,7 @@ import { ADMIN_ROLES, TIERS } from '../../constants';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { makeContext } from '../../common/log';
import { UserRoles } from '../../common/types/role';
import { v4 as uuidv4 } from 'uuid';
@ApiTags('users')
@Controller('users')
@ -95,8 +96,8 @@ export class UsersController {
async confirmUserAndInitPassword(
@Body() body: ConfirmRequest,
): Promise<ConfirmResponse> {
console.log(body);
await this.usersService.confirmUserAndInitPassword(body.token);
const context = makeContext(uuidv4());
await this.usersService.confirmUserAndInitPassword(context, body.token);
return {};
}
@ -173,8 +174,11 @@ export class UsersController {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(payload.userId);
//ユーザ作成処理
await this.usersService.createUser(
context,
payload,
name,
role as UserRoles,

View File

@ -33,6 +33,7 @@ import {
USER_ROLES,
} from '../../constants';
import { makeTestingModule } from '../../common/test/modules';
import { Context, makeContext } from '../../common/log';
import {
overrideAdB2cService,
overrideSendgridService,
@ -219,7 +220,12 @@ describe('UsersService.confirmUserAndInitPassword', () => {
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
expect(await service.confirmUserAndInitPassword(token)).toEqual(undefined);
expect(
await service.confirmUserAndInitPassword(
makeContext('trackingId'),
token,
),
).toEqual(undefined);
});
it('トークンの形式が不正な場合、形式不正エラーとなる。(メール認証API)', async () => {
@ -256,7 +262,9 @@ describe('UsersService.confirmUserAndInitPassword', () => {
sortCriteriaRepositoryMockValue,
);
const token = 'invalid.id.token';
await expect(service.confirmUserAndInitPassword(token)).rejects.toEqual(
await expect(
service.confirmUserAndInitPassword(makeContext('trackingId'), token),
).rejects.toEqual(
new HttpException(makeErrorResponse('E000101'), HttpStatus.BAD_REQUEST),
);
});
@ -298,7 +306,9 @@ describe('UsersService.confirmUserAndInitPassword', () => {
);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
await expect(service.confirmUserAndInitPassword(token)).rejects.toEqual(
await expect(
service.confirmUserAndInitPassword(makeContext('trackingId'), token),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST),
);
});
@ -338,7 +348,9 @@ describe('UsersService.confirmUserAndInitPassword', () => {
);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
await expect(service.confirmUserAndInitPassword(token)).rejects.toEqual(
await expect(
service.confirmUserAndInitPassword(makeContext('trackingId'), token),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
@ -392,6 +404,7 @@ describe('UsersService.createUser', () => {
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
@ -414,6 +427,7 @@ describe('UsersService.createUser', () => {
expect(
await service.createUser(
makeContext('trackingId'),
token,
name,
role,
@ -474,6 +488,7 @@ describe('UsersService.createUser', () => {
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
@ -496,6 +511,7 @@ describe('UsersService.createUser', () => {
expect(
await service.createUser(
makeContext('trackingId'),
token,
name,
role,
@ -559,6 +575,7 @@ describe('UsersService.createUser', () => {
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
@ -581,6 +598,7 @@ describe('UsersService.createUser', () => {
expect(
await service.createUser(
makeContext('trackingId'),
token,
name,
role,
@ -641,6 +659,7 @@ describe('UsersService.createUser', () => {
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
@ -663,6 +682,7 @@ describe('UsersService.createUser', () => {
expect(
await service.createUser(
makeContext('trackingId'),
token,
name,
role,
@ -718,6 +738,7 @@ describe('UsersService.createUser', () => {
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
@ -747,6 +768,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
name,
role,
@ -789,6 +811,7 @@ describe('UsersService.createUser', () => {
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
@ -811,6 +834,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
name,
role,
@ -857,6 +881,7 @@ describe('UsersService.createUser', () => {
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
@ -883,6 +908,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
name,
role,
@ -935,6 +961,7 @@ describe('UsersService.createUser', () => {
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
@ -957,6 +984,7 @@ describe('UsersService.createUser', () => {
expect(
await service.createUser(
makeContext('trackingId'),
token,
name,
role,
@ -982,6 +1010,7 @@ describe('UsersService.createUser', () => {
const externalId_2 = '0001';
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
@ -996,6 +1025,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
name,
role,
@ -1054,6 +1084,7 @@ describe('UsersService.createUser', () => {
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
@ -1083,6 +1114,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
name,
role,

View File

@ -123,6 +123,7 @@ export class UsersService {
* @returns void
*/
async createUser(
context: Context,
accessToken: AccessToken,
name: string,
role: UserRoles,
@ -135,7 +136,7 @@ export class UsersService {
encryptionPassword?: string | undefined,
prompt?: boolean | undefined,
): Promise<void> {
this.logger.log(`[IN] ${this.createUser.name}`);
this.logger.log(`[IN] [${context.trackingId}] ${this.createUser.name}`);
//DBよりアクセス者の所属するアカウントIDを取得する
let adminUser: EntityUser;
@ -184,6 +185,7 @@ export class UsersService {
try {
// idpにユーザーを作成
externalUser = await this.adB2cService.createUser(
context,
email,
ramdomPassword,
name,
@ -256,7 +258,14 @@ export class UsersService {
);
//SendGridAPIを呼び出してメールを送信する
await this.sendgridService.sendMail(email, from, subject, text, html);
await this.sendgridService.sendMail(
context,
email,
from,
subject,
text,
html,
);
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('create user failed');
@ -321,8 +330,13 @@ export class UsersService {
* confirm User And Init Password
* @param token
*/
async confirmUserAndInitPassword(token: string): Promise<void> {
this.logger.log(`[IN] ${this.confirmUserAndInitPassword.name}`);
async confirmUserAndInitPassword(
context: Context,
token: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.confirmUserAndInitPassword.name}`,
);
const pubKey = getPublicKey(this.configService);
@ -359,7 +373,14 @@ export class UsersService {
const html = `<p>OMDS TOP PAGE URL.<p><a href="${domains}">${domains}"</a><br>temporary password: ${ramdomPassword}`;
// メールを送信
await this.sendgridService.sendMail(email, from, subject, text, html);
await this.sendgridService.sendMail(
context,
email,
from,
subject,
text,
html,
);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {

View File

@ -56,11 +56,12 @@ export class AdB2cService {
* @returns user
*/
async createUser(
context: Context,
email: string,
password: string,
username: string,
): Promise<{ sub: string } | ConflictError> {
this.logger.log(`[IN] ${this.createUser.name}`);
this.logger.log(`[IN] [${context.trackingId}] ${this.createUser.name}`);
try {
// ユーザをADB2Cに登録
const newUser = await this.graphClient.api('users/').post({
@ -93,7 +94,7 @@ export class AdB2cService {
throw e;
} finally {
this.logger.log(`[OUT] ${this.createUser.name}`);
this.logger.log(`[OUT] [${context.trackingId}] ${this.createUser.name}`);
}
}

View File

@ -51,13 +51,22 @@ export class BlobstorageService {
this.sharedKeyCredentialEU,
);
}
/**
* Creates container
* @param companyName
* @param context
* @param accountId
* @param country
* @returns container
*/
async createContainer(accountId: number, country: string): Promise<void> {
this.logger.log(`[IN] ${this.createContainer.name}`);
async createContainer(
context: Context,
accountId: number,
country: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.createContainer.name}`,
);
// 国に応じたリージョンでコンテナ名を指定してClientを取得
const containerClient = this.getContainerClient(accountId, country);
@ -69,7 +78,9 @@ export class BlobstorageService {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] ${this.createContainer.name}`);
this.logger.log(
`[OUT] [${context.trackingId}] ${this.createContainer.name}`,
);
}
}
/**

View File

@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config';
import { sign } from '../../common/jwt';
import sendgrid from '@sendgrid/mail';
import { getPrivateKey } from '../../common/jwt/jwt';
import { Context } from '../../common/log';
@Injectable()
export class SendGridService {
@ -20,10 +21,15 @@ export class SendGridService {
* @returns
*/
async createMailContentFromEmailConfirm(
context: Context,
accountId: number,
userId: number,
email: string,
): Promise<{ subject: string; text: string; html: string }> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.createMailContentFromEmailConfirm.name}`,
);
const lifetime =
this.configService.get<number>('EMAIL_CONFIRM_LIFETIME') ?? 0;
const privateKey = getPrivateKey(this.configService);
@ -39,6 +45,9 @@ export class SendGridService {
const domains = this.configService.get<string>('APP_DOMAIN');
const path = 'mail-confirm/';
this.logger.log(
`[OUT] [${context.trackingId}] ${this.createMailContentFromEmailConfirm.name}`,
);
return {
subject: 'Verify your new account',
text: `The verification URL. ${domains}${path}?verify=${token}`,
@ -85,17 +94,23 @@ export class SendGridService {
/**
*
* @param accountId ID
* @param userId ID
* @returns user confirm token
* @param context
* @param to
* @param from
* @param subject
* @param text
* @param html
* @returns mail
*/
async sendMail(
context: Context,
to: string,
from: string,
subject: string,
text: string,
html: string,
): Promise<void> {
this.logger.log(`[IN] [${context.trackingId}] ${this.sendMail.name}`);
try {
const res = await sendgrid
.send({
@ -114,8 +129,10 @@ export class SendGridService {
`status code: ${res.statusCode} body: ${JSON.stringify(res.body)}`,
);
} catch (e) {
console.log(JSON.stringify(e));
this.logger.error(e);
throw e;
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.sendMail.name}`);
}
}
}