## 概要 [Task3265: IPアドレスを追跡用のIDに追加する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3265) - MiddlewareでUUIDを発行しリクエストのヘッダに追加する - 各コントローラーではヘッダからUUIDとIPアドレスを取得する - 取得したUUIDとADB2Cの外部IDでトラッキングIDを作成する - 作成したトラッキングIDとIPアドレスの繋がりをログに出力する。 ## レビューポイント - ADB2Cの外部IDがない場合にUnauthorized Userという文字列を入れているがほかの表現のほうが良いか - 外部IDもオプショナルにして入れなくてもよくする? - ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば
539 lines
15 KiB
TypeScript
539 lines
15 KiB
TypeScript
import {
|
||
Body,
|
||
Controller,
|
||
Get,
|
||
HttpException,
|
||
HttpStatus,
|
||
Logger,
|
||
Post,
|
||
Query,
|
||
Req,
|
||
UseGuards,
|
||
} from '@nestjs/common';
|
||
import {
|
||
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 {
|
||
AudioDownloadLocationRequest,
|
||
AudioDownloadLocationResponse,
|
||
AudioUploadFinishedRequest,
|
||
AudioUploadFinishedResponse,
|
||
AudioUploadLocationRequest,
|
||
AudioUploadLocationResponse,
|
||
TemplateDownloadLocationRequest,
|
||
TemplateDownloadLocationResponse,
|
||
TemplateUploadFinishedReqponse,
|
||
TemplateUploadFinishedRequest,
|
||
TemplateUploadLocationResponse,
|
||
} from './types/types';
|
||
import { AuthGuard } from '../../common/guards/auth/authguards';
|
||
import { RoleGuard } from '../../common/guards/role/roleguards';
|
||
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
|
||
import { retrieveAuthorizationToken } from '../../common/http/helper';
|
||
import { Request } from 'express';
|
||
import { makeContext, retrieveRequestId, retrieveIp } from '../../common/log';
|
||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||
|
||
@ApiTags('files')
|
||
@Controller('files')
|
||
export class FilesController {
|
||
private readonly logger = new Logger(FilesController.name);
|
||
constructor(private readonly filesService: FilesService) {}
|
||
|
||
@ApiResponse({
|
||
status: HttpStatus.OK,
|
||
type: AudioUploadFinishedResponse,
|
||
description: '成功時のレスポンス',
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.BAD_REQUEST,
|
||
description: '不正なパラメータ',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.UNAUTHORIZED,
|
||
description: '認証エラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||
description: '想定外のサーバーエラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiOperation({
|
||
operationId: 'uploadFinished',
|
||
description:
|
||
'アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します',
|
||
})
|
||
@ApiBearerAuth()
|
||
@UseGuards(AuthGuard)
|
||
@Post('audio/upload-finished')
|
||
async uploadFinished(
|
||
@Req() req: Request,
|
||
@Body() body: AudioUploadFinishedRequest,
|
||
): Promise<AudioUploadFinishedResponse> {
|
||
const accessToken = retrieveAuthorizationToken(req);
|
||
if (!accessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000107'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const ip = retrieveIp(req);
|
||
if (!ip) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000401'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const requestId = retrieveRequestId(req);
|
||
if (!requestId) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000501'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||
if (!decodedAccessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000101'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
const { userId } = decodedAccessToken as AccessToken;
|
||
|
||
const context = makeContext(userId, requestId);
|
||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||
|
||
const {
|
||
url,
|
||
authorId,
|
||
fileName,
|
||
duration,
|
||
createdDate,
|
||
finishedDate,
|
||
uploadedDate,
|
||
fileSize,
|
||
priority,
|
||
audioFormat,
|
||
comment,
|
||
workType,
|
||
optionItemList,
|
||
isEncrypted,
|
||
} = body;
|
||
|
||
const res = await this.filesService.uploadFinished(
|
||
context,
|
||
userId,
|
||
url,
|
||
authorId,
|
||
fileName,
|
||
duration,
|
||
createdDate,
|
||
finishedDate,
|
||
uploadedDate,
|
||
fileSize,
|
||
priority,
|
||
audioFormat,
|
||
comment,
|
||
workType,
|
||
optionItemList,
|
||
isEncrypted,
|
||
);
|
||
|
||
return { jobNumber: res.jobNumber };
|
||
}
|
||
|
||
@Get('audio/upload-location')
|
||
@ApiResponse({
|
||
status: HttpStatus.OK,
|
||
type: AudioUploadLocationResponse,
|
||
description: '成功時のレスポンス',
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.BAD_REQUEST,
|
||
description: '不正なパラメータ',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.UNAUTHORIZED,
|
||
description: '認証エラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||
description: '想定外のサーバーエラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiOperation({
|
||
operationId: 'uploadLocation',
|
||
description:
|
||
'ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します',
|
||
})
|
||
@ApiBearerAuth()
|
||
@UseGuards(AuthGuard)
|
||
async uploadLocation(
|
||
@Req() req: Request,
|
||
// クエリパラメータ AudioUploadLocationRequest は空であるため内部で使用しない。
|
||
// 使用しないことを宣言するために先頭にプレフィックス_(アンダースコア)をつけている
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
@Query() _query: AudioUploadLocationRequest,
|
||
): Promise<AudioUploadLocationResponse> {
|
||
const accessToken = retrieveAuthorizationToken(req);
|
||
if (!accessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000107'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const ip = retrieveIp(req);
|
||
if (!ip) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000401'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const requestId = retrieveRequestId(req);
|
||
if (!requestId) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000501'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||
if (!decodedAccessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000101'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
const { userId } = decodedAccessToken as AccessToken;
|
||
|
||
const context = makeContext(userId, requestId);
|
||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||
|
||
const url = await this.filesService.publishUploadSas(context, userId);
|
||
return { url };
|
||
}
|
||
|
||
@Get('audio/download-location')
|
||
@ApiResponse({
|
||
status: HttpStatus.OK,
|
||
type: AudioDownloadLocationResponse,
|
||
description: '成功時のレスポンス',
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.BAD_REQUEST,
|
||
description: '不正なパラメータ',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.UNAUTHORIZED,
|
||
description: '認証エラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||
description: '想定外のサーバーエラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiOperation({
|
||
operationId: 'downloadLocation',
|
||
description:
|
||
'指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します',
|
||
})
|
||
@ApiBearerAuth()
|
||
@UseGuards(AuthGuard)
|
||
@UseGuards(
|
||
RoleGuard.requireds({
|
||
roles: [ADMIN_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.TYPIST],
|
||
}),
|
||
)
|
||
async downloadLocation(
|
||
@Req() req: Request,
|
||
@Query() body: AudioDownloadLocationRequest,
|
||
): Promise<AudioDownloadLocationResponse> {
|
||
const { audioFileId } = body;
|
||
|
||
const accessToken = retrieveAuthorizationToken(req);
|
||
if (!accessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000107'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const ip = retrieveIp(req);
|
||
if (!ip) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000401'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const requestId = retrieveRequestId(req);
|
||
if (!requestId) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000501'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||
if (!decodedAccessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000101'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
const { userId } = decodedAccessToken as AccessToken;
|
||
|
||
const context = makeContext(userId, requestId);
|
||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||
|
||
const url = await this.filesService.publishAudioFileDownloadSas(
|
||
context,
|
||
userId,
|
||
audioFileId,
|
||
);
|
||
|
||
return { url };
|
||
}
|
||
|
||
@Get('template/download-location')
|
||
@ApiResponse({
|
||
status: HttpStatus.OK,
|
||
type: TemplateDownloadLocationResponse,
|
||
description: '成功時のレスポンス',
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.BAD_REQUEST,
|
||
description: '不正なパラメータ',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.UNAUTHORIZED,
|
||
description: '認証エラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||
description: '想定外のサーバーエラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiOperation({
|
||
operationId: 'downloadTemplateLocation',
|
||
description:
|
||
'指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します',
|
||
})
|
||
@ApiBearerAuth()
|
||
@UseGuards(AuthGuard)
|
||
@UseGuards(
|
||
RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST] }),
|
||
)
|
||
async downloadTemplateLocation(
|
||
@Req() req: Request,
|
||
@Query() body: TemplateDownloadLocationRequest,
|
||
): Promise<TemplateDownloadLocationResponse> {
|
||
const { audioFileId } = body;
|
||
|
||
const accessToken = retrieveAuthorizationToken(req);
|
||
if (!accessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000107'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const ip = retrieveIp(req);
|
||
if (!ip) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000401'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const requestId = retrieveRequestId(req);
|
||
if (!requestId) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000501'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||
if (!decodedAccessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000101'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
const { userId } = decodedAccessToken as AccessToken;
|
||
|
||
const context = makeContext(userId, requestId);
|
||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||
|
||
const url = await this.filesService.publishTemplateFileDownloadSas(
|
||
context,
|
||
userId,
|
||
audioFileId,
|
||
);
|
||
|
||
return { url };
|
||
}
|
||
|
||
@Get('template/upload-location')
|
||
@ApiResponse({
|
||
status: HttpStatus.OK,
|
||
type: TemplateUploadLocationResponse,
|
||
description: '成功時のレスポンス',
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.UNAUTHORIZED,
|
||
description: '認証エラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||
description: '想定外のサーバーエラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiOperation({
|
||
operationId: 'uploadTemplateLocation',
|
||
description:
|
||
'ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します',
|
||
})
|
||
@ApiBearerAuth()
|
||
@UseGuards(AuthGuard)
|
||
@UseGuards(
|
||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||
)
|
||
async uploadTemplateLocation(
|
||
@Req() req: Request,
|
||
): Promise<TemplateUploadLocationResponse> {
|
||
const accessToken = retrieveAuthorizationToken(req);
|
||
if (!accessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000107'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const ip = retrieveIp(req);
|
||
if (!ip) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000401'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const requestId = retrieveRequestId(req);
|
||
if (!requestId) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000501'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||
if (!decodedAccessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000101'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
const { userId } = decodedAccessToken as AccessToken;
|
||
|
||
const context = makeContext(userId, requestId);
|
||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||
|
||
const url = await this.filesService.publishTemplateFileUploadSas(
|
||
context,
|
||
userId,
|
||
);
|
||
|
||
return { url };
|
||
}
|
||
|
||
@ApiResponse({
|
||
status: HttpStatus.OK,
|
||
type: TemplateUploadFinishedReqponse,
|
||
description: '成功時のレスポンス',
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.BAD_REQUEST,
|
||
description: '不正なパラメータ',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.UNAUTHORIZED,
|
||
description: '認証エラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiResponse({
|
||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||
description: '想定外のサーバーエラー',
|
||
type: ErrorResponse,
|
||
})
|
||
@ApiOperation({
|
||
operationId: 'uploadTemplateFinished',
|
||
description: 'アップロードが完了したテンプレートファイルの情報を登録します',
|
||
})
|
||
@ApiBearerAuth()
|
||
@UseGuards(AuthGuard)
|
||
@UseGuards(
|
||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||
)
|
||
@Post('template/upload-finished')
|
||
async templateUploadFinished(
|
||
@Req() req: Request,
|
||
@Body() body: TemplateUploadFinishedRequest,
|
||
): Promise<TemplateUploadFinishedReqponse> {
|
||
const { name, url } = body;
|
||
const accessToken = retrieveAuthorizationToken(req);
|
||
if (!accessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000107'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const ip = retrieveIp(req);
|
||
if (!ip) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000401'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
|
||
const requestId = retrieveRequestId(req);
|
||
if (!requestId) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000501'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||
if (!decodedAccessToken) {
|
||
throw new HttpException(
|
||
makeErrorResponse('E000101'),
|
||
HttpStatus.UNAUTHORIZED,
|
||
);
|
||
}
|
||
const { userId } = decodedAccessToken as AccessToken;
|
||
|
||
const context = makeContext(userId, requestId);
|
||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||
await this.filesService.templateUploadFinished(context, userId, url, name);
|
||
return {};
|
||
}
|
||
}
|