makabe.t 01d92b2408 Merged PR 537: API実装(代行操作用トークン更新API)
## 概要
[Task2906: API実装(代行操作用トークン更新API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2906)

- アクセストークン更新APIとテストを実装しました。

## レビューポイント
- リポジトリのアカウントチェックは適切か
- テストケースは適切か

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
2023-10-31 01:47:00 +00:00

634 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types';
import {
OPTION_ITEM_NUM,
TASK_STATUS,
USER_ROLES,
} from '../../constants/index';
import { User } from '../../repositories/users/entity/user.entity';
import {
AudioFileNotFoundError,
AuthorUserNotMatchError,
TemplateFileNotFoundError,
} from './errors/types';
import {
AccountNotMatchError,
StatusNotMatchError,
TasksNotFoundError,
TypistUserNotFoundError,
} from '../../repositories/tasks/errors/types';
import { Context } from '../../common/log';
import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
@Injectable()
export class FilesService {
private readonly logger = new Logger(FilesService.name);
constructor(
private readonly usersRepository: UsersRepositoryService,
private readonly tasksRepository: TasksRepositoryService,
private readonly tasksRepositoryService: TasksRepositoryService,
private readonly templateFilesRepository: TemplateFilesRepositoryService,
private readonly blobStorageService: BlobstorageService,
) {}
/**
* Uploads finished
* @param url アップロード先Blob Storage(ファイル名含む)
* @param authorId 自分自身(ログイン認証)したAuthorID
* @param fileName 音声ファイル名
* @param duration 音声ファイルの録音時間(ミリ秒の整数値)
* @param createdDate 音声ファイルの録音作成日時(開始日時)yyyy-mm-ddThh:mm:ss.sss'
* @param finishedDate 音声ファイルの録音作成終了日時yyyy-mm-ddThh:mm:ss.sss
* @param uploadedDate 音声ファイルのアップロード日時yyyy-mm-ddThh:mm:ss.sss
* @param fileSize 音声ファイルのファイルサイズByte
* @param priority 優先度 "00":Normal / "01":High
* @param audioFormat 録音形式: DSS/DS2(SP)/DS2(QP)
* @param comment コメント
* @param workType WorkType
* @param optionItemList オプションアイテム音声メタデータ10個固定
* @param isEncrypted 暗号化されているか
* @returns finished
*/
async uploadFinished(
context: Context,
userId: string,
url: string,
authorId: string,
fileName: string,
duration: string,
createdDate: string,
finishedDate: string,
uploadedDate: string,
fileSize: number,
priority: string,
audioFormat: string,
comment: string,
workType: string,
optionItemList: AudioOptionItem[],
isEncrypted: boolean,
): Promise<AudioUploadFinishedResponse> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.uploadFinished.name} | params: { ` +
`url: ${url}, ` +
`authorId: ${authorId}, ` +
`fileName: ${fileName}, ` +
`duration: ${duration}, ` +
`createdDate: ${createdDate}, ` +
`finishedDate: ${finishedDate}, ` +
`uploadedDate: ${uploadedDate}, ` +
`fileSize: ${fileSize}, ` +
`priority: ${priority}, ` +
`audioFormat: ${audioFormat}, ` +
`comment: ${comment}, ` +
`workType: ${workType}, ` +
`optionItemList: ${JSON.stringify(optionItemList)}, ` +
`isEncrypted: ${isEncrypted} };`,
);
const formattedCreatedDate = new Date(createdDate);
const formattedFinishedDate = new Date(finishedDate);
const formattedUploadedDate = new Date(uploadedDate);
const isInvalidCreatedDate = isNaN(formattedCreatedDate.getTime());
const isInvalidFinishedDate = isNaN(formattedFinishedDate.getTime());
const isInvalidUploadedDate = isNaN(formattedUploadedDate.getTime());
// 日付フォーマットが不正ならパラメータ不正
if (
isInvalidCreatedDate ||
isInvalidFinishedDate ||
isInvalidUploadedDate
) {
if (isInvalidCreatedDate) {
this.logger.error(
`param createdDate is invalid format:[createdDate=${createdDate}]`,
);
}
if (isInvalidFinishedDate) {
this.logger.error(
`param finishedDate is invalid format:[finishedDate=${finishedDate}]`,
);
}
if (isInvalidUploadedDate) {
this.logger.error(
`param uploadedDate is invalid format:[uploadedDate=${uploadedDate}]`,
);
}
this.logger.log(
`[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
);
throw new HttpException(
makeErrorResponse('E010001'),
HttpStatus.BAD_REQUEST,
);
}
// オプションアイテムが10個ない場合はパラメータ不正
if (optionItemList.length !== OPTION_ITEM_NUM) {
this.logger.error(
`param optionItemList expects ${OPTION_ITEM_NUM} items, but has ${optionItemList.length} items`,
);
this.logger.log(
`[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
);
throw new HttpException(
makeErrorResponse('E010001'),
HttpStatus.BAD_REQUEST,
);
}
let user: User;
try {
// ユーザー取得
user = await this.usersRepository.findUserByExternalId(userId);
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.log(
`[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// URLにSASトークンがついている場合は取り除く
const urlObj = new URL(url);
urlObj.search = '';
const fileUrl = urlObj.toString();
this.logger.log(`Request URL: ${url}, Without param URL${fileUrl}`);
// 文字起こしタスク追加(音声ファイルとオプションアイテムも同時に追加)
// 追加時に末尾のJOBナンバーにインクリメントする
const task = await this.tasksRepositoryService.create(
user.account_id,
user.id,
priority,
fileUrl,
fileName,
authorId,
workType,
formattedCreatedDate,
duration,
formattedFinishedDate,
formattedUploadedDate,
fileSize,
audioFormat,
comment,
isEncrypted,
optionItemList,
);
return { jobNumber: task.job_number };
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
);
}
}
/**
* Publishs upload sas
* @param companyName
* @returns upload sas
*/
async publishUploadSas(
context: Context,
externalId: string,
): Promise<string> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.publishUploadSas.name} | params: { externalId: ${externalId} };`,
);
//DBから国情報とアカウントIDを取得する
try {
const user = await this.usersRepository.findUserByExternalId(externalId);
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
const accountId = user.account_id;
const country = user.account.country;
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
await this.blobStorageService.containerExists(
context,
accountId,
country,
);
// SASトークン発行
const url = await this.blobStorageService.publishUploadSas(
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.publishUploadSas.name}`,
);
}
}
/**
* 指定したIDの音声ファイルのダウンロードURLを取得する
* @param externalId
* @param audioFileId
* @returns audio file download sas
*/
async publishAudioFileDownloadSas(
context: Context,
externalId: string,
audioFileId: number,
): Promise<string> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.publishAudioFileDownloadSas.name} | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`,
);
//DBから国情報とアカウントID,ユーザーIDを取得する
let accountId: number;
let userId: number;
let country: string;
let isTypist: boolean;
let authorId: string | undefined;
try {
const user = await this.usersRepository.findUserByExternalId(externalId);
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
accountId = user.account.id;
userId = user.id;
country = user.account.country;
isTypist = user.role === USER_ROLES.TYPIST;
authorId = user.author_id ?? undefined;
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.log(
`[OUT] [${context.trackingId}] ${this.publishAudioFileDownloadSas.name}`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
const status = isTypist
? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
: Object.values(TASK_STATUS);
const task = await this.tasksRepository.getTaskAndAudioFile(
audioFileId,
accountId,
status,
);
const { file } = task;
// タスクに紐づく音声ファイルだけが消される場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
if (!file) {
throw new AudioFileNotFoundError(
`Audio file is not exists in DB. audio_file_id:${audioFileId}`,
);
}
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
if (!isTypist && file.author_id !== authorId) {
throw new AuthorUserNotMatchError(
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${file.author_id}, authorId:${authorId}`,
);
}
// ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー
if (isTypist && task.typist_user_id !== userId) {
throw new AuthorUserNotMatchError(
`task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`,
);
}
const filePath = `${file.file_name}`;
const isFileExist = await this.blobStorageService.fileExists(
context,
accountId,
country,
filePath,
);
if (!isFileExist) {
this.logger.log(`filePath:${filePath}`);
throw new AudioFileNotFoundError(
`Audio file is not exists in blob storage. audio_file_id:${audioFileId}, url:${file.url}, fileName:${file.file_name}`,
);
}
// SASトークン発行
const url = await this.blobStorageService.publishDownloadSas(
context,
accountId,
country,
filePath,
);
return url;
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TasksNotFoundError:
case AccountNotMatchError:
case StatusNotMatchError:
case AuthorUserNotMatchError:
case TypistUserNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.BAD_REQUEST,
);
case AudioFileNotFoundError:
throw new HttpException(
makeErrorResponse('E010701'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.publishAudioFileDownloadSas.name}`,
);
}
}
/**
* 指定したIDの音声ファイルに紐づいた文字起こしテンプレートファイルのダウンロードURLを取得する
* @param externalId
* @param audioFileId
* @returns template file download sas
*/
async publishTemplateFileDownloadSas(
context: Context,
externalId: string,
audioFileId: number,
): Promise<string> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.publishTemplateFileDownloadSas.name} | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`,
);
//DBから国情報とアカウントID,ユーザーIDを取得する
let accountId: number;
let userId: number;
let country: string;
let isTypist: boolean;
let authorId: string | undefined;
try {
const user = await this.usersRepository.findUserByExternalId(externalId);
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
accountId = user.account_id;
userId = user.id;
country = user.account.country;
isTypist = user.role === USER_ROLES.TYPIST;
authorId = user.author_id ?? undefined;
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.log(
`[OUT] [${context.trackingId}] ${this.publishTemplateFileDownloadSas.name}`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
const status = isTypist
? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
: Object.values(TASK_STATUS);
const task = await this.tasksRepository.getTaskAndAudioFile(
audioFileId,
accountId,
status,
);
const { file } = task;
// タスクに紐づく音声ファイルだけが消される場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
if (!file) {
throw new AudioFileNotFoundError(
`Audio file is not exists in DB. audio_file_id:${audioFileId}`,
);
}
const template_file = task.template_file;
// タスクに紐づくテンプレートファイルがない場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
if (!template_file) {
throw new TemplateFileNotFoundError(
`Template file is not exists in DB. audio_file_id:${audioFileId}`,
);
}
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
if (!isTypist && file.author_id !== authorId) {
throw new AuthorUserNotMatchError(
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${file.author_id}, authorId:${authorId}`,
);
}
// ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー
if (isTypist && task.typist_user_id !== userId) {
throw new AuthorUserNotMatchError(
`task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`,
);
}
const filePath = `Templates/${template_file.file_name}`;
const isFileExist = await this.blobStorageService.fileExists(
context,
accountId,
country,
filePath,
);
if (!isFileExist) {
throw new TemplateFileNotFoundError(
`Template file is not exists in blob storage. audio_file_id:${audioFileId}, url:${template_file.url}, fileName:${template_file.file_name}`,
);
}
// SASトークン発行
const url = await this.blobStorageService.publishDownloadSas(
context,
accountId,
country,
filePath,
);
return url;
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TasksNotFoundError:
case AccountNotMatchError:
case StatusNotMatchError:
case AuthorUserNotMatchError:
case TypistUserNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.BAD_REQUEST,
);
case AudioFileNotFoundError:
case TemplateFileNotFoundError:
throw new HttpException(
makeErrorResponse('E010701'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.publishTemplateFileDownloadSas.name}`,
);
}
}
/**
* ログインユーザーアカウントのテンプレートファイルのアップロードURLを取得する
* @param context
* @param externalId
* @returns template file upload sas
*/
async publishTemplateFileUploadSas(
context: Context,
externalId: string,
): Promise<string> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name} | params: { externalId: ${externalId} };`,
);
try {
const { account } = await this.usersRepository.findUserByExternalId(
externalId,
);
if (!account) {
throw new AccountNotFoundError('account not found.');
}
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
const isContainerExists = await this.blobStorageService.containerExists(
context,
account.id,
account.country,
);
if (!isContainerExists) {
throw new Error('container not found.');
}
// SASトークン発行
const url = await this.blobStorageService.publishTemplateUploadSas(
context,
account.id,
account.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}`,
);
}
}
/**
* テンプレートファイルのアップロード後にDBにテンプレートファイル情報を登録する
* @param context
* @param externalId
* @param url
* @param fileName
* @returns upload finished
*/
async templateUploadFinished(
context: Context,
externalId: string,
url: string,
fileName: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.templateUploadFinished.name} | params: { externalId: ${externalId}, url: ${url}, fileName: ${fileName} };`,
);
try {
// ユーザー取得
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// URLにSASトークンがついている場合は取り除く;
const urlObj = new URL(url);
urlObj.search = '';
const fileUrl = urlObj.toString();
this.logger.log(`Request URL: ${url}, Without param URL${fileUrl}`);
// テンプレートファイル情報をDBに登録
await this.templateFilesRepository.upsertTemplateFile(
accountId,
fileName,
fileUrl,
);
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.templateUploadFinished.name}`,
);
}
}
}