Merged PR 185: API修正(チェックアウト候補変更)

## 概要
[Task2070: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2070)

- DBの検索条件を変更
  - ユーザーグループ検索
    - 同一アカウント内という条件を追加
      - ユーザーグループIDは画面側から渡されるが、そのIDが同じアカウント内のIDかわからないため
  - タイピストユーザー検索
    - 同一アカウント内でロールが`typist`という条件を追加
      - 画面側から渡されるユーザーIDが、同じアカウント内のIDかわからないため
      - 画面側から渡されるユーザーIDが、Typistであるかわからないため
  - タスク検索
    - 同一アカウント内という条件を追加
      - 渡される音声ファイルIDが同じアカウント内であるかわからないため
    - 自身のロールが`author`だった場合、AuthorIDが一致するタスクという条件を追加(管理者ロールがある場合は条件無し)

## レビューポイント
- 不要な条件追加ではないか
- ロールの型変換処理に問題はないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認
## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2023-06-27 07:18:19 +00:00
parent 5be4995d7d
commit 8c3e1db63b
8 changed files with 135 additions and 24 deletions

View File

@ -72,3 +72,36 @@ describe('RoleGuard', () => {
expect(guards.checkRole('')).toBeFalsy();
});
});
describe('isRoles', () => {
it('roleの各要素が正しい値であったとき、許可される', () => {
const guards = RoleGuard.requireds({
roles: [[USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN], USER_ROLES.TYPIST],
});
const result = guards.isRoles('admin author');
expect(result).toBeTruthy();
});
it('roleの各要素に不正な値があったとき、拒否される', () => {
const guards = RoleGuard.requireds({
roles: [[USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN], USER_ROLES.TYPIST],
});
const result = guards.isRoles('admin xxx');
expect(result).toBeFalsy();
});
it('roleの各要素に重複があったとき、拒否される', () => {
const guards = RoleGuard.requireds({
roles: [[USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN], USER_ROLES.TYPIST],
});
const result = guards.isRoles('admin admin');
expect(result).toBeFalsy();
});
it('roleの値がないとき、拒否される', () => {
const guards = RoleGuard.requireds({
roles: [[USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN], USER_ROLES.TYPIST],
});
const result = guards.isRoles('');
expect(result).toBeFalsy();
});
});

View File

