import { Body, Controller, Get, HttpException, HttpStatus, Logger, Param, ParseIntPipe, Post, Query, Req, UseGuards, } from '@nestjs/common'; import { ApiResponse, ApiOperation, ApiTags, ApiBearerAuth, ApiParam, } from '@nestjs/swagger'; import { ErrorResponse } from '../../common/error/types/types'; import { Request } from 'express'; import { TasksService } from './tasks.service'; import { AudioNextRequest, AudioNextResponse, ChangeStatusRequest, ChangeStatusResponse, PostCheckoutPermissionRequest, PostCheckoutPermissionResponse, PostDeleteTaskRequest, PostDeleteTaskResponse, TasksRequest, TasksResponse, } from './types/types'; import { SortDirection, TaskListSortableAttribute, isSortDirection, isTaskListSortableAttribute, } from '../../common/types/sort'; import jwt from 'jsonwebtoken'; import { retrieveAuthorizationToken } from '../../common/http/helper'; import { AccessToken } from '../../common/token'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { ADMIN_ROLES, USER_ROLES } from '../../constants'; import { Roles } from '../../common/types/role'; import { makeContext, retrieveRequestId, retrieveIp } from '../../common/log'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; @ApiTags('tasks') @Controller('tasks') export class TasksController { private readonly logger = new Logger(TasksController.name); constructor(private readonly taskService: TasksService) {} @ApiResponse({ status: HttpStatus.OK, type: TasksResponse, 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: 'getTasks', description: '音声ファイル・文字起こしタスク情報をページ指定して取得します', }) @ApiBearerAuth() @UseGuards(AuthGuard) @Get() async getTasks( @Req() req, @Query() body: TasksRequest, ): Promise { 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, role } = decodedAccessToken as AccessToken; // RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う const roles = role.split(' ') as Roles[]; const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); const { limit, offset, status } = body; const paramName = isTaskListSortableAttribute(body.paramName ?? '') ? (body.paramName as TaskListSortableAttribute) : undefined; const direction = isSortDirection(body.direction ?? '') ? (body.direction as SortDirection) : undefined; const { tasks, total } = await this.taskService.getTasks( context, userId, roles, offset, limit, // statusが指定されていない場合は全てのステータスを取得する status?.split(','), paramName, direction, ); return { tasks, total, limit, offset }; } @Get('next') @ApiResponse({ status: HttpStatus.OK, type: AudioNextResponse, 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: 'getNextAudioFile', description: '指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します', }) @ApiBearerAuth() @UseGuards( RoleGuard.requireds({ roles: [USER_ROLES.TYPIST], }), ) async getNextAudioFile( @Req() req: Request, @Query() param: AudioNextRequest, ): Promise { const { endedFileId } = param; 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 nextFileId = await this.taskService.getNextTask( context, userId, endedFileId, ); return { nextFileId }; } @Post(':audioFileId/checkout') @ApiResponse({ status: HttpStatus.OK, type: ChangeStatusResponse, description: '成功時のレスポンス', }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '不正なパラメータ', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定したIDの音声ファイルが存在しない場合', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: '認証エラー', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', type: ErrorResponse, }) @ApiOperation({ operationId: 'checkout', description: '指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします)', }) @ApiBearerAuth() @UseGuards(AuthGuard) @UseGuards( RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST], }), ) async checkout( @Req() req: Request, @Param() param: ChangeStatusRequest, ): Promise { // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない 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, role } = decodedAccessToken as AccessToken; // RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う const roles = role.split(' ') as Roles[]; const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); await this.taskService.checkout(context, param.audioFileId, roles, userId); return {}; } @Post(':audioFileId/checkin') @ApiResponse({ status: HttpStatus.OK, type: ChangeStatusResponse, description: '成功時のレスポンス', }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '不正なパラメータ', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定したIDの音声ファイルが存在しない場合', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: '認証エラー', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', type: ErrorResponse, }) @ApiOperation({ operationId: 'checkin', description: '指定した文字起こしタスクをチェックインします(ステータスをFinishedにします)', }) @ApiBearerAuth() @UseGuards(AuthGuard) @UseGuards( RoleGuard.requireds({ roles: [USER_ROLES.TYPIST], }), ) async checkin( @Req() req: Request, @Param() params: ChangeStatusRequest, ): Promise { const { audioFileId } = params; // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない 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.taskService.checkin(context, audioFileId, userId); return {}; } @Post(':audioFileId/cancel') @ApiResponse({ status: HttpStatus.OK, type: ChangeStatusResponse, description: '成功時のレスポンス', }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '不正なパラメータ', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定したIDの音声ファイルが存在しない場合', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: '認証エラー', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', type: ErrorResponse, }) @ApiOperation({ operationId: 'cancel', description: '指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします)', }) @UseGuards(AuthGuard) @UseGuards( RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN, USER_ROLES.TYPIST], }), ) @ApiBearerAuth() async cancel( @Req() req: Request, @Param() params: ChangeStatusRequest, ): Promise { const { audioFileId } = params; // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない 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, role } = decodedAccessToken as AccessToken; // RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う const roles = role.split(' ') as Roles[]; const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); await this.taskService.cancel(context, audioFileId, userId, roles); return {}; } @Post(':audioFileId/suspend') @ApiResponse({ status: HttpStatus.OK, type: ChangeStatusResponse, description: '成功時のレスポンス', }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '不正なパラメータ', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定したIDの音声ファイルが存在しない場合', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: '認証エラー', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', type: ErrorResponse, }) @ApiOperation({ operationId: 'suspend', description: '指定した文字起こしタスクを一時中断します(ステータスをPendingにします)', }) @ApiBearerAuth() @UseGuards(AuthGuard) @UseGuards( RoleGuard.requireds({ roles: [USER_ROLES.TYPIST], }), ) async suspend( @Req() req: Request, @Param() params: ChangeStatusRequest, ): Promise { const { audioFileId } = params; // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない 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.taskService.suspend(context, audioFileId, userId); return {}; } @Post(':audioFileId/backup') @ApiResponse({ status: HttpStatus.OK, type: ChangeStatusResponse, description: '成功時のレスポンス', }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '不正なパラメータ', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定したIDの音声ファイルが存在しない場合', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: '認証エラー', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', type: ErrorResponse, }) @ApiOperation({ operationId: 'backup', description: '指定した文字起こしタスクをバックアップします(ステータスをBackupにします)', }) @ApiBearerAuth() @UseGuards(AuthGuard) @UseGuards( RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], }), ) async backup( @Req() req: Request, @Param() params: ChangeStatusRequest, ): Promise { const { audioFileId } = params; 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.taskService.backup(context, audioFileId, userId); return {}; } @Post(':audioFileId/checkout-permission') @ApiResponse({ status: HttpStatus.OK, type: PostCheckoutPermissionResponse, description: '成功時のレスポンス', }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '不正なパラメータ(タスクのステータス不正、指定ユーザー不正など)', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定したIDの音声ファイルが存在しない', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: '認証エラー', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', type: ErrorResponse, }) @ApiOperation({ operationId: 'changeCheckoutPermission', description: '指定した文字起こしタスクのチェックアウト候補を変更します。', }) @ApiParam({ name: 'audioFileId', required: true, description: 'ODMS Cloud上の音声ファイルID', }) @ApiBearerAuth() @UseGuards(AuthGuard) @UseGuards( RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN, USER_ROLES.AUTHOR] }), ) async changeCheckoutPermission( @Req() req: Request, //TODO [Task2243] checkoutやcheckinと同じパスパラメータなので記述方法を統一したい @Param(`audioFileId`, ParseIntPipe) audioFileId: number, @Body() body: PostCheckoutPermissionRequest, ): Promise { const { assignees } = 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, role } = decodedAccessToken as AccessToken; // RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う const roles = role.split(' ') as Roles[]; const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); await this.taskService.changeCheckoutPermission( context, audioFileId, assignees, userId, roles, ); return {}; } @Post(':audioFileId/delete') @ApiResponse({ status: HttpStatus.OK, type: PostDeleteTaskResponse, 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: 'deleteTask', description: '指定した文字起こしタスクを削除します。', }) @ApiBearerAuth() @UseGuards(AuthGuard) @UseGuards( RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN, USER_ROLES.AUTHOR] }), ) async deleteTask( @Req() req: Request, @Param() params: PostDeleteTaskRequest, ): Promise { const { audioFileId } = params; // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない 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.taskService.deleteTask(context, userId, audioFileId); return {}; } }