diff --git a/dictation_client/src/common/token.ts b/dictation_client/src/common/token.ts index c41442f..ac7c3e9 100644 --- a/dictation_client/src/common/token.ts +++ b/dictation_client/src/common/token.ts @@ -1,5 +1,6 @@ // トークンの型やtypeGuardの関数を配置するファイル export interface Token { + delegateUserId?: string; userId: string; role: string; tier: number; diff --git a/dictation_client/src/components/auth/constants.ts b/dictation_client/src/components/auth/constants.ts index ee22851..cf2b972 100644 --- a/dictation_client/src/components/auth/constants.ts +++ b/dictation_client/src/components/auth/constants.ts @@ -33,4 +33,20 @@ export const TIERS = { * 401エラー時にログアウトさせずに処理を継続するエラーコード * @const {string[]} */ -export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = ["E010209"]; +export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [ + "E010209", + "E010503", + "E010501", +]; + +/** + * アクセストークンを更新する基準の秒数 + * @const {number} + */ +export const TOKEN_UPDATE_TIME = 5 * 60; + +/** + * アクセストークンの更新チェックを行う間隔(ミリ秒) + * @const {number} + */ +export const TOKEN_UPDATE_INTERVAL_MS = 3 * 60 * 1000; diff --git a/dictation_client/src/components/auth/updateTokenTimer.tsx b/dictation_client/src/components/auth/updateTokenTimer.tsx index 27fa972..b615ac6 100644 --- a/dictation_client/src/components/auth/updateTokenTimer.tsx +++ b/dictation_client/src/components/auth/updateTokenTimer.tsx @@ -2,33 +2,56 @@ import React, { useCallback } from "react"; import { AppDispatch } from "app/store"; import { decodeToken } from "common/decodeToken"; import { useInterval } from "common/useInterval"; -import { updateTokenAsync, loadAccessToken } from "features/auth"; +import { + updateTokenAsync, + loadAccessToken, + updateDelegationTokenAsync, +} from "features/auth"; import { DateTime } from "luxon"; -import { useDispatch } from "react-redux"; -// アクセストークンを更新する基準の秒数 -const TOKEN_UPDATE_TIME = 5 * 60; -// アクセストークンの更新チェックを行う間隔(ミリ秒) -const TOKEN_UPDATE_INTERVAL_MS = 3 * 60 * 1000; +import { useDispatch, useSelector } from "react-redux"; +import { selectDelegationAccessToken } from "features/auth/selectors"; +import { useNavigate } from "react-router-dom"; +import { cleanupDelegateAccount } from "features/partner"; +import { TOKEN_UPDATE_INTERVAL_MS, TOKEN_UPDATE_TIME } from "./constants"; export const UpdateTokenTimer = () => { const dispatch: AppDispatch = useDispatch(); + const navigate = useNavigate(); + + const delegattionToken = useSelector(selectDelegationAccessToken); // 期限が5分以内であれば更新APIを呼ぶ const updateToken = useCallback(async () => { // localStorageからトークンを取得 const jwt = loadAccessToken(); + // 現在時刻を取得 + const now = DateTime.local().toSeconds(); // selectorに以下の判定処理を移したかったが、初期表示時の値でしか判定できないのでComponent内に置く if (jwt) { const token = decodeToken(jwt); if (token) { const { exp } = token; - const now = DateTime.local().toSeconds(); if (exp - now <= TOKEN_UPDATE_TIME) { await dispatch(updateTokenAsync()); } } } - }, [dispatch]); + + // 代行操作トークン更新処理 + if (delegattionToken) { + const token = decodeToken(delegattionToken); + if (token) { + const { exp } = token; + if (exp - now <= TOKEN_UPDATE_TIME) { + const { meta } = await dispatch(updateDelegationTokenAsync()); + if (meta.requestStatus === "rejected") { + dispatch(cleanupDelegateAccount()); + navigate("/partners"); + } + } + } + } + }, [dispatch, delegattionToken, navigate]); useInterval(updateToken, TOKEN_UPDATE_INTERVAL_MS); diff --git a/dictation_client/src/components/delegate/index.tsx b/dictation_client/src/components/delegate/index.tsx index c5d18e7..1bfce9e 100644 --- a/dictation_client/src/components/delegate/index.tsx +++ b/dictation_client/src/components/delegate/index.tsx @@ -21,10 +21,16 @@ export const DelegationBar: React.FC = (): JSX.Element => { const navigate = useNavigate(); const onClickExit = useCallback(() => { + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } dispatch(clearDelegationToken()); dispatch(cleanupDelegateAccount()); navigate("/partners"); - }, [dispatch, navigate]); + }, [dispatch, navigate, t]); return (
{HEADER_NAME}
+{t(HEADER_NAME)}
The verification URL.
`,
+ };
+ }
+
+ /**
+ * メールを送信する
+ * @param to
+ * @param from
+ * @param subject
+ * @param text
+ * @param html
+ * @returns mail
+ */
+ async sendMail(
+ to: string,
+ from: string,
+ subject: string,
+ text: string,
+ html: string
+ ): Promise OMDS TOP PAGE URL. ${this.appDomain}" OMDS TOP PAGE URL. ${this.appDomain} The verification URL. ${this.appDomain}${path}?verify=${token}"`,
+ html: ` The verification URL. ${this.appDomain}${path}?verify=${token}`,
};
}
@@ -87,7 +87,7 @@ export class SendGridService {
return {
subject: 'Verify your new account',
text: `The verification URL. ${this.appDomain}${path}?verify=${token}`,
- html: ` The verification URL. ${this.appDomain}${path}?verify=${token}"`,
+ html: ` The verification URL. ${this.appDomain}${path}?verify=${token}`,
};
}
diff --git a/dictation_server/src/repositories/tasks/entity/task.entity.ts b/dictation_server/src/repositories/tasks/entity/task.entity.ts
index 124d4d1..7abb66f 100644
--- a/dictation_server/src/repositories/tasks/entity/task.entity.ts
+++ b/dictation_server/src/repositories/tasks/entity/task.entity.ts
@@ -10,6 +10,8 @@ import {
JoinColumn,
OneToMany,
ManyToOne,
+ CreateDateColumn,
+ UpdateDateColumn,
} from 'typeorm';
import { bigintTransformer } from '../../../common/entity';
@@ -37,8 +39,24 @@ export class Task {
started_at: Date | null;
@Column({ nullable: true, type: 'datetime' })
finished_at: Date | null;
- @Column({})
+
+ @Column({ nullable: true, type: 'datetime' })
+ created_by: string | null;
+
+ @CreateDateColumn({
+ default: () => "datetime('now', 'localtime')",
+ type: 'datetime',
+ }) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
+
+ @Column({ nullable: true, type: 'datetime' })
+ updated_by: string | null;
+
+ @UpdateDateColumn({
+ default: () => "datetime('now', 'localtime')",
+ type: 'datetime',
+ }) // defaultはSQLite用設定値.本番用は別途migrationで設定
+ updated_at: Date;
@OneToOne(() => AudioFile, (audiofile) => audiofile.task)
@JoinColumn({ name: 'audio_file_id' })
file: AudioFile | null;
diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts
index 63a813a..e3daecb 100644
--- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts
+++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts
@@ -1,10 +1,12 @@
import { Injectable } from '@nestjs/common';
import {
DataSource,
+ EntityManager,
FindOptionsOrder,
FindOptionsOrderValue,
In,
IsNull,
+ Repository,
} from 'typeorm';
import { Task } from './entity/task.entity';
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
@@ -35,6 +37,8 @@ import {
import { Roles } from '../../common/types/role';
import { TaskStatus, isTaskStatus } from '../../common/types/taskStatus';
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
+import { Workflow } from '../workflows/entity/workflow.entity';
+import { Worktype } from '../worktypes/entity/worktype.entity';
@Injectable()
export class TasksRepositoryService {
@@ -710,18 +714,6 @@ export class TasksRepositoryService {
task.audio_file_id = savedAudioFile.id;
- const optionItems = paramOptionItems.map((x) => {
- return {
- audio_file_id: savedAudioFile.id,
- label: x.optionItemLabel,
- value: x.optionItemValue,
- };
- });
-
- const optionItemRepo = entityManager.getRepository(AudioOptionItem);
- const newAudioOptionItems = optionItemRepo.create(optionItems);
- await optionItemRepo.save(newAudioOptionItems);
-
const taskRepo = entityManager.getRepository(Task);
// アカウント内でJOBナンバーが有効なタスクのうち最新のものを取得
@@ -743,8 +735,19 @@ export class TasksRepositoryService {
}
task.job_number = newJobNumber;
- const newTask = taskRepo.create(task);
- const persisted = await taskRepo.save(newTask);
+ const persisted = await taskRepo.save(task);
+
+ const optionItems = paramOptionItems.map((x) => {
+ return {
+ audio_file_id: persisted.audio_file_id,
+ label: x.optionItemLabel,
+ value: x.optionItemValue,
+ };
+ });
+
+ const optionItemRepo = entityManager.getRepository(AudioOptionItem);
+ const newAudioOptionItems = optionItemRepo.create(optionItems);
+ await optionItemRepo.save(newAudioOptionItems);
return persisted;
},
);
@@ -952,6 +955,227 @@ export class TasksRepositoryService {
return tasks;
});
}
+
+ /**
+ * ルーティングルールを取得し、タスクのチェックアウト権限を設定する
+ * @param audioFileId
+ * @param accountId
+ * @param [myAuthorId]
+ * @returns typistIds: タイピストIDの一覧 / typistGroupIds: タイピストグループIDの一覧
+ */
+ async autoRouting(
+ audioFileId: number,
+ accountId: number,
+ myAuthorId?: string, // API実行者のAuthorId
+ ): Promise<{ typistIds: number[]; typistGroupIds: number[] }> {
+ return await this.dataSource.transaction(async (entityManager) => {
+ // 音声ファイルを取得
+ const audioFileRepo = entityManager.getRepository(AudioFile);
+ const audioFile = await audioFileRepo.findOne({
+ relations: {
+ task: true,
+ },
+ where: {
+ id: audioFileId,
+ account_id: accountId,
+ },
+ });
+ if (!audioFile) {
+ throw new Error(
+ `audio file not found. audio_file_id:${audioFileId}, accountId:${accountId}`,
+ );
+ }
+
+ const { task } = audioFile;
+
+ if (!task) {
+ throw new Error(
+ `task not found. audio_file_id:${audioFileId}, accountId:${accountId}`,
+ );
+ }
+ // authorIdをもとにユーザーを取得
+ const userRepo = entityManager.getRepository(User);
+ const authorUser = await userRepo.findOne({
+ where: {
+ author_id: audioFile.author_id,
+ account_id: accountId,
+ },
+ });
+
+ // 音声ファイル上のworktypeIdをもとにworktypeを取得
+ const worktypeRepo = entityManager.getRepository(Worktype);
+ const worktypeRecord = await worktypeRepo.findOne({
+ where: {
+ custom_worktype_id: audioFile.work_type_id,
+ account_id: accountId,
+ },
+ });
+
+ // 音声ファイル上のworktypeIdが設定されているが、一致するworktypeが存在しない場合はエラーを出して終了
+ if (!worktypeRecord && audioFile.work_type_id !== '') {
+ throw new Error(
+ `worktype not found. worktype:${audioFile.work_type_id}, accountId:${accountId}`,
+ );
+ }
+
+ // Workflow(ルーティングルール)を取得
+ const workflowRepo = entityManager.getRepository(Workflow);
+ const workflow = await workflowRepo.findOne({
+ relations: {
+ workflowTypists: true,
+ },
+ where: {
+ account_id: accountId,
+ author_id: authorUser?.id ?? IsNull(), // authorUserが存在しない場合は、必ずヒットしないようにNULLを設定する
+ worktype_id: worktypeRecord?.id ?? IsNull(),
+ },
+ });
+
+ // Workflow(ルーティングルール)があればタスクのチェックアウト権限を設定する
+ if (workflow) {
+ return await this.setCheckoutPermissionAndTemplate(
+ workflow,
+ task,
+ accountId,
+ entityManager,
+ userRepo,
+ );
+ }
+
+ // 音声ファイルの情報からルーティングルールを取得できない場合は、
+ // API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得する
+ // API実行者のAuthorIdがない場合はエラーを出して終了
+ if (!myAuthorId) {
+ throw new Error(`There is no AuthorId for the API executor.`);
+ }
+ // API実行者のAuthorIdをもとにユーザーを取得
+ const myAuthorUser = await userRepo.findOne({
+ where: {
+ author_id: myAuthorId,
+ account_id: accountId,
+ },
+ });
+ if (!myAuthorUser) {
+ throw new Error(
+ `user not found. authorId:${myAuthorId}, accountId:${accountId}`,
+ );
+ }
+ const defaultWorkflow = await workflowRepo.findOne({
+ relations: {
+ workflowTypists: true,
+ },
+ where: {
+ account_id: accountId,
+ author_id: myAuthorUser.id,
+ worktype_id: worktypeRecord?.id ?? IsNull(),
+ },
+ });
+
+ // API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得できない場合はエラーを出して終了
+ if (!defaultWorkflow) {
+ throw new Error(
+ `workflow not found. authorUserId:${myAuthorUser.id}, accountId:${accountId}, worktypeId:${worktypeRecord?.id}`,
+ );
+ }
+
+ // Workflow(ルーティングルール)があればタスクのチェックアウト権限を設定する
+ return await this.setCheckoutPermissionAndTemplate(
+ defaultWorkflow,
+ task,
+ accountId,
+ entityManager,
+ userRepo,
+ );
+ });
+ }
+
+ /**
+ * workflowに紐づけられているタイピスト・タイピストグループで、タスクのチェックアウト権限を設定
+ * workflowに紐づけられているテンプレートファイルIDをタスクに設定
+ *
+ * @param workflow
+ * @param task
+ * @param accountId
+ * @param entityManager
+ * @param userRepo
+ * @returns checkout permission
+ */
+ private async setCheckoutPermissionAndTemplate(
+ workflow: Workflow,
+ task: Task,
+ accountId: number,
+ entityManager: EntityManager,
+ userRepo: Repository
temporary password: ${ramdomPassword}`;
+ const html = `
temporary password: ${ramdomPassword}`;
// メールを送信
await this.sendgridService.sendMail(
diff --git a/dictation_server/src/features/workflows/workflows.controller.ts b/dictation_server/src/features/workflows/workflows.controller.ts
index 4c6ac32..ff312e1 100644
--- a/dictation_server/src/features/workflows/workflows.controller.ts
+++ b/dictation_server/src/features/workflows/workflows.controller.ts
@@ -63,7 +63,9 @@ export class WorkflowsController {
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
- @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
+ @UseGuards(
+ RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
+ )
@Get()
async getWorkflows(@Req() req: Request): Promise