Merged PR 98: API実装(SASトークン発行)
## 概要 [Task1737: API実装(SASトークン発行)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1737) - SASトークンを発行する処理を実装 - コンテナ作成処理を実装 - 本来はアカウント登録時に作成されるが、動作検証のためSAS発行時に作成するように実装している - コンテナ存在確認の処理を実装 ## レビューポイント - 実装した処理に漏れはないか - エラーの処理に考慮漏れはないか - テストケースは十分か ## 動作確認状況 - ローカルでコンテナ作成とURL発行を確認 ## 補足 - 相談、参考資料などがあれば
This commit is contained in:
parent
7da5a1fda4
commit
4cf444ab42
@ -12,4 +12,4 @@ REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000
|
||||
TENANT_NAME=adb2codmsdev
|
||||
SIGNIN_FLOW_NAME=b2c_1_signin_dev
|
||||
EMAIL_CONFIRM_LIFETIME=86400000
|
||||
APP_DOMAIN=https://10.1.0.10:4443/
|
||||
APP_DOMAIN=https://10.1.0.10:4443/
|
||||
|
||||
@ -15,4 +15,14 @@ SENDGRID_API_KEY=xxxxxxxxxxxxxxxx
|
||||
MAIL_FROM=xxxxx@xxxxx.xxxx
|
||||
NOTIFICATION_HUB_NAME=ntf-odms-shared
|
||||
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
|
||||
APP_DOMAIN=http://localhost:8081/
|
||||
APP_DOMAIN=http://localhost:8081/
|
||||
STORAGE_TOKEN_EXPIRE_TIME=30
|
||||
STORAGE_ACCOUNT_NAME_US=saodmsusdev
|
||||
STORAGE_ACCOUNT_NAME_AU=saodmsaudev
|
||||
STORAGE_ACCOUNT_NAME_EU=saodmseudev
|
||||
STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXX
|
||||
STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX
|
||||
STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX
|
||||
STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA
|
||||
STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA
|
||||
STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA
|
||||
|
||||
136
dictation_server/package-lock.json
generated
136
dictation_server/package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"@azure/identity": "^3.1.3",
|
||||
"@azure/keyvault-secrets": "^4.6.0",
|
||||
"@azure/notification-hubs": "^1.0.1",
|
||||
"@azure/storage-blob": "^12.14.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.5",
|
||||
"@nestjs/axios": "^0.1.0",
|
||||
"@nestjs/common": "^9.3.12",
|
||||
@ -375,6 +376,30 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.1.tgz",
|
||||
"integrity": "sha512-A3x+um3cAPgQe42Lu7Iv/x8/fNjhL/nIoEfqFxfn30EyxK6zC13n+OUxzZBRC0IzQqssqIbt4INf5YG7lYYFtw==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-auth": "^1.3.0",
|
||||
"@azure/core-tracing": "1.0.0-preview.13",
|
||||
"@azure/core-util": "^1.1.1",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"@types/node-fetch": "^2.5.0",
|
||||
"@types/tunnel": "^0.0.3",
|
||||
"form-data": "^4.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"process": "^0.11.10",
|
||||
"tslib": "^2.2.0",
|
||||
"tunnel": "^0.0.6",
|
||||
"uuid": "^8.3.0",
|
||||
"xml2js": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http-compat": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-1.3.0.tgz",
|
||||
@ -388,6 +413,30 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/@azure/core-tracing": {
|
||||
"version": "1.0.0-preview.13",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz",
|
||||
"integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.0.1",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/xml2js": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-lro": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.1.tgz",
|
||||
@ -602,6 +651,36 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/storage-blob": {
|
||||
"version": "12.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.14.0.tgz",
|
||||
"integrity": "sha512-g8GNUDpMisGXzBeD+sKphhH5yLwesB4JkHr1U6be/X3F+cAMcyGLPD1P89g2M7wbEtUJWoikry1rlr83nNRBzg==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-http": "^3.0.0",
|
||||
"@azure/core-lro": "^2.2.0",
|
||||
"@azure/core-paging": "^1.1.1",
|
||||
"@azure/core-tracing": "1.0.0-preview.13",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"events": "^3.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/storage-blob/node_modules/@azure/core-tracing": {
|
||||
"version": "1.0.0-preview.13",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz",
|
||||
"integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.0.1",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
|
||||
@ -2486,6 +2565,14 @@
|
||||
"openapi-generator": "bin/openapi-generator"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz",
|
||||
"integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
|
||||
@ -2854,8 +2941,29 @@
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.18.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.21.tgz",
|
||||
"integrity": "sha512-TassPGd0AEZWA10qcNnXnSNwHlLfSth8XwUaWc3gTSDmBz/rKb613Qw5qRf6o2fdRBrLbsgeC9PMZshobkuUqg==",
|
||||
"devOptional": true
|
||||
"integrity": "sha512-TassPGd0AEZWA10qcNnXnSNwHlLfSth8XwUaWc3gTSDmBz/rKb613Qw5qRf6o2fdRBrLbsgeC9PMZshobkuUqg=="
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz",
|
||||
"integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch/node_modules/form-data": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
||||
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
@ -2922,6 +3030,14 @@
|
||||
"@types/superagent": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tunnel": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz",
|
||||
"integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||
@ -8582,6 +8698,14 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@ -10051,6 +10175,14 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"engines": {
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
"@azure/identity": "^3.1.3",
|
||||
"@azure/keyvault-secrets": "^4.6.0",
|
||||
"@azure/notification-hubs": "^1.0.1",
|
||||
"@azure/storage-blob": "^12.14.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.5",
|
||||
"@nestjs/axios": "^0.1.0",
|
||||
"@nestjs/common": "^9.3.12",
|
||||
|
||||
@ -28,6 +28,7 @@ import { FilesService } from './features/files/files.service';
|
||||
import { TasksService } from './features/tasks/tasks.service';
|
||||
import { TasksController } from './features/tasks/tasks.controller';
|
||||
import { TasksModule } from './features/tasks/tasks.module';
|
||||
import { BlobstorageModule } from './gateways/blobstorage/blobstorage.module';
|
||||
import { LicensesModule } from './features/licenses/licenses.module';
|
||||
import { LicensesService } from './features/licenses/licenses.service';
|
||||
import { LicensesController } from './features/licenses/licenses.controller';
|
||||
@ -68,6 +69,7 @@ import { LicensesController } from './features/licenses/licenses.controller';
|
||||
}),
|
||||
NotificationModule,
|
||||
NotificationhubModule,
|
||||
BlobstorageModule,
|
||||
LicensesModule,
|
||||
],
|
||||
controllers: [
|
||||
|
||||
@ -27,3 +27,57 @@ export const TIER_4 = 4;
|
||||
* @const {number}
|
||||
*/
|
||||
export const TIER_5 = 5;
|
||||
|
||||
/**
|
||||
* 音声ファイルをEast USに保存する国リスト
|
||||
* @const {number}
|
||||
*/
|
||||
export const BLOB_STORAGE_REGION_US = ['CA', 'KY', 'US'];
|
||||
|
||||
/**
|
||||
* 音声ファイルをAustralia Eastに保存する国リスト
|
||||
* @const {number}
|
||||
*/
|
||||
export const BLOB_STORAGE_REGION_AU = ['AU', 'NZ'];
|
||||
|
||||
/**
|
||||
* 音声ファイルをNorth Europeに保存する国リスト
|
||||
* @const {number}
|
||||
*/
|
||||
export const BLOB_STORAGE_REGION_EU = [
|
||||
'AT',
|
||||
'BE',
|
||||
'BG',
|
||||
'HR',
|
||||
'CY',
|
||||
'CZ',
|
||||
'DK',
|
||||
'EE',
|
||||
'FI',
|
||||
'FR',
|
||||
'DE',
|
||||
'GR',
|
||||
'HU',
|
||||
'IS',
|
||||
'IE',
|
||||
'IT',
|
||||
'LV',
|
||||
'LI',
|
||||
'LT',
|
||||
'LU',
|
||||
'MT',
|
||||
'NL',
|
||||
'NO',
|
||||
'PL',
|
||||
'PT',
|
||||
'RO',
|
||||
'RS',
|
||||
'SK',
|
||||
'SI',
|
||||
'ZA',
|
||||
'ES',
|
||||
'SE',
|
||||
'CH',
|
||||
'TR',
|
||||
'GB',
|
||||
];
|
||||
|
||||
@ -71,13 +71,14 @@ export class AuthService {
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
// 要求された環境用トークンの寿命を決定
|
||||
const refreshTokenLifetime = type === 'web' ? lifetimeWeb : lifetimeDefault;
|
||||
|
||||
const privateKey = await this.cryptoService.getPrivateKey();
|
||||
|
||||
const token = sign<RefreshToken>(
|
||||
{
|
||||
//ユーザーの属しているアカウントの管理者にユーザーが設定されていればadminをセットする
|
||||
role: `${user.role} ${
|
||||
user.account.primary_admin_user_id === user.id ||
|
||||
user.account.secondary_admin_user_id === user.id
|
||||
|
||||
@ -8,11 +8,13 @@ import {
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiResponse,
|
||||
ApiOperation,
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { AccessToken } from '../../common/token';
|
||||
import { ErrorResponse } from '../../common/error/types/types';
|
||||
import { FilesService } from './files.service';
|
||||
import {
|
||||
@ -89,14 +91,18 @@ export class FilesController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
async uploadLocation(
|
||||
@Headers() headers,
|
||||
@Headers('authorization') authorization: string,
|
||||
@Query() query: AudioUploadLocationRequest,
|
||||
): Promise<AudioUploadLocationResponse> {
|
||||
const {} = query;
|
||||
// コンテナ作成処理の前にアクセストークンの認証を行う
|
||||
// アップロード先を決定する国情報はトークンから取得する想定
|
||||
|
||||
return { url: '' };
|
||||
//TODO Guardsで認証するからここではデコードするだけ
|
||||
const token = authorization.substring(
|
||||
'Bearer '.length,
|
||||
authorization.length,
|
||||
);
|
||||
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
|
||||
const url = await this.filesService.publishUploadSas(accessToken);
|
||||
return { url };
|
||||
}
|
||||
|
||||
@Get('audio/download-location')
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FilesService } from './files.service';
|
||||
import { FilesController } from './files.controller';
|
||||
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
|
||||
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersRepositoryModule, BlobstorageModule],
|
||||
providers: [FilesService],
|
||||
controllers: [FilesController],
|
||||
})
|
||||
|
||||
@ -1,18 +1,70 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FilesService } from './files.service';
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import {
|
||||
makeBlobstorageServiceMockValue,
|
||||
makeDefaultUsersRepositoryMockValue,
|
||||
makeFilesServiceMock,
|
||||
} from './test/files.service.mock';
|
||||
|
||||
describe('FilesService', () => {
|
||||
let service: FilesService;
|
||||
it('アップロードSASトークンが乗っているURLを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(blobParam, userRepoParam);
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [FilesService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FilesService>(FilesService);
|
||||
expect(
|
||||
await service.publishUploadSas({
|
||||
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
|
||||
role: 'Author',
|
||||
}),
|
||||
).toEqual('https://blob-storage?sas-token');
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
it('アカウント専用コンテナが無い場合でも、コンテナ作成しURLを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
blobParam.containerExists = false;
|
||||
|
||||
const service = await makeFilesServiceMock(blobParam, userRepoParam);
|
||||
|
||||
expect(
|
||||
await service.publishUploadSas({
|
||||
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
|
||||
role: 'Author',
|
||||
}),
|
||||
).toEqual('https://blob-storage?sas-token');
|
||||
});
|
||||
|
||||
it('ユーザー情報の取得に失敗した場合、例外エラーを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const service = await makeFilesServiceMock(blobParam, {
|
||||
findUserByExternalId: new Error(''),
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.publishUploadSas({
|
||||
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
|
||||
role: 'Author',
|
||||
}),
|
||||
).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
|
||||
);
|
||||
});
|
||||
|
||||
it('コンテナ作成に失敗した場合、例外エラーを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const service = await makeFilesServiceMock(blobParam, {
|
||||
findUserByExternalId: new Error(''),
|
||||
});
|
||||
blobParam.publishUploadSas = new Error('Azure service down');
|
||||
|
||||
await expect(
|
||||
service.publishUploadSas({
|
||||
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
|
||||
role: 'Author',
|
||||
}),
|
||||
).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import { AccessToken } from '../../common/token';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||||
|
||||
@Injectable()
|
||||
export class FilesService {}
|
||||
export class FilesService {
|
||||
private readonly logger = new Logger(FilesService.name);
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepositoryService,
|
||||
private readonly blobStorageService: BlobstorageService,
|
||||
) {}
|
||||
/**
|
||||
* Publishs upload sas
|
||||
* @param companyName
|
||||
* @returns upload sas
|
||||
*/
|
||||
async publishUploadSas(token: AccessToken): Promise<string> {
|
||||
//DBから国情報とアカウントIDを取得する
|
||||
let accountId: number;
|
||||
let country: string;
|
||||
try {
|
||||
const user = await this.usersRepository.findUserByExternalId(
|
||||
token.userId,
|
||||
);
|
||||
if (user.account) {
|
||||
accountId = user.account.id;
|
||||
country = user.account.country;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
|
||||
const isContainerExist = await this.blobStorageService.containerExists(
|
||||
accountId,
|
||||
country,
|
||||
);
|
||||
//TODO コンテナが無ければ作成
|
||||
if (!isContainerExist) {
|
||||
await this.blobStorageService.createContainer(accountId, country);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// SASトークン発行
|
||||
const url = await this.blobStorageService.publishUploadSas(
|
||||
accountId,
|
||||
country,
|
||||
);
|
||||
return url;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
116
dictation_server/src/features/files/test/files.service.mock.ts
Normal file
116
dictation_server/src/features/files/test/files.service.mock.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service';
|
||||
import { User } from '../../../repositories/users/entity/user.entity';
|
||||
import { UsersRepositoryService } from '../../../repositories/users/users.repository.service';
|
||||
import { FilesService } from '../files.service';
|
||||
|
||||
export type BlobstorageServiceMockValue = {
|
||||
createContainer: void | Error;
|
||||
containerExists: boolean | Error;
|
||||
publishUploadSas: string | Error;
|
||||
};
|
||||
|
||||
export type UsersRepositoryMockValue = {
|
||||
findUserByExternalId: User | Error;
|
||||
};
|
||||
|
||||
export const makeFilesServiceMock = async (
|
||||
blobStorageService: BlobstorageServiceMockValue,
|
||||
usersRepositoryMockValue: UsersRepositoryMockValue,
|
||||
): Promise<FilesService> => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [FilesService],
|
||||
})
|
||||
.useMocker((token) => {
|
||||
switch (token) {
|
||||
case BlobstorageService:
|
||||
return makeBlobstorageServiceMock(blobStorageService);
|
||||
case UsersRepositoryService:
|
||||
return makeUsersRepositoryMock(usersRepositoryMockValue);
|
||||
}
|
||||
})
|
||||
.compile();
|
||||
|
||||
return module.get<FilesService>(FilesService);
|
||||
};
|
||||
|
||||
export const makeBlobstorageServiceMock = (
|
||||
value: BlobstorageServiceMockValue,
|
||||
) => {
|
||||
const { containerExists, createContainer, publishUploadSas } = value;
|
||||
|
||||
return {
|
||||
containerExists:
|
||||
containerExists instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(containerExists)
|
||||
: jest.fn<Promise<boolean>, []>().mockResolvedValue(containerExists),
|
||||
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),
|
||||
};
|
||||
};
|
||||
|
||||
export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
|
||||
const { findUserByExternalId } = value;
|
||||
|
||||
return {
|
||||
findUserByExternalId:
|
||||
findUserByExternalId instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(findUserByExternalId)
|
||||
: jest.fn<Promise<User>, []>().mockResolvedValue(findUserByExternalId),
|
||||
};
|
||||
};
|
||||
|
||||
export const makeBlobstorageServiceMockValue =
|
||||
(): BlobstorageServiceMockValue => {
|
||||
return {
|
||||
containerExists: true,
|
||||
publishUploadSas: 'https://blob-storage?sas-token',
|
||||
createContainer: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// 個別のテストケースに対応してそれぞれのMockを用意するのは無駄が多いのでテストケース内で個別の値を設定する
|
||||
export const makeDefaultUsersRepositoryMockValue =
|
||||
(): UsersRepositoryMockValue => {
|
||||
return {
|
||||
findUserByExternalId: {
|
||||
id: 2,
|
||||
external_id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
|
||||
account_id: 1234567890123456,
|
||||
role: 'none',
|
||||
author_id: '',
|
||||
accepted_terms_version: '1.0',
|
||||
email_verified: true,
|
||||
deleted_at: null,
|
||||
created_by: 'test',
|
||||
created_at: new Date(),
|
||||
updated_by: null,
|
||||
updated_at: null,
|
||||
auto_renew: true,
|
||||
license_alert: true,
|
||||
notification: true,
|
||||
account: {
|
||||
id: 2,
|
||||
parent_account_id: 2,
|
||||
tier: 5,
|
||||
country: '',
|
||||
delegation_permission: true,
|
||||
locked: false,
|
||||
company_name: '',
|
||||
verified: true,
|
||||
primary_admin_user_id: 2,
|
||||
deleted_at: null,
|
||||
created_by: '',
|
||||
created_at: new Date(),
|
||||
updated_by: '',
|
||||
updated_at: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BlobstorageService } from './blobstorage.service';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
exports: [BlobstorageService],
|
||||
imports: [ConfigModule],
|
||||
providers: [BlobstorageService],
|
||||
})
|
||||
export class BlobstorageModule {}
|
||||
169
dictation_server/src/gateways/blobstorage/blobstorage.service.ts
Normal file
169
dictation_server/src/gateways/blobstorage/blobstorage.service.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
BlobServiceClient,
|
||||
StorageSharedKeyCredential,
|
||||
ContainerClient,
|
||||
ContainerSASPermissions,
|
||||
generateBlobSASQueryParameters,
|
||||
} from '@azure/storage-blob';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
BLOB_STORAGE_REGION_AU,
|
||||
BLOB_STORAGE_REGION_EU,
|
||||
BLOB_STORAGE_REGION_US,
|
||||
} from '../../constants';
|
||||
|
||||
@Injectable()
|
||||
export class BlobstorageService {
|
||||
private readonly logger = new Logger(BlobstorageService.name);
|
||||
private readonly blobServiceClientUS: BlobServiceClient;
|
||||
private readonly blobServiceClientEU: BlobServiceClient;
|
||||
private readonly blobServiceClientAU: BlobServiceClient;
|
||||
private readonly sharedKeyCredentialUS: StorageSharedKeyCredential;
|
||||
private readonly sharedKeyCredentialAU: StorageSharedKeyCredential;
|
||||
private readonly sharedKeyCredentialEU: StorageSharedKeyCredential;
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
// TODO リソース作成後に接続情報をKeyVaultに格納し、そこから取得するように修正する
|
||||
this.sharedKeyCredentialUS = new StorageSharedKeyCredential(
|
||||
this.configService.get('STORAGE_ACCOUNT_NAME_US'),
|
||||
this.configService.get('STORAGE_ACCOUNT_KEY_US'),
|
||||
);
|
||||
this.sharedKeyCredentialAU = new StorageSharedKeyCredential(
|
||||
this.configService.get('STORAGE_ACCOUNT_NAME_AU'),
|
||||
this.configService.get('STORAGE_ACCOUNT_KEY_AU'),
|
||||
);
|
||||
this.sharedKeyCredentialEU = new StorageSharedKeyCredential(
|
||||
this.configService.get('STORAGE_ACCOUNT_NAME_EU'),
|
||||
this.configService.get('STORAGE_ACCOUNT_KEY_EU'),
|
||||
);
|
||||
this.blobServiceClientUS = new BlobServiceClient(
|
||||
this.configService.get('STORAGE_ACCOUNT_ENDPOINT_US'),
|
||||
this.sharedKeyCredentialUS,
|
||||
);
|
||||
this.blobServiceClientAU = new BlobServiceClient(
|
||||
this.configService.get('STORAGE_ACCOUNT_ENDPOINT_AU'),
|
||||
this.sharedKeyCredentialAU,
|
||||
);
|
||||
this.blobServiceClientEU = new BlobServiceClient(
|
||||
this.configService.get('STORAGE_ACCOUNT_ENDPOINT_EU'),
|
||||
this.sharedKeyCredentialEU,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Creates container
|
||||
* @param companyName
|
||||
* @returns container
|
||||
*/
|
||||
async createContainer(accountId: number, country: string): Promise<void> {
|
||||
this.logger.log(`[IN] ${this.createContainer.name}`);
|
||||
|
||||
// 国に応じたリージョンでコンテナ名を指定してClientを取得
|
||||
const containerClient = this.getContainerClient(accountId, country);
|
||||
|
||||
try {
|
||||
// コンテナ作成
|
||||
await containerClient.create();
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(`[OUT] ${this.createContainer.name}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Containers exists
|
||||
* @param country
|
||||
* @param accountId
|
||||
* @returns exists
|
||||
*/
|
||||
async containerExists(accountId: number, country: string): Promise<boolean> {
|
||||
// 国に応じたリージョンでコンテナ名を指定してClientを取得
|
||||
const containerClient = this.getContainerClient(accountId, country);
|
||||
const exists = await containerClient.exists();
|
||||
return exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishs upload sas
|
||||
* @param accountId
|
||||
* @param country
|
||||
* @returns upload sas
|
||||
*/
|
||||
async publishUploadSas(accountId: number, country: string): Promise<string> {
|
||||
this.logger.log(`[IN] ${this.publishUploadSas.name}`);
|
||||
let containerClient: ContainerClient;
|
||||
let sharedKeyCredential: StorageSharedKeyCredential;
|
||||
try {
|
||||
// コンテナ名を指定してClientを取得
|
||||
containerClient = this.getContainerClient(accountId, country);
|
||||
// 国に対応したリージョンの接続情報を取得する
|
||||
sharedKeyCredential = this.getSharedKeyCredential(country);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
//SASの有効期限を設定
|
||||
//TODO 有効期限は仮で30分
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setMinutes(
|
||||
expiryDate.getMinutes() +
|
||||
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
|
||||
);
|
||||
|
||||
//SASの権限を設定
|
||||
const permissions = new ContainerSASPermissions();
|
||||
permissions.create = true;
|
||||
|
||||
//SASを発行
|
||||
const sasToken = generateBlobSASQueryParameters(
|
||||
{
|
||||
containerName: containerClient.containerName,
|
||||
permissions: permissions,
|
||||
startsOn: new Date(),
|
||||
expiresOn: expiryDate,
|
||||
},
|
||||
sharedKeyCredential,
|
||||
);
|
||||
|
||||
return `${containerClient.url}?${sasToken}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets container client
|
||||
* @param companyName
|
||||
* @returns container client
|
||||
*/
|
||||
private getContainerClient(
|
||||
accountId: number,
|
||||
country: string,
|
||||
): ContainerClient {
|
||||
const containerName = `account-${accountId}`;
|
||||
if (BLOB_STORAGE_REGION_US.includes(country)) {
|
||||
return this.blobServiceClientUS.getContainerClient(containerName);
|
||||
} else if (BLOB_STORAGE_REGION_AU.includes(country)) {
|
||||
return this.blobServiceClientAU.getContainerClient(containerName);
|
||||
} else if (BLOB_STORAGE_REGION_EU.includes(country)) {
|
||||
return this.blobServiceClientEU.getContainerClient(containerName);
|
||||
} else {
|
||||
throw new Error('invalid country');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets shared key credential
|
||||
* @param country
|
||||
* @returns shared key credential
|
||||
*/
|
||||
private getSharedKeyCredential(country: string): StorageSharedKeyCredential {
|
||||
if (BLOB_STORAGE_REGION_US.includes(country)) {
|
||||
return this.sharedKeyCredentialUS;
|
||||
} else if (BLOB_STORAGE_REGION_AU.includes(country)) {
|
||||
return this.sharedKeyCredentialAU;
|
||||
} else if (BLOB_STORAGE_REGION_EU.includes(country)) {
|
||||
return this.sharedKeyCredentialEU;
|
||||
} else {
|
||||
throw new Error('invalid country');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -93,7 +93,7 @@ export class UsersRepositoryService {
|
||||
return user;
|
||||
}
|
||||
|
||||
async findUserByExternalId(sub: string): Promise<User | undefined> {
|
||||
async findUserByExternalId(sub: string): Promise<User> {
|
||||
const user = await this.dataSource.getRepository(User).findOne({
|
||||
where: {
|
||||
external_id: sub,
|
||||
@ -104,7 +104,7 @@ export class UsersRepositoryService {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return undefined;
|
||||
throw new UserNotFoundError();
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user