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:
saito.k 2023-05-17 00:38:39 +00:00
parent 7da5a1fda4
commit 4cf444ab42
15 changed files with 651 additions and 27 deletions

View File

@ -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/

View File

@ -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

View File

@ -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",

View File

@ -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",

View File

@ -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: [

View File

@ -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',
];

View File

@ -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

View File

@ -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')

View File

@ -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],
})

View File

@ -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),
);
});
});

View File

@ -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,
);
}
}
}

View 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,
},
},
};
};

View File

@ -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 {}

View 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');
}
}
}

View File

@ -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;
}