diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 30131c1..9d3f7d3 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -20,13 +20,26 @@ export const errorCodes = [ "E000104", // トークン署名エラー "E000105", // トークン発行元エラー "E000106", // トークンアルゴリズムエラー + "E000107", // トークン不足エラー + "E000108", // トークン権限エラー + "E000301", // ADB2Cへのリクエスト上限超過エラー + "E010001", // パラメータ形式不正エラー "E010201", // 未認証ユーザエラー "E010202", // 認証済ユーザエラー + "E010203", // 管理ユーザ権限エラー + "E010204", // ユーザ不在エラー + "E010205", // DBのRoleが想定外の値エラー + "E010206", // DBのTierが想定外の値エラー + "E010207", // ユーザーのRole変更不可エラー + "E010208", // ユーザーの暗号化パスワード不足エラー "E010301", // メールアドレス登録済みエラー "E010302", // authorId重複エラー "E010401", // PONumber重複エラー + "E010501", // アカウント不在エラー "E010601", // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) "E010602", // タスク変更権限不足エラー + "E010603", // タスク不在エラー + "E010701", // Blobファイル不在エラー "E010801", // ライセンス不在エラー "E010802", // ライセンス取り込み済みエラー ] as const; diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index 0616cac..8c35726 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -105,3 +105,86 @@ export const addUserAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const updateUserAsync = createAsyncThunk< + { + /* Empty Object */ + }, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("users/updateUserAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const usersApi = new UsersApi(config); + const { updateUser } = state.user.apps; + + const authorId = + updateUser.role === USER_ROLES.AUTHOR ? updateUser.authorId : undefined; + const encryption = + updateUser.role === USER_ROLES.AUTHOR ? updateUser.encryption : undefined; + const encryptionPassword = + updateUser.role === USER_ROLES.AUTHOR + ? updateUser.encryptionPassword + : undefined; + const prompt = + updateUser.role === USER_ROLES.AUTHOR ? updateUser.prompt : undefined; + + try { + await usersApi.updateUser( + { + id: updateUser.id, + role: updateUser.role, + authorId, + encryption, + encryptionPassword, + prompt, + autoRenew: updateUser.autoRenew, + licenseAlart: updateUser.licenseAlert, + notification: updateUser.notification, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換"z + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + // Roleが変更できない + if (error.code === "E010207") { + errorMessage = getTranslationID("userListPage.message.roleChangeError"); + } + // AuthorIdが重複している + if (error.code === "E010302") { + errorMessage = getTranslationID( + "userListPage.message.authorIdConflictError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/user/selectors.ts b/dictation_client/src/features/user/selectors.ts index a416b9f..854e02b 100644 --- a/dictation_client/src/features/user/selectors.ts +++ b/dictation_client/src/features/user/selectors.ts @@ -40,6 +40,47 @@ export const selectInputValidationErrors = (state: RootState) => { }; }; +export const selectUpdateValidationErrors = (state: RootState) => { + const { role, authorId, encryption, encryptionPassword } = + state.user.apps.updateUser; + const { encryption: initEncryption } = state.user.apps.selectedUser; + + // Authorの場合、AuthorIDが必須(空文字,undefinedは不可) + const hasErrorEmptyAuthorId = + role === USER_ROLES.AUTHOR && (authorId === "" || !authorId); + + const hasErrorIncorrectAuthorId = checkErrorIncorrectAuthorId( + authorId ?? undefined, + role + ); + + let hasErrorIncorrectEncryptionPassword = false; + + const passwordError = checkErrorIncorrectEncryptionPassword( + encryptionPassword, + role, + encryption + ); + + if (passwordError) { + // 最初にEncryptionがfasleで、Encryptionがtrueに変更された場合、EncryptionPasswordが必須 + if (!initEncryption) { + hasErrorIncorrectEncryptionPassword = true; + // Encryptionがある状態で変更がある場合、EncryptionPasswordが空でもエラーにしない + } else if (!encryptionPassword || encryptionPassword === "") { + hasErrorIncorrectEncryptionPassword = false; + } else { + hasErrorIncorrectEncryptionPassword = true; + } + } + + return { + hasErrorEmptyAuthorId, + hasErrorIncorrectAuthorId, + hasErrorIncorrectEncryptionPassword, + }; +}; + // encreyptionPasswordのチェック const checkErrorIncorrectEncryptionPassword = ( encryptionPassword: string | undefined, @@ -180,3 +221,9 @@ const convertValueBasedOnRole = ( typistGroupName: "-", }; }; + +export const selectUpdateUser = (state: RootState) => + state.user.apps.updateUser; + +export const selectHasPasswordMask = (state: RootState) => + state.user.apps.hasPasswordMask; diff --git a/dictation_client/src/features/user/state.ts b/dictation_client/src/features/user/state.ts index 9b6f3ee..503d798 100644 --- a/dictation_client/src/features/user/state.ts +++ b/dictation_client/src/features/user/state.ts @@ -1,5 +1,5 @@ import { User } from "../../api/api"; -import { AddUser } from "./types"; +import { AddUser, UpdateUser } from "./types"; export interface UsersState { domain: Domain; @@ -12,5 +12,8 @@ export interface Domain { export interface Apps { addUser: AddUser; + selectedUser: UpdateUser; + updateUser: UpdateUser; + hasPasswordMask: boolean; isLoading: boolean; } diff --git a/dictation_client/src/features/user/types.ts b/dictation_client/src/features/user/types.ts index 4af56bb..40d03f9 100644 --- a/dictation_client/src/features/user/types.ts +++ b/dictation_client/src/features/user/types.ts @@ -36,6 +36,20 @@ export interface AddUser { prompt?: boolean; } +export interface UpdateUser { + id: number; + name: string; + email: string; + role: RoleType; + authorId?: string | undefined; + encryption?: boolean | undefined; + encryptionPassword?: string | undefined; + prompt?: boolean | undefined; + autoRenew: boolean; + licenseAlert: boolean; + notification: boolean; +} + export type RoleType = typeof USER_ROLES[keyof typeof USER_ROLES]; // 受け取った値がUSER_ROLESの型であるかどうかを判定する diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index b7f6d2c..46066d7 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -1,12 +1,38 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { USER_ROLES } from "components/auth/constants"; import { UsersState } from "./state"; -import { addUserAsync, listUsersAsync } from "./operations"; +import { addUserAsync, listUsersAsync, updateUserAsync } from "./operations"; import { RoleType } from "./types"; const initialState: UsersState = { domain: { users: [] }, apps: { + updateUser: { + id: 0, + name: "", + email: "", + role: USER_ROLES.NONE, + authorId: undefined, + encryption: undefined, + encryptionPassword: undefined, + prompt: undefined, + autoRenew: true, + licenseAlert: true, + notification: true, + }, + selectedUser: { + id: 0, + name: "", + email: "", + role: USER_ROLES.NONE, + authorId: undefined, + encryption: undefined, + encryptionPassword: undefined, + prompt: undefined, + autoRenew: true, + licenseAlert: true, + notification: true, + }, addUser: { name: "", role: USER_ROLES.NONE, @@ -19,6 +45,7 @@ const initialState: UsersState = { prompt: false, encryptionPassword: "", }, + hasPasswordMask: false, isLoading: false, }, }; @@ -85,6 +112,108 @@ export const userSlice = createSlice({ cleanupAddUser: (state) => { state.apps.addUser = initialState.apps.addUser; }, + changeUpdateUser: (state, action: PayloadAction<{ id: number }>) => { + const { id } = action.payload; + + const user = state.domain.users.find((x) => x.id === id); + + if (!user) { + return; + } + + state.apps.updateUser.id = user.id; + state.apps.updateUser.name = user.name; + state.apps.updateUser.email = user.email; + state.apps.updateUser.role = user.role as RoleType; + state.apps.updateUser.authorId = user.authorId; + state.apps.updateUser.encryption = user.encryption; + state.apps.updateUser.encryptionPassword = undefined; + state.apps.updateUser.prompt = user.prompt; + state.apps.updateUser.autoRenew = user.autoRenew; + state.apps.updateUser.licenseAlert = user.licenseAlert; + state.apps.updateUser.notification = user.notification; + + state.apps.selectedUser.id = user.id; + state.apps.selectedUser.name = user.name; + state.apps.selectedUser.email = user.email; + state.apps.selectedUser.role = user.role as RoleType; + state.apps.selectedUser.authorId = user.authorId; + state.apps.selectedUser.encryption = user.encryption; + state.apps.selectedUser.encryptionPassword = undefined; + state.apps.selectedUser.prompt = user.prompt; + state.apps.selectedUser.autoRenew = user.autoRenew; + state.apps.selectedUser.licenseAlert = user.licenseAlert; + state.apps.selectedUser.notification = user.notification; + + state.apps.hasPasswordMask = user.encryption; + }, + changeUpdateRole: (state, action: PayloadAction<{ role: RoleType }>) => { + const { role } = action.payload; + state.apps.updateUser.role = role; + }, + changeUpdateAuthorId: ( + state, + action: PayloadAction<{ authorId: string }> + ) => { + const { authorId } = action.payload; + state.apps.updateUser.authorId = authorId; + }, + changeUpdateEncryption: ( + state, + action: PayloadAction<{ encryption: boolean }> + ) => { + const { encryption } = action.payload; + state.apps.updateUser.encryption = encryption; + const initEncryption = state.apps.selectedUser.encryption; + const password = state.apps.updateUser.encryptionPassword; + + if (initEncryption && encryption && !password) { + state.apps.hasPasswordMask = true; + } + }, + changeUpdateEncryptionPassword: ( + state, + action: PayloadAction<{ encryptionPassword: string }> + ) => { + const { encryptionPassword } = action.payload; + state.apps.updateUser.encryptionPassword = + encryptionPassword === "" ? undefined : encryptionPassword; + }, + changeUpdatePrompt: (state, action: PayloadAction<{ prompt: boolean }>) => { + const { prompt } = action.payload; + state.apps.updateUser.prompt = prompt; + }, + changeUpdateAutoRenew: ( + state, + action: PayloadAction<{ autoRenew: boolean }> + ) => { + const { autoRenew } = action.payload; + state.apps.updateUser.autoRenew = autoRenew; + }, + changeUpdateLicenseAlert: ( + state, + action: PayloadAction<{ licenseAlert: boolean }> + ) => { + const { licenseAlert } = action.payload; + state.apps.updateUser.licenseAlert = licenseAlert; + }, + changeUpdateNotification: ( + state, + action: PayloadAction<{ notification: boolean }> + ) => { + const { notification } = action.payload; + state.apps.updateUser.notification = notification; + }, + changeHasPasswordMask: ( + state, + action: PayloadAction<{ hasPasswordMask: boolean }> + ) => { + const { hasPasswordMask } = action.payload; + state.apps.hasPasswordMask = hasPasswordMask; + }, + cleanupUpdateUser: (state) => { + state.apps.updateUser = initialState.apps.updateUser; + }, }, extraReducers: (builder) => { builder.addCase(listUsersAsync.pending, (state) => { @@ -106,6 +235,15 @@ export const userSlice = createSlice({ builder.addCase(addUserAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(updateUserAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(updateUserAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateUserAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); @@ -118,9 +256,20 @@ export const { changeLicenseAlert, changeNotification, cleanupAddUser, + changeUpdateUser, + changeUpdateRole, + changeUpdateAuthorId, + changeUpdateEncryption, + changeUpdateEncryptionPassword, + changeUpdatePrompt, + changeUpdateAutoRenew, + changeUpdateLicenseAlert, + changeUpdateNotification, + cleanupUpdateUser, changeEncryption, changePrompt, changeEncryptionPassword, + changeHasPasswordMask, } = userSlice.actions; export default userSlice.reducer; diff --git a/dictation_client/src/pages/UserListPage/index.tsx b/dictation_client/src/pages/UserListPage/index.tsx index 3b590dd..787f902 100644 --- a/dictation_client/src/pages/UserListPage/index.tsx +++ b/dictation_client/src/pages/UserListPage/index.tsx @@ -16,22 +16,33 @@ import { isLicenseStatusType } from "features/user/types"; import { LICENSE_STATUS } from "features/user/constants"; import { isApproveTier } from "features/auth/utils"; import { TIERS } from "components/auth/constants"; +import { changeUpdateUser } from "features/user/userSlice"; import personAdd from "../../assets/images/person_add.svg"; import checkFill from "../../assets/images/check_fill.svg"; import checkOutline from "../../assets/images/check_outline.svg"; import progress_activit from "../../assets/images/progress_activit.svg"; import { UserAddPopup } from "./popup"; +import { UserUpdatePopup } from "./updatePopup"; const UserListPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); const [t] = useTranslation(); const [isPopupOpen, setIsPopupOpen] = useState(false); + const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false); const onOpen = useCallback(() => { setIsPopupOpen(true); }, [setIsPopupOpen]); + const onUpdateOpen = useCallback( + (id: number) => { + setIsUpdatePopupOpen(true); + dispatch(changeUpdateUser({ id })); + }, + [setIsUpdatePopupOpen, dispatch] + ); + useEffect(() => { // ユーザ一覧取得処理を呼び出す dispatch(listUsersAsync()); @@ -44,6 +55,13 @@ const UserListPage: React.FC = (): JSX.Element => { return ( <> + { + setIsUpdatePopupOpen(false); + dispatch(listUsersAsync()); + }} + /> { @@ -130,7 +148,12 @@ const UserListPage: React.FC = (): JSX.Element => {