@ -9,11 +9,10 @@ import { AccessToken } from '../../token';
import { Request } from 'express';
import { retrieveAuthorizationToken } from '../../../common/http/helper';
import { makeErrorResponse } from '../../../common/error/makeErrorResponse';
export type RoleType = 'typist' | 'author' | 'none' | 'admin';
import { Roles } from '../../types/role';
import { ADMIN_ROLES, USER_ROLES } from '../../../constants';
export interface RoleSetting {
roles: (RoleType | RoleType[])[];
roles: (Roles | Roles[])[];
}
export class RoleGuard implements CanActivate {
@ -46,6 +45,13 @@ export class RoleGuard implements CanActivate {
return true;
}
if (!this.isRoles(payload.role)) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const isValid = this.checkRole(payload.role);
if (isValid) {
return true;
@ -87,6 +93,31 @@ export class RoleGuard implements CanActivate {
return false;
}
/**
* publicメソッドとして切り出したもの
* Roleの値の妥当性チェック
* @param role roleの値
* @returns true/false
*/
isRoles = (role: string): boolean => {
const userRoles = role.split(' ') as Roles[];
const rolesValues = [
...Object.values(ADMIN_ROLES),
...Object.values(USER_ROLES),
];
//重複チェック
if (userRoles.length !== new Set(userRoles).size) {
return false;
}
//各要素が正しい値かチェック
return userRoles.every((role) => {
if (!rolesValues.includes(role)) {
return false;
}
return true;
});
};
/**
* Guardを作成する
* { roles: ['admin', 'author'] } "admin""author"

View File

@ -0,0 +1,8 @@
import { ADMIN_ROLES, USER_ROLES } from '../../../constants';
/**
* Token.roleに配置されうる文字列リテラル型
*/
export type Roles =
| (typeof ADMIN_ROLES)[keyof typeof ADMIN_ROLES]
| (typeof USER_ROLES)[keyof typeof USER_ROLES];

View File

@ -101,13 +101,6 @@ export const USER_ROLES = {
TYPIST: 'typist',
} as const;
/**
* Token.roleに配置されうる文字列リテラル型
*/
export type Roles =
| (typeof ADMIN_ROLES)[keyof typeof ADMIN_ROLES]
| (typeof USER_ROLES)[keyof typeof USER_ROLES];
/**
*
* @const {string}

View File

@ -40,6 +40,7 @@ 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';
@ApiTags('tasks')
@Controller('tasks')
@ -430,7 +431,20 @@ export class TasksController {
@Body() body: PostCheckoutPermissionRequest,
): Promise<PostCheckoutPermissionResponse> {
const { assignees } = body;
await this.taskService.changeCheckoutPermission(audioFileId, assignees);
const accessToken = retrieveAuthorizationToken(req);
const { role, userId } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
// RoleGuardでroleの要素が正しい値であることは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
await this.taskService.changeCheckoutPermission(
audioFileId,
assignees,
userId,
roles,
);
return {};
}

View File

@ -674,7 +674,7 @@ describe('TasksService', () => {
});
});
describe('TasksService', () => {
describe('changeCheckoutPermission', () => {
// TODO sqliteを用いたテストを別途実装予定
/*
@ -692,9 +692,14 @@ describe('TasksService', () => {
adb2cServiceMockValue,
);
expect(await service.tasksService.changeCheckoutPermission(1, [])).toEqual(
undefined,
);
expect(
await service.tasksService.changeCheckoutPermission(
1,
[],
'xxx-xxx-xxxx',
['admin'],
),
).toEqual(undefined);
});
it('ユーザーが存在しない場合、タスクのチェックアウト権限を変更できない', async () => {
@ -710,7 +715,9 @@ describe('TasksService', () => {
);
await expect(
service.tasksService.changeCheckoutPermission(1, []),
service.tasksService.changeCheckoutPermission(1, [], 'xxx-xxxx-xxxx', [
'admin',
]),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010601'), HttpStatus.BAD_REQUEST),
);

View File

@ -17,11 +17,12 @@ import {
} from '../../gateways/adb2c/adb2c.service';
import { AdB2cUser } from '../../gateways/adb2c/types/types';
import { CheckoutPermission } from '../../repositories/checkout_permissions/entity/checkout_permission.entity';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import {
TasksNotFoundError,
TypistUserGroupNotFoundError,
TypistUserNotFoundError,
} from '../../repositories/tasks/errors/types';
import { Roles } from '../../common/types/role';
@Injectable()
export class TasksService {
@ -170,17 +171,25 @@ export class TasksService {
async changeCheckoutPermission(
audioFileId: number,
assignees: Assignee[],
externalId: string,
role: Roles[],
): Promise<void> {
try {
const { author_id, account_id } =
await this.usersRepository.findUserByExternalId(externalId);
await this.taskRepository.changeCheckoutPermission(
audioFileId,
author_id,
account_id,
role,
assignees,
);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case UserNotFoundError:
case TypistUserNotFoundError:
case TypistUserGroupNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),

View File

@ -7,7 +7,7 @@ import {
IsNull,
} from 'typeorm';
import { Task } from './entity/task.entity';
import { TASK_STATUS } from '../../constants';
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
import { AudioOptionItem as ParamOptionItem } from '../../features/files/types/types';
import { AudioFile } from '../audio_files/entity/audio_file.entity';
import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity';
@ -25,6 +25,7 @@ import {
TypistUserGroupNotFoundError,
TypistUserNotFoundError,
} from './errors/types';
import { Roles } from '../../common/types/role';
@Injectable()
export class TasksRepositoryService {
@ -381,13 +382,16 @@ export class TasksRepositoryService {
/**
* Changes checkout permission
* @param audioFileId
* @param audio_file_id
* @param assignees
* @param accountId
* @returns checkout permission
*/
async changeCheckoutPermission(
audioFileId: number,
audio_file_id: number,
author_id: string,
account_id: number,
roles: Roles[],
assignees: Assignee[],
): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
@ -401,6 +405,7 @@ export class TasksRepositoryService {
const groupRecords = await groupRepo.find({
where: {
id: In(userGroupIds),
account_id: account_id,
deleted_at: IsNull(),
},
});
@ -423,6 +428,8 @@ export class TasksRepositoryService {
const userRecords = await userRepo.find({
where: {
id: In(typistUserIds),
account_id: account_id,
role: USER_ROLES.TYPIST,
deleted_at: IsNull(),
},
});
@ -439,12 +446,21 @@ export class TasksRepositoryService {
const taskRepo = entityManager.getRepository(Task);
const taskRecord = await taskRepo.findOne({
where: { audio_file_id: audioFileId, status: TASK_STATUS.UPLOADED },
where: {
audio_file_id: audio_file_id,
status: TASK_STATUS.UPLOADED,
account_id: account_id,
file: {
author_id: roles.includes(ADMIN_ROLES.ADMIN)
? undefined
: author_id,
},
},
});
//タスクが存在しない or ステータスがUploadedでなければエラー
if (!taskRecord) {
throw new TasksNotFoundError(
`Task not found Error. audio_file_id:${audioFileId}`,
`Task not found Error. audio_file_id:${audio_file_id}`,
);
}