diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index 014c7e1..31ce97c 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -173,6 +173,16 @@ export const overrideBlobstorageService = ( accountId: number, country: string, ) => Promise; + containerExists?: ( + context: Context, + accountId: number, + country: string, + ) => Promise; + publishTemplateUploadSas?: ( + context: Context, + accountId: number, + country: string, + ) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -189,6 +199,18 @@ export const overrideBlobstorageService = ( writable: true, }); } + if (overrides.containerExists) { + Object.defineProperty(obj, obj.containerExists.name, { + value: overrides.containerExists, + writable: true, + }); + } + if (overrides.publishTemplateUploadSas) { + Object.defineProperty(obj, obj.publishTemplateUploadSas.name, { + value: overrides.publishTemplateUploadSas, + writable: true, + }); + } }; /** diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index 8a89dc4..09b9c5e 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -290,13 +290,16 @@ export class FilesController { @Req() req: Request, ): Promise { const token = retrieveAuthorizationToken(req); - const accessToken = jwt.decode(token, { json: true }) as AccessToken; + const { userId } = jwt.decode(token, { json: true }) as AccessToken; - const context = makeContext(accessToken.userId); + const context = makeContext(userId); - console.log(context.trackingId); + const url = await this.filesService.publishTemplateFileUploadSas( + context, + userId, + ); - return { url: '' }; + return { url }; } @ApiResponse({ diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index b981224..0fb797c 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -10,7 +10,13 @@ import { DataSource } from 'typeorm'; import { createTask, makeTestingModuleWithBlob } from './test/utility'; import { FilesService } from './files.service'; import { makeContext } from '../../common/log'; -import { makeTestSimpleAccount, makeTestUser } from '../../common/test/utility'; +import { + makeTestAccount, + makeTestSimpleAccount, + makeTestUser, +} from '../../common/test/utility'; +import { makeTestingModule } from '../../common/test/modules'; +import { overrideBlobstorageService } from '../../common/test/overrides'; describe('音声ファイルアップロードURL取得', () => { it('アップロードSASトークンが乗っているURLを返却する', async () => { @@ -779,6 +785,102 @@ describe('テンプレートファイルダウンロードURL取得', () => { }); }); +describe('publishTemplateFileUploadSas', () => { + 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('テンプレートファイルアップロードSASトークンが乗っているURLを取得できる', async () => { + const module = await makeTestingModule(source); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const context = makeContext(admin.external_id); + const baseUrl = `https://saodmsusdev.blob.core.windows.net/account-${account.id}/Templates`; + + //SASトークンを返却する + overrideBlobstorageService(service, { + containerExists: async () => true, + publishTemplateUploadSas: async () => `${baseUrl}?sas-token`, + }); + + const url = await service.publishTemplateFileUploadSas( + context, + admin.external_id, + ); + + expect(url).toBe(`${baseUrl}?sas-token`); + }); + + it('blobストレージにコンテナが存在しない場合はエラーとなる', async () => { + const module = await makeTestingModule(source); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { admin } = await makeTestAccount(source, { tier: 5 }); + + const context = makeContext(admin.external_id); + + //Blobコンテナ存在チェックに失敗するようにする + overrideBlobstorageService(service, { + containerExists: async () => false, + publishTemplateUploadSas: async () => '', + }); + + try { + await service.publishTemplateFileUploadSas(context, admin.external_id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); + + it('SASトークンの取得に失敗した場合はエラーとなる', async () => { + const module = await makeTestingModule(source); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { admin } = await makeTestAccount(source, { tier: 5 }); + + const context = makeContext(admin.external_id); + + //BlobのSASトークン生成に失敗するようにする + overrideBlobstorageService(service, { + containerExists: async () => true, + publishTemplateUploadSas: async () => { + throw new Error('blob failed'); + }, + }); + + try { + await service.publishTemplateFileUploadSas(context, admin.external_id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + const optionItemList = [ { optionItemLabel: 'label_01', diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index aaff710..befe6b7 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -535,4 +535,53 @@ export class FilesService { ); } } + + /** + * ログインユーザーアカウントのテンプレートファイルのアップロードURLを取得する + * @param context + * @param externalId + * @returns template file upload sas + */ + async publishTemplateFileUploadSas( + context: Context, + externalId: string, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name} | params: { externalId: ${externalId} };`, + ); + try { + const { + account: { id: accountId, country }, + } = await this.usersRepository.findUserByExternalId(externalId); + + // 国に応じたリージョンのBlobストレージにコンテナが存在するか確認 + const isContainerExists = await this.blobStorageService.containerExists( + context, + accountId, + country, + ); + if (!isContainerExists) { + throw new Error('container not found.'); + } + + // SASトークン発行 + const url = await this.blobStorageService.publishTemplateUploadSas( + context, + accountId, + country, + ); + + return url; + } catch (e) { + this.logger.error(`error=${e}`); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name}`, + ); + } + } } diff --git a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts index 0c5d6af..7358bfd 100644 --- a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts +++ b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts @@ -25,6 +25,7 @@ export class BlobstorageService { private readonly sharedKeyCredentialUS: StorageSharedKeyCredential; private readonly sharedKeyCredentialAU: StorageSharedKeyCredential; private readonly sharedKeyCredentialEU: StorageSharedKeyCredential; + private readonly sasTokenExpireHour: number; constructor(private readonly configService: ConfigService) { this.sharedKeyCredentialUS = new StorageSharedKeyCredential( this.configService.get('STORAGE_ACCOUNT_NAME_US'), @@ -50,6 +51,14 @@ export class BlobstorageService { this.configService.get('STORAGE_ACCOUNT_ENDPOINT_EU'), this.sharedKeyCredentialEU, ); + + const expireTime = Number( + this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'), + ); + if (Number.isNaN(expireTime)) { + throw new Error(`STORAGE_TOKEN_EXPIRE_TIME is invalid value NaN`); + } + this.sasTokenExpireHour = expireTime; } /** @@ -206,10 +215,7 @@ export class BlobstorageService { //SASの有効期限を設定 const expiryDate = new Date(); - expiryDate.setHours( - expiryDate.getHours() + - this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'), - ); + expiryDate.setHours(expiryDate.getHours() + this.sasTokenExpireHour); //SASの権限を設定。Pendingにしたものを再アップロードする運用をするため、上書き可能にする const permissions = new ContainerSASPermissions(); @@ -237,6 +243,59 @@ export class BlobstorageService { return url.toString(); } + /** + * SASトークン付きのBlobStorageテンプレートファイルアップロードURLを生成し返却します + * @param accountId + * @param country + * @returns template upload sas + */ + async publishTemplateUploadSas( + context: Context, + accountId: number, + country: string, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.publishTemplateUploadSas.name}`, + ); + + try { + // コンテナ名を指定してClientを取得 + const containerClient = this.getContainerClient(accountId, country); + // 国に対応したリージョンの接続情報を取得する + const sharedKeyCredential = this.getSharedKeyCredential(country); + //SASの有効期限を設定 + const expiryDate = new Date(); + expiryDate.setHours(expiryDate.getHours() + this.sasTokenExpireHour); + + //SASの権限を設定。同名ファイルを再アップロードできる運用をするため、上書き可能にする + const permissions = new ContainerSASPermissions(); + permissions.write = true; + + //SASを発行 + const sasToken = generateBlobSASQueryParameters( + { + containerName: containerClient.containerName, + permissions: permissions, + startsOn: new Date(), + expiresOn: expiryDate, + }, + sharedKeyCredential, + ); + + const url = new URL('Templates', containerClient.url); + url.search = `${sasToken}`; + + return url.toString(); + } catch (e) { + this.logger.error(`error=${e}`); + throw e; + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.publishTemplateUploadSas.name}`, + ); + } + } + /** * SASトークン付きのBlobStorageダウンロードURLを生成し返却します * @param accountId @@ -274,10 +333,7 @@ export class BlobstorageService { //SASの有効期限を設定 const expiryDate = new Date(); - expiryDate.setHours( - expiryDate.getHours() + - this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'), - ); + expiryDate.setHours(expiryDate.getHours() + this.sasTokenExpireHour); //SASの権限を設定(ダウンロードのため読み取り許可) const permissions = new BlobSASPermissions();