From 45774236a800f074c80b787f7e6f5d980c8fc6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piggy=20Park=20=28=EB=B0=95=EC=9A=A9=ED=83=9C=29?= Date: Tue, 22 Jul 2025 12:37:51 +0900 Subject: [PATCH 01/21] feat(user-page): add user-MFA bulk setting modals (#6033) * chore: update user mfa setting params and model Signed-off-by: samuel.park * fix(user-table): add MFA management table action Signed-off-by: samuel.park * feat(user-mfa): add user bulk MFA setting modal and mutation Signed-off-by: samuel.park * feat(user-mfa): add user bulk MFA disable modal and mutation Signed-off-by: samuel.park * chore: add MFA modal components to user management page Signed-off-by: samuel.park * chore: add user MFA setting translations Signed-off-by: samuel.park * feat(user-mfa-setting): add single user select case Signed-off-by: samuel.park * chore: apply copilot reviews Signed-off-by: piggggggggy * chore: apply changed planning Signed-off-by: samuel.park --------- Signed-off-by: samuel.park Signed-off-by: piggggggggy --- .../identity/user/schema/api-verbs/create.ts | 3 + .../identity/user/schema/api-verbs/update.ts | 3 + .../api-clients/identity/user/schema/model.ts | 9 +- .../components/info-tooltip/InfoTooltip.vue | 4 +- .../iam/components/UserManagementTable.vue | 4 +- .../components/UserManagementTableToolbox.vue | 15 +- .../mfa/UserMFASecretKeyDeleteModal.vue | 106 +++++ .../components/mfa/UserMFASettingModal.vue | 280 ++++++++++++++ .../use-user-mfa-disable-mutation.ts | 50 +++ .../mutations/use-user-update-mutation.ts | 50 +++ .../services/iam/constants/user-constant.ts | 2 + .../src/services/iam/pages/UserMainPage.vue | 8 + .../src/services/iam/store/user-page-store.ts | 2 +- apps/web/src/services/iam/types/user-type.ts | 4 +- .../console-translation-2.8.babel | 362 ++++++++++++++++++ packages/language-pack/en.json | 19 + packages/language-pack/ja.json | 19 + packages/language-pack/ko.json | 19 + 18 files changed, 948 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/services/iam/components/mfa/UserMFASecretKeyDeleteModal.vue create mode 100644 apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue create mode 100644 apps/web/src/services/iam/composables/mutations/use-user-mfa-disable-mutation.ts create mode 100644 apps/web/src/services/iam/composables/mutations/use-user-update-mutation.ts diff --git a/apps/web/src/api-clients/identity/user/schema/api-verbs/create.ts b/apps/web/src/api-clients/identity/user/schema/api-verbs/create.ts index 7ad83faf97..81a72335c3 100644 --- a/apps/web/src/api-clients/identity/user/schema/api-verbs/create.ts +++ b/apps/web/src/api-clients/identity/user/schema/api-verbs/create.ts @@ -1,4 +1,5 @@ import type { Tags } from '@/api-clients/_common/schema/model'; +import type { MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type'; import type { AuthType } from '@/api-clients/identity/user/schema/type'; @@ -12,4 +13,6 @@ export interface UserCreateParameters { timezone?: string; tags?: Tags; reset_password?: boolean; + enforce_mfa?: boolean; + enforce_mfa_type?: MultiFactorAuthType; // only when enforce_mfa is true, this field is required } diff --git a/apps/web/src/api-clients/identity/user/schema/api-verbs/update.ts b/apps/web/src/api-clients/identity/user/schema/api-verbs/update.ts index c7278cc482..d73a90ce52 100644 --- a/apps/web/src/api-clients/identity/user/schema/api-verbs/update.ts +++ b/apps/web/src/api-clients/identity/user/schema/api-verbs/update.ts @@ -1,4 +1,5 @@ import type { Tags } from '@/api-clients/_common/schema/model'; +import type { MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type'; export interface UserUpdateParameters { @@ -10,4 +11,6 @@ export interface UserUpdateParameters { timezone?: string; tags?: Tags; reset_password?: boolean; + enforce_mfa?: boolean; + enforce_mfa_type?: MultiFactorAuthType; // only when enforce_mfa is true, this field is required } diff --git a/apps/web/src/api-clients/identity/user/schema/model.ts b/apps/web/src/api-clients/identity/user/schema/model.ts index 7edf82ba1d..b91a1dfe4e 100644 --- a/apps/web/src/api-clients/identity/user/schema/model.ts +++ b/apps/web/src/api-clients/identity/user/schema/model.ts @@ -14,7 +14,7 @@ export interface UserModel { auth_type: AuthType; // backend role_type: RoleType; role_id?: string; - mfa: UserMfa; + mfa?: UserMfa; language: string; timezone: string; api_key_count: number; @@ -26,9 +26,10 @@ export interface UserModel { } export interface UserMfa { - state: UserMfaState, - mfa_type: MultiFactorAuthType, - options: { + state?: UserMfaState, + mfa_type?: MultiFactorAuthType, + options?: { + enforce?: boolean, // if true, mfa_type is required email?: string, user_secret_id?: string, } diff --git a/apps/web/src/common/components/info-tooltip/InfoTooltip.vue b/apps/web/src/common/components/info-tooltip/InfoTooltip.vue index 5274de29c2..03550df8f2 100644 --- a/apps/web/src/common/components/info-tooltip/InfoTooltip.vue +++ b/apps/web/src/common/components/info-tooltip/InfoTooltip.vue @@ -1,9 +1,11 @@ + + diff --git a/apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue b/apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue new file mode 100644 index 0000000000..5df2f06419 --- /dev/null +++ b/apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue @@ -0,0 +1,280 @@ + + + + + + diff --git a/apps/web/src/services/iam/composables/mutations/use-user-mfa-disable-mutation.ts b/apps/web/src/services/iam/composables/mutations/use-user-mfa-disable-mutation.ts new file mode 100644 index 0000000000..fd63f8b1f8 --- /dev/null +++ b/apps/web/src/services/iam/composables/mutations/use-user-mfa-disable-mutation.ts @@ -0,0 +1,50 @@ +import { useMutation, useQueryClient } from '@tanstack/vue-query'; + +import { useUserApi } from '@/api-clients/identity/user/composables/use-user-api'; +import type { UserDisableMfaParameters } from '@/api-clients/identity/user/schema/api-verbs/disable-mfa'; +import type { UserModel } from '@/api-clients/identity/user/schema/model'; +import { useServiceQueryKey } from '@/query/query-key/use-service-query-key'; + +interface UseUserMfaDisableMutationOptions { + onSuccess?: (data: UserModel, variables: UserDisableMfaParameters) => Promise | void; + onError?: (error: Error, variables: UserDisableMfaParameters) => Promise | void; + onSettled?: (data: UserModel | undefined, error: Error | null, variables: UserDisableMfaParameters) => Promise | void; +} + +export const useUserMfaDisableMutation = ({ + onSuccess, + onError, + onSettled, +}: UseUserMfaDisableMutationOptions = {}) => { + const { userAPI } = useUserApi(); + + const queryClient = useQueryClient(); + const { withSuffix: userGetSuffix } = useServiceQueryKey('identity', 'user', 'get'); + const { key: userListKey } = useServiceQueryKey('identity', 'user', 'list'); + const { withSuffix: workspaceUserGetSuffix } = useServiceQueryKey('identity', 'workspace-user', 'get'); + const { key: workspaceUserListKey } = useServiceQueryKey('identity', 'workspace-user', 'list'); + + + return useMutation({ + mutationFn: (params: UserDisableMfaParameters) => { + if (!params.user_id) { + if (import.meta.env.DEV) throw new Error('[useUserMfaDisableMutation.ts] User ID is required'); + else throw new Error('[User MFA Disable] Something went wrong! Try again later. If the problem persists, please contact support.'); + } + return userAPI.disableMfa(params); + }, + onSuccess: async (data, variables) => { + await queryClient.invalidateQueries({ queryKey: userGetSuffix(variables.user_id) }); + await queryClient.invalidateQueries({ queryKey: userListKey.value }); + await queryClient.invalidateQueries({ queryKey: workspaceUserGetSuffix(variables.user_id) }); + await queryClient.invalidateQueries({ queryKey: workspaceUserListKey.value }); + if (onSuccess) await onSuccess(data, variables); + }, + onError: async (error, variables) => { + if (onError) await onError(error, variables); + }, + onSettled: async (data, error, variables) => { + if (onSettled) await onSettled(data, error, variables); + }, + }); +}; diff --git a/apps/web/src/services/iam/composables/mutations/use-user-update-mutation.ts b/apps/web/src/services/iam/composables/mutations/use-user-update-mutation.ts new file mode 100644 index 0000000000..0baf0a2e7d --- /dev/null +++ b/apps/web/src/services/iam/composables/mutations/use-user-update-mutation.ts @@ -0,0 +1,50 @@ +import { useMutation, useQueryClient } from '@tanstack/vue-query'; + +import { useUserApi } from '@/api-clients/identity/user/composables/use-user-api'; +import type { UserUpdateParameters } from '@/api-clients/identity/user/schema/api-verbs/update'; +import type { UserModel } from '@/api-clients/identity/user/schema/model'; +import { useServiceQueryKey } from '@/query/query-key/use-service-query-key'; + +interface UseUserUpdateMutationOptions { + onSuccess?: (data: UserModel, variables: UserUpdateParameters) => Promise | void; + onError?: (error: Error, variables: UserUpdateParameters) => Promise | void; + onSettled?: (data: UserModel | undefined, error: Error | null, variables: UserUpdateParameters) => Promise | void; +} + +export const useUserUpdateMutation = ({ + onSuccess, + onError, + onSettled, +}: UseUserUpdateMutationOptions = {}) => { + const { userAPI } = useUserApi(); + + const queryClient = useQueryClient(); + const { withSuffix: userGetSuffix } = useServiceQueryKey('identity', 'user', 'get'); + const { key: userListKey } = useServiceQueryKey('identity', 'user', 'list'); + const { withSuffix: workspaceUserGetSuffix } = useServiceQueryKey('identity', 'workspace-user', 'get'); + const { key: workspaceUserListKey } = useServiceQueryKey('identity', 'workspace-user', 'list'); + + + return useMutation({ + mutationFn: (params: UserUpdateParameters) => { + if (!params.user_id) { + if (import.meta.env.DEV) throw new Error('[useUserUpdateMutation.ts] User ID is required'); + else throw new Error('[User Update] Something went wrong! Try again later. If the problem persists, please contact support.'); + } + return userAPI.update(params); + }, + onSuccess: async (data, variables) => { + await queryClient.invalidateQueries({ queryKey: userGetSuffix(variables.user_id) }); + await queryClient.invalidateQueries({ queryKey: userListKey.value }); + await queryClient.invalidateQueries({ queryKey: workspaceUserGetSuffix(variables.user_id) }); + await queryClient.invalidateQueries({ queryKey: workspaceUserListKey.value }); + if (onSuccess) await onSuccess(data, variables); + }, + onError: async (error, variables) => { + if (onError) await onError(error, variables); + }, + onSettled: async (data, error, variables) => { + if (onSettled) await onSettled(data, error, variables); + }, + }); +}; diff --git a/apps/web/src/services/iam/constants/user-constant.ts b/apps/web/src/services/iam/constants/user-constant.ts index a198fd1beb..ed657fbd8b 100644 --- a/apps/web/src/services/iam/constants/user-constant.ts +++ b/apps/web/src/services/iam/constants/user-constant.ts @@ -19,6 +19,8 @@ export const USER_MODAL_TYPE = { DISABLE: 'disable', UPDATE: 'update', ASSIGN: 'assign', + SET_MFA: 'set-mfa', + DELETE_MFA_SECRET_KEY: 'delete-mfa-secret-key', } as const; // Table diff --git a/apps/web/src/services/iam/pages/UserMainPage.vue b/apps/web/src/services/iam/pages/UserMainPage.vue index 4d0b978897..0d4ea3cfd3 100644 --- a/apps/web/src/services/iam/pages/UserMainPage.vue +++ b/apps/web/src/services/iam/pages/UserMainPage.vue @@ -12,6 +12,8 @@ import { useAuthorizationStore } from '@/store/authorization/authorization-store import { useGrantScopeGuard } from '@/common/composables/grant-scope-guard'; import { usePageEditableStatus } from '@/common/composables/page-editable-status'; +import UserMFASecretKeyDeleteModal from '@/services/iam/components/mfa/UserMFASecretKeyDeleteModal.vue'; +import UserMFASettingModal from '@/services/iam/components/mfa/UserMFASettingModal.vue'; import UserAssignToGroupModal from '@/services/iam/components/UserAssignToGroupModal.vue'; import UserManagementAddModal from '@/services/iam/components/UserManagementAddModal.vue'; import UserManagementFormModal from '@/services/iam/components/UserManagementFormModal.vue'; @@ -103,6 +105,12 @@ onUnmounted(() => { + + diff --git a/apps/web/src/services/iam/store/user-page-store.ts b/apps/web/src/services/iam/store/user-page-store.ts index 75eed15da5..57ab779206 100644 --- a/apps/web/src/services/iam/store/user-page-store.ts +++ b/apps/web/src/services/iam/store/user-page-store.ts @@ -38,7 +38,7 @@ export const useUserPageStore = defineStore('page-user', () => { selectedUser: {} as UserListItemType, roles: [] as RoleModel[], totalCount: 0, - selectedIndices: [], + selectedIndices: [] as number[], pageStart: 1, pageLimit: 15, searchFilters: [] as ConsoleFilter[], diff --git a/apps/web/src/services/iam/types/user-type.ts b/apps/web/src/services/iam/types/user-type.ts index 2449b503af..babf7421c4 100644 --- a/apps/web/src/services/iam/types/user-type.ts +++ b/apps/web/src/services/iam/types/user-type.ts @@ -24,7 +24,7 @@ export interface ExtendUserListItemType extends UserListItemType { mfa_state?: 'ON'|'OFF' } -export type UserPageModalType = 'removeMixed' | 'removeOnlyWorkspaceGroup' | 'removeOnlyWorkspace' | 'add' | 'form' | 'status' | 'assignToUserGroup'; +export type UserPageModalType = 'removeMixed' | 'removeOnlyWorkspaceGroup' | 'removeOnlyWorkspace' | 'add' | 'form' | 'status' | 'assignToUserGroup' | 'setMfa' | 'deleteMfaSecretKey'; export interface ModalSettingState { type: string; @@ -36,7 +36,7 @@ export interface ModalSettingState { export interface ModalState { visible?: UserPageModalType; type: string; - title: string; + title: string | TranslateResult; themeColor: string; } diff --git a/packages/language-pack/console-translation-2.8.babel b/packages/language-pack/console-translation-2.8.babel index e1549e1c09..6c338587cb 100644 --- a/packages/language-pack/console-translation-2.8.babel +++ b/packages/language-pack/console-translation-2.8.babel @@ -60557,6 +60557,347 @@ + + MFA + + + DELETE_MFA_SECRET_KEY_BUTTON_TEXT + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + DELETE_MFA_SECRET_KEY_FAILED_MESSAGE + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + DELETE_MFA_SECRET_KEY_MODAL_DESC + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + DELETE_MFA_SECRET_KEY_MODAL_TITLE + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + DELETE_MFA_SECRET_KEY_SUCCESS_MESSAGE + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + MFA_TYPE_NO_ACTIVE_METHOD + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + MFA_TYPE_SELECT_FIELD_TITLE + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + MFA_TYPE_SELECT_FIELD_TOOLTIP_TEXT + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + REQUIRED_FIELD_TOOLTIP_TEXT + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + REQUIRED_SETTING_FIELD_TITLE + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + SET_MFA_MODAL_CONFIRM_BUTTON_TEXT + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + SET_MFA_MODAL_TITLE + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + SET_MFA_SUCCESS_MESSAGE + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + SET_MFA_WARNING_DESC + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + SET_MFA_WARNING_TITLE + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + SINGLE_USER_MFA_TYPE_INFO_TEXT + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + NON_REMOVABLE false @@ -60979,6 +61320,27 @@ + + SET_MFA + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + STATE false diff --git a/packages/language-pack/en.json b/packages/language-pack/en.json index fd79b4036f..16e5878287 100644 --- a/packages/language-pack/en.json +++ b/packages/language-pack/en.json @@ -3280,6 +3280,24 @@ "GO_TO_WORKSPACE_GROUP": "Go to set up workspace groups", "INVITE_TITLE": "Invite Users to '{workspace_name}'", "KEEP_ONLY_REMOVABLE_URERS": "Keep only removable users in the list below", + "MFA": { + "DELETE_MFA_SECRET_KEY_BUTTON_TEXT": "Delete MFA Secret Key", + "DELETE_MFA_SECRET_KEY_FAILED_MESSAGE": "", + "DELETE_MFA_SECRET_KEY_MODAL_DESC": "Deletes the Multi-factor Authentication(MFA) secret key for the [{selectedUsers}] selected users.", + "DELETE_MFA_SECRET_KEY_MODAL_TITLE": "Delete Multi-factor Authentication Secret Key", + "DELETE_MFA_SECRET_KEY_SUCCESS_MESSAGE": "MFA secret key successfully deleted for the selected users.", + "MFA_TYPE_NO_ACTIVE_METHOD": "No active method", + "MFA_TYPE_SELECT_FIELD_TITLE": "Multi-factor Authentication Method", + "MFA_TYPE_SELECT_FIELD_TOOLTIP_TEXT": ".", + "REQUIRED_FIELD_TOOLTIP_TEXT": "Choose whether to require Multi-Factor Authentication (MFA) for the selected users.", + "REQUIRED_SETTING_FIELD_TITLE": "Multi-factor Authentication Required Settings", + "SET_MFA_MODAL_CONFIRM_BUTTON_TEXT": "Save Changes", + "SET_MFA_MODAL_TITLE": "Set Multi-factor Authentication Policy", + "SET_MFA_SUCCESS_MESSAGE": "MFA settings applied to the selected users.", + "SET_MFA_WARNING_DESC": "This setting only affects selected Local Users, as it is not applicable to External Users.", + "SET_MFA_WARNING_TITLE": "External Users Excluded", + "SINGLE_USER_MFA_TYPE_INFO_TEXT": "MFA type currently in use by this account" + }, "NON_REMOVABLE": "Non-Removable", "REMOVABLE": "Removable", "REMOVE_MIXED_TYPE_DESC": "To exclude the non-removable users from the list and click the button below to proceed. ", @@ -3301,6 +3319,7 @@ "ROLE": "Role", "ROLE_NAME": "Role Name", "ROLE_TYPE": "Role Type", + "SET_MFA": "Set MFA", "STATE": "State", "TAB_SELECTED_DATA": "Selected Data", "TAG": "Tag", diff --git a/packages/language-pack/ja.json b/packages/language-pack/ja.json index a42a539d45..5ad578664e 100644 --- a/packages/language-pack/ja.json +++ b/packages/language-pack/ja.json @@ -3280,6 +3280,24 @@ "GO_TO_WORKSPACE_GROUP": "ワークスペースグループの設定に進む", "INVITE_TITLE": "ユーザーを'{workspace_name}'に招待", "KEEP_ONLY_REMOVABLE_URERS": "以下のリストには削除可能なユーザーのみを残してください。", + "MFA": { + "DELETE_MFA_SECRET_KEY_BUTTON_TEXT": "", + "DELETE_MFA_SECRET_KEY_FAILED_MESSAGE": "", + "DELETE_MFA_SECRET_KEY_MODAL_DESC": "", + "DELETE_MFA_SECRET_KEY_MODAL_TITLE": "", + "DELETE_MFA_SECRET_KEY_SUCCESS_MESSAGE": "", + "MFA_TYPE_NO_ACTIVE_METHOD": "", + "MFA_TYPE_SELECT_FIELD_TITLE": "", + "MFA_TYPE_SELECT_FIELD_TOOLTIP_TEXT": "", + "REQUIRED_FIELD_TOOLTIP_TEXT": "", + "REQUIRED_SETTING_FIELD_TITLE": "", + "SET_MFA_MODAL_CONFIRM_BUTTON_TEXT": "", + "SET_MFA_MODAL_TITLE": "", + "SET_MFA_SUCCESS_MESSAGE": "", + "SET_MFA_WARNING_DESC": "", + "SET_MFA_WARNING_TITLE": "", + "SINGLE_USER_MFA_TYPE_INFO_TEXT": "" + }, "NON_REMOVABLE": "削除不可能", "REMOVABLE": "削除可能", "REMOVE_MIXED_TYPE_DESC": "削除できないユーザーをリストから除外し、下のボタンをクリックして続行してください。", @@ -3301,6 +3319,7 @@ "ROLE": "ロール", "ROLE_NAME": "ロール名", "ROLE_TYPE": "ロールタイプ", + "SET_MFA": "", "STATE": "状態", "TAB_SELECTED_DATA": "選択したデータ", "TAG": "タグ", diff --git a/packages/language-pack/ko.json b/packages/language-pack/ko.json index 2c334f32e9..450215c181 100644 --- a/packages/language-pack/ko.json +++ b/packages/language-pack/ko.json @@ -3280,6 +3280,24 @@ "GO_TO_WORKSPACE_GROUP": "워크스페이스 그룹 설정하러 가기", "INVITE_TITLE": "'{workspace_name}'에 사용자 추가", "KEEP_ONLY_REMOVABLE_URERS": "아래 목록 중 제거 가능한 사용자만 보기", + "MFA": { + "DELETE_MFA_SECRET_KEY_BUTTON_TEXT": "", + "DELETE_MFA_SECRET_KEY_FAILED_MESSAGE": "", + "DELETE_MFA_SECRET_KEY_MODAL_DESC": "", + "DELETE_MFA_SECRET_KEY_MODAL_TITLE": "", + "DELETE_MFA_SECRET_KEY_SUCCESS_MESSAGE": "", + "MFA_TYPE_NO_ACTIVE_METHOD": "", + "MFA_TYPE_SELECT_FIELD_TITLE": "", + "MFA_TYPE_SELECT_FIELD_TOOLTIP_TEXT": "", + "REQUIRED_FIELD_TOOLTIP_TEXT": "", + "REQUIRED_SETTING_FIELD_TITLE": "", + "SET_MFA_MODAL_CONFIRM_BUTTON_TEXT": "", + "SET_MFA_MODAL_TITLE": "", + "SET_MFA_SUCCESS_MESSAGE": "", + "SET_MFA_WARNING_DESC": "", + "SET_MFA_WARNING_TITLE": "", + "SINGLE_USER_MFA_TYPE_INFO_TEXT": "이 계정이 현재 사용 중인 수단" + }, "NON_REMOVABLE": "삭제 불가능", "REMOVABLE": "삭제 가능", "REMOVE_MIXED_TYPE_DESC": "아래 버튼을 클릭하면 제거 불가한 사용자들을 항목에서 제외할 수 있습니다.", @@ -3301,6 +3319,7 @@ "ROLE": "역할(Role)", "ROLE_NAME": "역할(Role)명", "ROLE_TYPE": "역할(Role) 타입", + "SET_MFA": "", "STATE": "상태", "TAB_SELECTED_DATA": "선택한 데이터 ", "TAG": "태그", From 9607cbf9552d1f72f904ac09f5fa2819214bd611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piggy=20Park=20=28=EB=B0=95=EC=9A=A9=ED=83=9C=29?= Date: Wed, 23 Jul 2025 10:02:29 +0900 Subject: [PATCH 02/21] feat(mfa-disable): create New `modal-controller` func component & separate mfa-disable button and modal (#6038) * feat(modal-controller): creat modal-controller wrapper component Signed-off-by: samuel.park * feat(MFA-disable): separate disable button & apply modal-controller Signed-off-by: samuel.park * feat: apply separated MFA-disable button Signed-off-by: samuel.park * chore: refactor mfa-disable mutation composable Signed-off-by: samuel.park * chore: add translation Signed-off-by: samuel.park * chore: small fix Signed-off-by: samuel.park * chore: typo Signed-off-by: samuel.park * chore: typo Signed-off-by: samuel.park --------- Signed-off-by: samuel.park --- .../components/modals/ModalController.vue | 46 +++++++ .../mfa/UserMFASettingDisableButton.vue | 118 ++++++++++++++++++ .../components/mfa/UserMFASettingModal.vue | 31 ++--- .../use-user-mfa-disable-mutation.ts | 14 ++- .../services/iam/constants/modal.constant.ts | 4 + .../src/services/iam/pages/UserMainPage.vue | 8 +- .../console-translation-2.8.babel | 27 +++- packages/language-pack/en.json | 3 +- packages/language-pack/ja.json | 3 +- packages/language-pack/ko.json | 3 +- 10 files changed, 221 insertions(+), 36 deletions(-) create mode 100644 apps/web/src/common/components/modals/ModalController.vue create mode 100644 apps/web/src/services/iam/components/mfa/UserMFASettingDisableButton.vue create mode 100644 apps/web/src/services/iam/constants/modal.constant.ts diff --git a/apps/web/src/common/components/modals/ModalController.vue b/apps/web/src/common/components/modals/ModalController.vue new file mode 100644 index 0000000000..4f5ca450c3 --- /dev/null +++ b/apps/web/src/common/components/modals/ModalController.vue @@ -0,0 +1,46 @@ + + + diff --git a/apps/web/src/services/iam/components/mfa/UserMFASettingDisableButton.vue b/apps/web/src/services/iam/components/mfa/UserMFASettingDisableButton.vue new file mode 100644 index 0000000000..2e89255a7f --- /dev/null +++ b/apps/web/src/services/iam/components/mfa/UserMFASettingDisableButton.vue @@ -0,0 +1,118 @@ + + + diff --git a/apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue b/apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue index 5df2f06419..2b34009557 100644 --- a/apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue +++ b/apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue @@ -3,7 +3,7 @@ import { computed, ref } from 'vue'; import type { TranslateResult } from 'vue-i18n'; import { - PButtonModal, PScopedNotification, PFieldGroup, PDivider, PToggleButton, PRadioGroup, PRadio, PButton, PCollapsibleToggle, PStatus, + PButtonModal, PScopedNotification, PFieldGroup, PDivider, PToggleButton, PRadioGroup, PRadio, PCollapsibleToggle, PStatus, } from '@cloudforet/mirinae'; import type { StatusTheme } from '@cloudforet/mirinae/types/data-display/status/type'; @@ -19,13 +19,13 @@ import ErrorHandler from '@/common/composables/error/errorHandler'; import { gray, green, red } from '@/styles/colors'; +import UserMFASettingDisableButton from '@/services/iam/components/mfa/UserMFASettingDisableButton.vue'; import { useUserUpdateMutation } from '@/services/iam/composables/mutations/use-user-update-mutation'; -import { MULTI_FACTOR_AUTH_ITEMS, USER_MODAL_TYPE } from '@/services/iam/constants/user-constant'; +import { MULTI_FACTOR_AUTH_ITEMS } from '@/services/iam/constants/user-constant'; import { useUserPageStore } from '@/services/iam/store/user-page-store'; import type { UserListItemType } from '@/services/iam/types/user-type'; - /* Store */ const userPageStore = useUserPageStore(); const userPageState = userPageStore.state; @@ -64,9 +64,12 @@ const singleUserMfaTypeStatusTheme = computed(() => { return selectedUserMFAState === 'ENABLED' ? green[600] : red[500]; }); +// MFA Disable Target +const selectedMFAEnabledUsers = computed(() => userPageGetters.selectedUsers.filter((user) => user.auth_type === 'LOCAL' && user.mfa?.state === 'ENABLED') || []); + + // UI Conditions const isIncludedExternalAuthTypeUser = computed(() => userPageGetters.selectedUsers.some((user) => user.auth_type !== 'LOCAL')); -const isDisableButtonEnabled = computed(() => userPageGetters.selectedUsers.some((user) => user.mfa?.state === 'ENABLED')); /* API */ @@ -108,16 +111,6 @@ const handleChangeMfaType = (value: MultiFactorAuthType) => { selectedMfaType.value = value; }; -const handleDeleteMfaSecretKey = () => { - if (!isDisableButtonEnabled.value) return; - userPageStore.updateModalSettings({ - type: USER_MODAL_TYPE.DELETE_MFA_SECRET_KEY, - title: '', - themeColor: 'alert', - modalVisibleType: 'deleteMfaSecretKey', - }); -}; - const handleConfirm = async () => { if (isUpdateUserPending.value) return; @@ -250,15 +243,7 @@ const handleConfirm = async () => { - - {{ $t('IAM.USER.MAIN.MODAL.MFA.DELETE_MFA_SECRET_KEY_BUTTON_TEXT') }} - + diff --git a/apps/web/src/services/iam/composables/mutations/use-user-mfa-disable-mutation.ts b/apps/web/src/services/iam/composables/mutations/use-user-mfa-disable-mutation.ts index fd63f8b1f8..fcd7f67a08 100644 --- a/apps/web/src/services/iam/composables/mutations/use-user-mfa-disable-mutation.ts +++ b/apps/web/src/services/iam/composables/mutations/use-user-mfa-disable-mutation.ts @@ -9,12 +9,16 @@ interface UseUserMfaDisableMutationOptions { onSuccess?: (data: UserModel, variables: UserDisableMfaParameters) => Promise | void; onError?: (error: Error, variables: UserDisableMfaParameters) => Promise | void; onSettled?: (data: UserModel | undefined, error: Error | null, variables: UserDisableMfaParameters) => Promise | void; + mutationOptions?: { + manualInvalidate?: boolean; + }; } export const useUserMfaDisableMutation = ({ onSuccess, onError, onSettled, + mutationOptions = { manualInvalidate: false }, }: UseUserMfaDisableMutationOptions = {}) => { const { userAPI } = useUserApi(); @@ -34,10 +38,12 @@ export const useUserMfaDisableMutation = ({ return userAPI.disableMfa(params); }, onSuccess: async (data, variables) => { - await queryClient.invalidateQueries({ queryKey: userGetSuffix(variables.user_id) }); - await queryClient.invalidateQueries({ queryKey: userListKey.value }); - await queryClient.invalidateQueries({ queryKey: workspaceUserGetSuffix(variables.user_id) }); - await queryClient.invalidateQueries({ queryKey: workspaceUserListKey.value }); + if (!mutationOptions?.manualInvalidate) { + await queryClient.invalidateQueries({ queryKey: userGetSuffix(variables.user_id) }); + await queryClient.invalidateQueries({ queryKey: userListKey.value }); + await queryClient.invalidateQueries({ queryKey: workspaceUserGetSuffix(variables.user_id) }); + await queryClient.invalidateQueries({ queryKey: workspaceUserListKey.value }); + } if (onSuccess) await onSuccess(data, variables); }, onError: async (error, variables) => { diff --git a/apps/web/src/services/iam/constants/modal.constant.ts b/apps/web/src/services/iam/constants/modal.constant.ts new file mode 100644 index 0000000000..295c0220a0 --- /dev/null +++ b/apps/web/src/services/iam/constants/modal.constant.ts @@ -0,0 +1,4 @@ +export const USER_MODAL_MAP = { + SET_MFA: 'set-mfa', + DISABLE_MFA: 'disable-mfa', +} as const; diff --git a/apps/web/src/services/iam/pages/UserMainPage.vue b/apps/web/src/services/iam/pages/UserMainPage.vue index 0d4ea3cfd3..92b015d41f 100644 --- a/apps/web/src/services/iam/pages/UserMainPage.vue +++ b/apps/web/src/services/iam/pages/UserMainPage.vue @@ -12,7 +12,6 @@ import { useAuthorizationStore } from '@/store/authorization/authorization-store import { useGrantScopeGuard } from '@/common/composables/grant-scope-guard'; import { usePageEditableStatus } from '@/common/composables/page-editable-status'; -import UserMFASecretKeyDeleteModal from '@/services/iam/components/mfa/UserMFASecretKeyDeleteModal.vue'; import UserMFASettingModal from '@/services/iam/components/mfa/UserMFASettingModal.vue'; import UserAssignToGroupModal from '@/services/iam/components/UserAssignToGroupModal.vue'; import UserManagementAddModal from '@/services/iam/components/UserManagementAddModal.vue'; @@ -25,8 +24,10 @@ import UserManagementRemoveMixedTypeModal import UserManagementStatusModal from '@/services/iam/components/UserManagementStatusModal.vue'; import UserManagementTab from '@/services/iam/components/UserManagementTab.vue'; import UserManagementTable from '@/services/iam/components/UserManagementTable.vue'; +import { USER_MODAL_MAP } from '@/services/iam/constants/modal.constant'; import { useUserPageStore } from '@/services/iam/store/user-page-store'; + const appContextStore = useAppContextStore(); const userPageStore = useUserPageStore(); const userPageState = userPageStore.state; @@ -108,9 +109,10 @@ onUnmounted(() => { - + /> --> + diff --git a/packages/language-pack/console-translation-2.8.babel b/packages/language-pack/console-translation-2.8.babel index 6c338587cb..5f7be3445e 100644 --- a/packages/language-pack/console-translation-2.8.babel +++ b/packages/language-pack/console-translation-2.8.babel @@ -60603,7 +60603,7 @@ - DELETE_MFA_SECRET_KEY_MODAL_DESC + DELETE_MFA_SECRET_KEY_MODAL_TITLE false @@ -60624,7 +60624,7 @@ - DELETE_MFA_SECRET_KEY_MODAL_TITLE + DELETE_MFA_SECRET_KEY_SUCCESS_MESSAGE false @@ -60645,7 +60645,28 @@ - DELETE_MFA_SECRET_KEY_SUCCESS_MESSAGE + DELETE_MULTI_MFA_SECRET_KEY_MODAL_DESC + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + + + DELETE_SINGLE_MFA_SECRET_KEY_MODAL_DESC false diff --git a/packages/language-pack/en.json b/packages/language-pack/en.json index 16e5878287..c4debb0b47 100644 --- a/packages/language-pack/en.json +++ b/packages/language-pack/en.json @@ -3283,9 +3283,10 @@ "MFA": { "DELETE_MFA_SECRET_KEY_BUTTON_TEXT": "Delete MFA Secret Key", "DELETE_MFA_SECRET_KEY_FAILED_MESSAGE": "", - "DELETE_MFA_SECRET_KEY_MODAL_DESC": "Deletes the Multi-factor Authentication(MFA) secret key for the [{selectedUsers}] selected users.", "DELETE_MFA_SECRET_KEY_MODAL_TITLE": "Delete Multi-factor Authentication Secret Key", "DELETE_MFA_SECRET_KEY_SUCCESS_MESSAGE": "MFA secret key successfully deleted for the selected users.", + "DELETE_MULTI_MFA_SECRET_KEY_MODAL_DESC": "Deletes the Multi-factor Authentication(MFA) secret key for the [{selectedUsers}] selected users.", + "DELETE_SINGLE_MFA_SECRET_KEY_MODAL_DESC": "Deletes the Multi-factor Authentication(MFA) secret key for the selected user.", "MFA_TYPE_NO_ACTIVE_METHOD": "No active method", "MFA_TYPE_SELECT_FIELD_TITLE": "Multi-factor Authentication Method", "MFA_TYPE_SELECT_FIELD_TOOLTIP_TEXT": ".", diff --git a/packages/language-pack/ja.json b/packages/language-pack/ja.json index 5ad578664e..7df98fa028 100644 --- a/packages/language-pack/ja.json +++ b/packages/language-pack/ja.json @@ -3283,9 +3283,10 @@ "MFA": { "DELETE_MFA_SECRET_KEY_BUTTON_TEXT": "", "DELETE_MFA_SECRET_KEY_FAILED_MESSAGE": "", - "DELETE_MFA_SECRET_KEY_MODAL_DESC": "", "DELETE_MFA_SECRET_KEY_MODAL_TITLE": "", "DELETE_MFA_SECRET_KEY_SUCCESS_MESSAGE": "", + "DELETE_MULTI_MFA_SECRET_KEY_MODAL_DESC": "", + "DELETE_SINGLE_MFA_SECRET_KEY_MODAL_DESC": "", "MFA_TYPE_NO_ACTIVE_METHOD": "", "MFA_TYPE_SELECT_FIELD_TITLE": "", "MFA_TYPE_SELECT_FIELD_TOOLTIP_TEXT": "", diff --git a/packages/language-pack/ko.json b/packages/language-pack/ko.json index 450215c181..7fe7458984 100644 --- a/packages/language-pack/ko.json +++ b/packages/language-pack/ko.json @@ -3283,9 +3283,10 @@ "MFA": { "DELETE_MFA_SECRET_KEY_BUTTON_TEXT": "", "DELETE_MFA_SECRET_KEY_FAILED_MESSAGE": "", - "DELETE_MFA_SECRET_KEY_MODAL_DESC": "", "DELETE_MFA_SECRET_KEY_MODAL_TITLE": "", "DELETE_MFA_SECRET_KEY_SUCCESS_MESSAGE": "", + "DELETE_MULTI_MFA_SECRET_KEY_MODAL_DESC": "", + "DELETE_SINGLE_MFA_SECRET_KEY_MODAL_DESC": "", "MFA_TYPE_NO_ACTIVE_METHOD": "", "MFA_TYPE_SELECT_FIELD_TITLE": "", "MFA_TYPE_SELECT_FIELD_TOOLTIP_TEXT": "", From 4736aa56f6855ddd27224c01f20191b4eb48af79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piggy=20Park=20=28=EB=B0=95=EC=9A=A9=ED=83=9C=29?= Date: Wed, 23 Jul 2025 15:53:50 +0900 Subject: [PATCH 03/21] chore: separate and apply mfa-setting form components (#6039) * chore: edit change MFA request Signed-off-by: samuel.park * fix(mfa-setting): separate mfa-setting components Signed-off-by: samuel.park * chore: apply separated components Signed-off-by: samuel.park * fix(existing-user-modal): apply mfa-setting form section Signed-off-by: samuel.park * chore: small fix Signed-off-by: samuel.park * chore: edit annotations Signed-off-by: samuel.park --------- Signed-off-by: samuel.park --- .../identity/user-profile/schema/constant.ts | 5 + .../identity/user-profile/schema/type.ts | 3 +- .../identity/user/schema/api-verbs/create.ts | 6 +- .../identity/user/schema/api-verbs/update.ts | 6 +- .../workspace-user/schema/api-verbs/create.ts | 3 + .../iam/components/UserManagementAddModal.vue | 26 +- .../components/UserManagementFormModal.vue | 71 +++-- .../mfa/UserBulkMFASettingModal.vue | 150 ++++++++++ .../mfa/UserMFASettingDisableButton.vue | 8 +- .../mfa/UserMFASettingEnforceForm.vue | 104 +++++++ .../mfa/UserMFASettingFormLayout.vue | 133 +++++++++ .../components/mfa/UserMFASettingModal.vue | 265 ------------------ .../src/services/iam/pages/UserMainPage.vue | 6 +- .../src/services/iam/store/user-page-store.ts | 2 +- 14 files changed, 472 insertions(+), 316 deletions(-) create mode 100644 apps/web/src/services/iam/components/mfa/UserBulkMFASettingModal.vue create mode 100644 apps/web/src/services/iam/components/mfa/UserMFASettingEnforceForm.vue create mode 100644 apps/web/src/services/iam/components/mfa/UserMFASettingFormLayout.vue delete mode 100644 apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue diff --git a/apps/web/src/api-clients/identity/user-profile/schema/constant.ts b/apps/web/src/api-clients/identity/user-profile/schema/constant.ts index 2eb11920bf..be88fccd66 100644 --- a/apps/web/src/api-clients/identity/user-profile/schema/constant.ts +++ b/apps/web/src/api-clients/identity/user-profile/schema/constant.ts @@ -2,3 +2,8 @@ export const MULTI_FACTOR_AUTH_TYPE = { OTP: 'OTP', EMAIL: 'EMAIL', } as const; + +export const MFA_STATE = { + ENABLED: 'ENABLED', + DISABLED: 'DISABLED', +} as const; diff --git a/apps/web/src/api-clients/identity/user-profile/schema/type.ts b/apps/web/src/api-clients/identity/user-profile/schema/type.ts index 1c6a813feb..d1d614f889 100644 --- a/apps/web/src/api-clients/identity/user-profile/schema/type.ts +++ b/apps/web/src/api-clients/identity/user-profile/schema/type.ts @@ -1,3 +1,4 @@ -import type { MULTI_FACTOR_AUTH_TYPE } from '@/api-clients/identity/user-profile/schema/constant'; +import type { MFA_STATE, MULTI_FACTOR_AUTH_TYPE } from '@/api-clients/identity/user-profile/schema/constant'; export type MultiFactorAuthType = typeof MULTI_FACTOR_AUTH_TYPE[keyof typeof MULTI_FACTOR_AUTH_TYPE]; +export type MfaState = typeof MFA_STATE[keyof typeof MFA_STATE]; diff --git a/apps/web/src/api-clients/identity/user/schema/api-verbs/create.ts b/apps/web/src/api-clients/identity/user/schema/api-verbs/create.ts index 81a72335c3..e55cbc6693 100644 --- a/apps/web/src/api-clients/identity/user/schema/api-verbs/create.ts +++ b/apps/web/src/api-clients/identity/user/schema/api-verbs/create.ts @@ -1,5 +1,5 @@ import type { Tags } from '@/api-clients/_common/schema/model'; -import type { MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type'; +import type { MfaState, MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type'; import type { AuthType } from '@/api-clients/identity/user/schema/type'; @@ -13,6 +13,6 @@ export interface UserCreateParameters { timezone?: string; tags?: Tags; reset_password?: boolean; - enforce_mfa?: boolean; - enforce_mfa_type?: MultiFactorAuthType; // only when enforce_mfa is true, this field is required + enforce_mfa_state?: MfaState; + enforce_mfa_type?: MultiFactorAuthType; // only when enforce_mfa_state is ENABLED, this field is required } diff --git a/apps/web/src/api-clients/identity/user/schema/api-verbs/update.ts b/apps/web/src/api-clients/identity/user/schema/api-verbs/update.ts index d73a90ce52..dd46464b3c 100644 --- a/apps/web/src/api-clients/identity/user/schema/api-verbs/update.ts +++ b/apps/web/src/api-clients/identity/user/schema/api-verbs/update.ts @@ -1,5 +1,5 @@ import type { Tags } from '@/api-clients/_common/schema/model'; -import type { MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type'; +import type { MfaState, MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type'; export interface UserUpdateParameters { @@ -11,6 +11,6 @@ export interface UserUpdateParameters { timezone?: string; tags?: Tags; reset_password?: boolean; - enforce_mfa?: boolean; - enforce_mfa_type?: MultiFactorAuthType; // only when enforce_mfa is true, this field is required + enforce_mfa_state?: MfaState; + enforce_mfa_type?: MultiFactorAuthType; // only when enforce_mfa_state is ENABLED, this field is required } diff --git a/apps/web/src/api-clients/identity/workspace-user/schema/api-verbs/create.ts b/apps/web/src/api-clients/identity/workspace-user/schema/api-verbs/create.ts index ed17d6383a..5a36caaa45 100644 --- a/apps/web/src/api-clients/identity/workspace-user/schema/api-verbs/create.ts +++ b/apps/web/src/api-clients/identity/workspace-user/schema/api-verbs/create.ts @@ -1,4 +1,5 @@ import type { Tags } from '@/api-clients/_common/schema/model'; +import type { MfaState, MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type'; import type { AuthType } from '@/api-clients/identity/user/schema/type'; export interface WorkspaceUserCreateParameters { @@ -11,5 +12,7 @@ export interface WorkspaceUserCreateParameters { timezone?: string; tags?: Tags; reset_password?: boolean; + enforce_mfa_state?: MfaState; + enforce_mfa_type?: MultiFactorAuthType; role_id: string; } diff --git a/apps/web/src/services/iam/components/UserManagementAddModal.vue b/apps/web/src/services/iam/components/UserManagementAddModal.vue index 32d91aa705..f886c78d69 100644 --- a/apps/web/src/services/iam/components/UserManagementAddModal.vue +++ b/apps/web/src/services/iam/components/UserManagementAddModal.vue @@ -13,6 +13,8 @@ import { RESOURCE_GROUP } from '@/api-clients/_common/schema/constant'; import type { Tags } from '@/api-clients/_common/schema/model'; import type { RoleCreateParameters } from '@/api-clients/identity/role-binding/schema/api-verbs/create'; import type { RoleBindingModel } from '@/api-clients/identity/role-binding/schema/model'; +import { MFA_STATE } from '@/api-clients/identity/user-profile/schema/constant'; +import type { MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type'; import type { UserCreateParameters } from '@/api-clients/identity/user/schema/api-verbs/create'; import type { UserModel } from '@/api-clients/identity/user/schema/model'; import type { AuthType } from '@/api-clients/identity/user/schema/type'; @@ -29,16 +31,21 @@ import { showSuccessMessage } from '@/lib/helper/notice-alert-helper'; import ErrorHandler from '@/common/composables/error/errorHandler'; +import UserMFASettingEnforceForm from '@/services/iam/components/mfa/UserMFASettingEnforceForm.vue'; import UserManagementAddAdminRole from '@/services/iam/components/UserManagementAddAdminRole.vue'; import UserManagementAddPassword from '@/services/iam/components/UserManagementAddPassword.vue'; import UserManagementAddRole from '@/services/iam/components/UserManagementAddRole.vue'; import UserManagementAddTag from '@/services/iam/components/UserManagementAddTag.vue'; import UserManagementAddUser from '@/services/iam/components/UserManagementAddUser.vue'; -import { USER_MODAL_TYPE } from '@/services/iam/constants/user-constant'; +import { MULTI_FACTOR_AUTH_ITEMS, USER_MODAL_TYPE } from '@/services/iam/constants/user-constant'; import { checkEmailFormat } from '@/services/iam/helpers/user-management-form-validations'; import { useUserPageStore } from '@/services/iam/store/user-page-store'; import type { AddModalMenuItem, AddAdminRoleFormState } from '@/services/iam/types/user-type'; +interface UserMFASettingFormState { + isRequiredMfa: boolean; + selectedMfaType: MultiFactorAuthType; +} const userPageStore = useUserPageStore(); const userPageState = userPageStore.state; @@ -84,6 +91,11 @@ const state = reactive({ tags: {} as Tags, }); +const mfaSettingState = reactive({ + isRequiredMfa: false, + selectedMfaType: MULTI_FACTOR_AUTH_ITEMS[0].type, +}); + /* Component */ const handleAdminRoleChangeInput = (items: AddAdminRoleFormState) => { if (items.role) state.role = items.role; @@ -147,6 +159,11 @@ const fetchCreateUser = async (item: AddModalMenuItem): Promise => { timezone: domainSettings?.timezone || 'UTC', }; + if (userPageState.isAdminMode && userInfoParams.auth_type === 'LOCAL') { + userInfoParams.enforce_mfa_state = mfaSettingState.isRequiredMfa ? MFA_STATE.ENABLED : MFA_STATE.DISABLED; + userInfoParams.enforce_mfa_type = mfaSettingState.isRequiredMfa ? mfaSettingState.selectedMfaType : undefined; + } + const createRoleBinding = async () => { if (userPageStore.getters.isWorkspaceOwner || state.isSetAdminRole) { await fetchCreateRoleBinding(item); @@ -243,6 +260,13 @@ watch(() => route.query, (query) => { :password.sync="state.password" :disabled-reset-password="state.disabledResetPassword" /> +
+ +
({ + isRequiredMfa: false, + selectedMfaType: MULTI_FACTOR_AUTH_ITEMS[0].type, +}); + const formState = reactive({ name: '', email: '', @@ -101,7 +112,7 @@ const handleChangeInputs = (value) => { if (value.passwordType) formState.passwordType = value.passwordType; if (value.role) formState.role = value.role; }; -const buildUserInfoParams = (): UserManagementData => ({ +const buildUserInfoParams = (): UserUpdateParameters => ({ user_id: state.data.user_id || '', name: formState.name, email: formState.isValidEmail ? formState.email : state.data.email || '', @@ -126,9 +137,6 @@ const handleConfirm = async () => { } userPageStore.setUserEmail(state.data.user_id, state.data.email); } - if (state.isChangedMfaToggle) { - await fetchPostDisableMfa(); - } if (state.roleBindingList.length > 0 && !state.isChangedRoleToggle) { await fetchDeleteRoleBinding(); @@ -137,6 +145,10 @@ const handleConfirm = async () => { } const userInfoParams = buildUserInfoParams(); + if (state.data.auth_type === 'LOCAL') { // Only Local Auth Type Users can be updated + userInfoParams.enforce_mfa_state = mfaSettingFormState.isRequiredMfa ? MFA_STATE.ENABLED : MFA_STATE.DISABLED; + userInfoParams.enforce_mfa_type = mfaSettingFormState.isRequiredMfa ? mfaSettingFormState.selectedMfaType : undefined; + } await SpaceConnector.clientV2.identity.user.update(userInfoParams); showSuccessMessage(i18n.t('IAM.USER.MAIN.MODAL.ALT_S_UPDATE_USER'), ''); @@ -187,24 +199,7 @@ const fetchDeleteRoleBinding = async () => { ErrorHandler.handleRequestError(e, e.message); } }; -const fetchPostDisableMfa = async () => { - state.mfaLoading = true; - try { - await postUserDisableMfa({ - user_id: state.data.user_id || '', - }); - if (state.loginUserId === state.data.user_id) { - userStore.setMfa({ - ...state.data.mfa as UserMfa, - state: 'DISABLED', - }); - } - } catch (e: any) { - ErrorHandler.handleRequestError(e, e.message); - } finally { - state.mfaLoading = false; - } -}; + const fetchListRoleBindingInfo = async () => { const response = await SpaceConnector.clientV2.identity.roleBinding.list>({ user_id: state.data.user_id || '', @@ -275,7 +270,11 @@ watch(() => userPageState.modal.visible, async (visible) => { v-if="state.data.auth_type === 'LOCAL'" @change-input="handleChangeInputs" /> - + +import { computed, ref } from 'vue'; + +import { + PButtonModal, PScopedNotification, +} from '@cloudforet/mirinae'; + +import { MFA_STATE } from '@/api-clients/identity/user-profile/schema/constant'; +import type { MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type'; +import type { UserUpdateParameters } from '@/api-clients/identity/user/schema/api-verbs/update'; +import { i18n } from '@/translations'; + +import { showErrorMessage, showSuccessMessage } from '@/lib/helper/notice-alert-helper'; + +import ErrorHandler from '@/common/composables/error/errorHandler'; + +import UserMFASettingFormLayout from '@/services/iam/components/mfa/UserMFASettingFormLayout.vue'; +import { useUserUpdateMutation } from '@/services/iam/composables/mutations/use-user-update-mutation'; +import { MULTI_FACTOR_AUTH_ITEMS } from '@/services/iam/constants/user-constant'; +import { useUserPageStore } from '@/services/iam/store/user-page-store'; +import type { UserListItemType } from '@/services/iam/types/user-type'; + + +/* Store */ +const userPageStore = useUserPageStore(); +const userPageState = userPageStore.state; +const userPageGetters = userPageStore.getters; + +/* State */ +const selectedMfaType = ref(MULTI_FACTOR_AUTH_ITEMS[0].type); +const isRequiredMfa = ref(false); + +/* Computed */ +// Only Local Auth Type Users can be updated +const selectedMFAControllableUsers = computed(() => userPageGetters.selectedUsers.filter((user) => user.auth_type === 'LOCAL')); + +// UI Conditions +const isIncludedExternalAuthTypeUser = computed(() => userPageGetters.selectedUsers.some((user) => user.auth_type !== 'LOCAL')); + +/* API */ +// Store failed user IDs +const failedUserIds = new Set(); + +const { mutateAsync: updateUser, isPending: isUpdateUserPending } = useUserUpdateMutation({ + onError: (error: any, variables: UserUpdateParameters) => { + // Store failed user IDs for logging failed users + failedUserIds.add(variables.user_id); + throw new Error(error.message); + }, +}); + +/* Utils */ +const closeModal = () => { + userPageStore.updateModalSettings({ + type: '', + title: '', + themeColor: 'primary', + modalVisibleType: undefined, + }); +}; + +/* Events */ +const handleClose = () => { + closeModal(); +}; + +const handleConfirm = async () => { + if (isUpdateUserPending.value) return; + + // Update MFA Promise for each user (Bulk) + const userUpdatePromises = selectedMFAControllableUsers.value.map(async (user) => { + if (!user.user_id) { + if (import.meta.env.DEV) throw new Error('[UserMFASettingModal.vue] There are users without user_id'); + else throw new Error('[User MFA Setting] Something went wrong! Try again later. If the problem persists, please contact support.'); + } + + // API Policy: enforce_mfa_type is required when enforce_mfa_state is ENABLED + if (isRequiredMfa.value && !selectedMfaType.value) { + if (import.meta.env.DEV) throw new Error('[UserMFASettingModal.vue] MFA type is required when required MFA is true.'); + else throw new Error('[User MFA Setting] Something went wrong! Try again later. If the problem persists, please contact support.'); + } + + return updateUser({ + user_id: user.user_id, + enforce_mfa_state: isRequiredMfa.value ? MFA_STATE.ENABLED : MFA_STATE.DISABLED, + enforce_mfa_type: selectedMfaType.value, + }); + }); + + if (userUpdatePromises.length === 0) { + showErrorMessage('[User MFA Setting] There are no users to update.', ''); + return; + } + + try { + const results = await Promise.allSettled(userUpdatePromises); + if (results.every((result) => result.status === 'fulfilled')) { + showSuccessMessage(i18n.t('IAM.USER.MAIN.MODAL.MFA.SET_MFA_SUCCESS_MESSAGE'), ''); + closeModal(); + } else if (results.some((result) => result.status === 'rejected')) { // Bulk update MFA failed + if (import.meta.env.DEV) { + const joinedFailedUserIds = Array.from(failedUserIds.keys()).join(', '); + throw new Error(`[UserMFASettingModal.vue] Bulk update MFA failed user IDs: [${joinedFailedUserIds}]`); + } else throw new Error('[User MFA Setting] Something went wrong! Try again later. If the problem persists, please contact support.'); + } + } catch (error: any) { + ErrorHandler.handleError(error); + showErrorMessage(error.message, error); + } finally { + // Clear failed user IDs + failedUserIds.clear(); + } +}; + + + + diff --git a/apps/web/src/services/iam/components/mfa/UserMFASettingDisableButton.vue b/apps/web/src/services/iam/components/mfa/UserMFASettingDisableButton.vue index 2e89255a7f..2e7aaf1c33 100644 --- a/apps/web/src/services/iam/components/mfa/UserMFASettingDisableButton.vue +++ b/apps/web/src/services/iam/components/mfa/UserMFASettingDisableButton.vue @@ -17,17 +17,19 @@ import { useUserMfaDisableMutation } from '@/services/iam/composables/mutations/ import { USER_MODAL_MAP } from '@/services/iam/constants/modal.constant'; interface UserMFASettingDisableButtonProps { - selectedTarget: UserModel | UserModel[]; + selectedTarget?: UserModel | UserModel[]; } const props = defineProps(); /* Computed */ const selectedTargetUsers = computed(() => { + if (!props.selectedTarget) return []; if (Array.isArray(props.selectedTarget)) return props.selectedTarget; return [props.selectedTarget]; }); -const isSingleTargetUser = computed(() => !Array.isArray(props.selectedTarget)); +const isSingleTargetUser = computed(() => !!props.selectedTarget && !Array.isArray(props.selectedTarget)); +const buttonDisabled = computed(() => !props.selectedTarget || selectedTargetUsers.value.length === 0); /* API */ // Store failed user IDs @@ -84,7 +86,7 @@ const handleDeleteMfaSecretKey = async (onClose: () => void) => { diff --git a/apps/web/src/services/iam/components/mfa/UserMFASettingEnforceForm.vue b/apps/web/src/services/iam/components/mfa/UserMFASettingEnforceForm.vue new file mode 100644 index 0000000000..2b5357c11b --- /dev/null +++ b/apps/web/src/services/iam/components/mfa/UserMFASettingEnforceForm.vue @@ -0,0 +1,104 @@ + + + diff --git a/apps/web/src/services/iam/components/mfa/UserMFASettingFormLayout.vue b/apps/web/src/services/iam/components/mfa/UserMFASettingFormLayout.vue new file mode 100644 index 0000000000..492fe33cff --- /dev/null +++ b/apps/web/src/services/iam/components/mfa/UserMFASettingFormLayout.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue b/apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue deleted file mode 100644 index 2b34009557..0000000000 --- a/apps/web/src/services/iam/components/mfa/UserMFASettingModal.vue +++ /dev/null @@ -1,265 +0,0 @@ - - - - - - diff --git a/apps/web/src/services/iam/pages/UserMainPage.vue b/apps/web/src/services/iam/pages/UserMainPage.vue index 92b015d41f..2852251a07 100644 --- a/apps/web/src/services/iam/pages/UserMainPage.vue +++ b/apps/web/src/services/iam/pages/UserMainPage.vue @@ -12,7 +12,7 @@ import { useAuthorizationStore } from '@/store/authorization/authorization-store import { useGrantScopeGuard } from '@/common/composables/grant-scope-guard'; import { usePageEditableStatus } from '@/common/composables/page-editable-status'; -import UserMFASettingModal from '@/services/iam/components/mfa/UserMFASettingModal.vue'; +import UserBulkMFASettingModal from '@/services/iam/components/mfa/UserBulkMFASettingModal.vue'; import UserAssignToGroupModal from '@/services/iam/components/UserAssignToGroupModal.vue'; import UserManagementAddModal from '@/services/iam/components/UserManagementAddModal.vue'; import UserManagementFormModal from '@/services/iam/components/UserManagementFormModal.vue'; @@ -106,8 +106,8 @@ onUnmounted(() => { - - + /> diff --git a/apps/web/src/services/iam/store/user-page-store.ts b/apps/web/src/services/iam/store/user-page-store.ts index db9895ddfa..732b40805e 100644 --- a/apps/web/src/services/iam/store/user-page-store.ts +++ b/apps/web/src/services/iam/store/user-page-store.ts @@ -26,8 +26,15 @@ import { useAuthorizationStore } from '@/store/authorization/authorization-store import ErrorHandler from '@/common/composables/error/errorHandler'; +import type { UserModalType } from '@/services/iam/types/modal.type'; import type { UserListItemType, ModalSettingState, ModalState } from '@/services/iam/types/user-type'; +interface UserPageModalState { + previousModalType: UserModalType | undefined; + bulkMfaSettingModalVisible: boolean; + mfaSecretKeyDeleteModalVisible: boolean; +} + export const useUserPageStore = defineStore('page-user', () => { const authorizationStore = useAuthorizationStore(); @@ -52,6 +59,13 @@ export const useUserPageStore = defineStore('page-user', () => { visible: undefined, } as ModalState, }); + + const modalState = reactive({ + previousModalType: undefined, + bulkMfaSettingModalVisible: false, + mfaSecretKeyDeleteModalVisible: false, + }); + const getters = reactive({ isWorkspaceOwner: computed(() => authorizationStore.state.currentRoleInfo?.roleType === ROLE_TYPE.WORKSPACE_OWNER), selectedUsers: computed(() => { @@ -71,6 +85,19 @@ export const useUserPageStore = defineStore('page-user', () => { return map; }), }); + + const mutations = { + setPreviousModalType(type: UserModalType | undefined) { + modalState.previousModalType = type; + }, + setBulkMfaSettingModalVisible(visible: boolean) { + modalState.bulkMfaSettingModalVisible = visible; + }, + setMfaSecretKeyDeleteModalVisible(visible: boolean) { + modalState.mfaSecretKeyDeleteModalVisible = visible; + }, + }; + const actions = { // User reset() { @@ -213,7 +240,9 @@ export const useUserPageStore = defineStore('page-user', () => { }; return { state, + modalState, getters, + ...mutations, ...actions, }; }); diff --git a/apps/web/src/services/iam/types/modal.type.ts b/apps/web/src/services/iam/types/modal.type.ts new file mode 100644 index 0000000000..2e27ffdb02 --- /dev/null +++ b/apps/web/src/services/iam/types/modal.type.ts @@ -0,0 +1,3 @@ +import type { USER_MODAL_MAP } from '@/services/iam/constants/modal.constant'; + +export type UserModalType = typeof USER_MODAL_MAP[keyof typeof USER_MODAL_MAP]; From 0a519ed9b6c4bbe73d63604e139394bd3113ceb2 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Thu, 24 Jul 2025 10:30:55 +0900 Subject: [PATCH 10/21] chore: solve empty case Signed-off-by: samuel.park --- .../services/iam/components/UserManagementAddModal.vue | 2 +- .../services/iam/components/UserManagementFormModal.vue | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/web/src/services/iam/components/UserManagementAddModal.vue b/apps/web/src/services/iam/components/UserManagementAddModal.vue index f886c78d69..f830f17b46 100644 --- a/apps/web/src/services/iam/components/UserManagementAddModal.vue +++ b/apps/web/src/services/iam/components/UserManagementAddModal.vue @@ -160,7 +160,7 @@ const fetchCreateUser = async (item: AddModalMenuItem): Promise => { }; if (userPageState.isAdminMode && userInfoParams.auth_type === 'LOCAL') { - userInfoParams.enforce_mfa_state = mfaSettingState.isRequiredMfa ? MFA_STATE.ENABLED : MFA_STATE.DISABLED; + userInfoParams.enforce_mfa_state = mfaSettingState.isRequiredMfa ? MFA_STATE.ENABLED : undefined; userInfoParams.enforce_mfa_type = mfaSettingState.isRequiredMfa ? mfaSettingState.selectedMfaType : undefined; } diff --git a/apps/web/src/services/iam/components/UserManagementFormModal.vue b/apps/web/src/services/iam/components/UserManagementFormModal.vue index 892b3bfe24..4e4a721147 100644 --- a/apps/web/src/services/iam/components/UserManagementFormModal.vue +++ b/apps/web/src/services/iam/components/UserManagementFormModal.vue @@ -157,8 +157,13 @@ const handleConfirm = async () => { const userInfoParams = buildUserInfoParams(); if (state.data.auth_type === 'LOCAL') { // Only Local Auth Type Users can be updated - userInfoParams.enforce_mfa_state = mfaSettingFormState.isRequiredMfa ? MFA_STATE.ENABLED : MFA_STATE.DISABLED; - userInfoParams.enforce_mfa_type = mfaSettingFormState.isRequiredMfa ? mfaSettingFormState.selectedMfaType : undefined; + const existingMfa = state.data.mfa; + if (!!existingMfa?.options?.enforce !== mfaSettingFormState.isRequiredMfa) { + userInfoParams.enforce_mfa_state = mfaSettingFormState.isRequiredMfa ? MFA_STATE.ENABLED : MFA_STATE.DISABLED; + } + if (userInfoParams.enforce_mfa_state === MFA_STATE.ENABLED) { + userInfoParams.enforce_mfa_type = mfaSettingFormState.isRequiredMfa ? mfaSettingFormState.selectedMfaType : undefined; + } } await SpaceConnector.clientV2.identity.user.update(userInfoParams); From 41cfc707562f88fdc2161fe0bb60b70f19c8f59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piggy=20Park=20=28=EB=B0=95=EC=9A=A9=ED=83=9C=29?= Date: Mon, 28 Jul 2025 14:26:12 +0900 Subject: [PATCH 11/21] chore: MFA QA (#6058) * fix(mfa): solve switching mfa-type bug Signed-off-by: samuel.park * feat(user-form-modal): apply vue query for sync Signed-off-by: samuel.park * fix(user-cache): apply user cache sync to userProfile Signed-off-by: samuel.park * fix(my-account): solve mfa resync bug Signed-off-by: samuel.park * chore: fix ui (info-tooltip) Signed-off-by: samuel.park * chore: solve type lint error Signed-off-by: samuel.park --------- Signed-off-by: samuel.park --- .../use-user-profile-confirm-mfa-mutation.ts | 2 +- .../components/UserManagementFormInfoForm.vue | 18 +++-- .../components/UserManagementFormModal.vue | 65 +++++++++++-------- ...serManagementFormNotificationEmailForm.vue | 13 +++- .../UserManagementFormPasswordForm.vue | 17 +++-- .../components/UserManagementTableToolbox.vue | 18 +++-- .../mfa/UserMFASettingEnforceForm.vue | 7 -- .../use-user-mfa-disable-mutation.ts | 4 ++ .../mutations/use-user-update-mutation.ts | 4 ++ .../iam/composables/use-user-get-query.ts | 27 ++++++++ .../src/services/iam/pages/UserMainPage.vue | 4 +- .../src/services/iam/store/user-page-store.ts | 4 ++ ...AccountMultiFactorAuthEmailEnableModal.vue | 6 +- ...erAccountMultiFactorAuthOTPEnableModal.vue | 1 + 14 files changed, 136 insertions(+), 54 deletions(-) create mode 100644 apps/web/src/services/iam/composables/use-user-get-query.ts diff --git a/apps/web/src/common/components/mfa/composables/use-user-profile-confirm-mfa-mutation.ts b/apps/web/src/common/components/mfa/composables/use-user-profile-confirm-mfa-mutation.ts index 9faf13e4f6..5e46b7a08a 100644 --- a/apps/web/src/common/components/mfa/composables/use-user-profile-confirm-mfa-mutation.ts +++ b/apps/web/src/common/components/mfa/composables/use-user-profile-confirm-mfa-mutation.ts @@ -18,7 +18,7 @@ export const useUserProfileConfirmMfaMutation = ({ const { userProfileAPI } = useUserProfileApi(); return useMutation({ - mutationFn: userProfileAPI.confirmMfa, + mutationFn: (params: UserProfileConfirmMfaParameters) => userProfileAPI.confirmMfa(params), onSuccess: async (data, variables) => { if (onSuccess) await onSuccess(data, variables); }, diff --git a/apps/web/src/services/iam/components/UserManagementFormInfoForm.vue b/apps/web/src/services/iam/components/UserManagementFormInfoForm.vue index e40b405a86..f6ab36eac6 100644 --- a/apps/web/src/services/iam/components/UserManagementFormInfoForm.vue +++ b/apps/web/src/services/iam/components/UserManagementFormInfoForm.vue @@ -5,10 +5,13 @@ import { PFieldGroup, PTextInput, } from '@cloudforet/mirinae'; +import type { UserModel } from '@/api-clients/identity/user/schema/model'; + import { useProxyValue } from '@/common/composables/proxy-state'; +import { useUserGetQuery } from '@/services/iam/composables/use-user-get-query'; import { useUserPageStore } from '@/services/iam/store/user-page-store'; -import type { UserListItemType } from '@/services/iam/types/user-type'; + interface Props { name?: string @@ -18,20 +21,26 @@ const props = withDefaults(defineProps(), { }); const userPageStore = useUserPageStore(); +const userPageState = userPageStore.state; const emit = defineEmits<{(e: 'update:name', value: string): void}>(); +const { data: userData, isLoading: isUserLoading } = useUserGetQuery({ + userId: computed(() => userPageState.selectedUserForForm?.user_id || ''), +}); + const state = reactive({ - data: computed(() => userPageStore.getters.selectedUsers[0]), + data: computed(() => userData.value), proxyName: useProxyValue('name', props, emit), }); + /* Components */ const handleChangeName = (value: string) => { state.proxyName = value; }; const setForm = () => { - state.proxyName = state.data.name || ''; + state.proxyName = state.data?.name || ''; }; /* Init */ @@ -45,7 +54,7 @@ onMounted(() => { - @@ -54,6 +63,7 @@ onMounted(() => { class="input-form" > userPageState.selectedUserForForm?.user_id || ''), +}); + const emit = defineEmits<{(e: 'confirm'): void; }>(); const state = reactive({ loading: false, mfaLoading: false, - data: computed(() => userPageStore.getters.selectedUsers[0]), + // data: computed(() => userPageStore.getters.selectedUsers[0]), + data: computed(() => userData.value as UserModel), smtpEnabled: computed(() => config.get('SMTP_ENABLED')), mfa: computed(() => userStore.state.mfa), loginUserId: computed(() => userStore.state.userId), @@ -104,11 +111,12 @@ const closeModal = () => { }; const handleClose = () => { closeModal(); + userPageStore.setSelectedUserForForm(undefined); }; const setForm = () => { - formState.name = state.data.name || ''; - formState.email = state.data.email || ''; - formState.tags = state.data.tags || {}; + formState.name = state.data?.name || ''; + formState.email = state.data?.email || ''; + formState.tags = state.data?.tags || {}; }; const handleChangeInputs = (value) => { if (value.email) formState.email = value.email; @@ -118,12 +126,12 @@ const handleChangeInputs = (value) => { if (value.role) formState.role = value.role; }; const buildUserInfoParams = (): UserUpdateParameters => ({ - user_id: state.data.user_id || '', + user_id: state.data?.user_id || '', name: formState.name, - email: formState.isValidEmail ? formState.email : state.data.email || '', + email: formState.isValidEmail ? formState.email : state.data?.email || '', tags: formState.tags || {}, password: formState.password || '', - reset_password: state.data.auth_type === 'LOCAL' && formState.passwordType === PASSWORD_TYPE.RESET, + reset_password: state.data?.auth_type === 'LOCAL' && formState.passwordType === PASSWORD_TYPE.RESET, }); const handleOpenDisableMfaModal = () => { @@ -140,13 +148,13 @@ const handleConfirm = async () => { if (formState.isValidEmail) { await updateUserEmail(); await verifyUserEmail(); - if (state.loginUserId === state.data.user_id) { + if (state.loginUserId === state.data?.user_id) { await userStore.updateUser({ - email: state.data.email, + email: state.data?.email, }); userStore.setEmailVerified(true); } - userPageStore.setUserEmail(state.data.user_id, state.data.email); + userPageStore.setUserEmail(state.data?.user_id, state.data?.email); } if (state.roleBindingList.length > 0 && !state.isChangedRoleToggle) { @@ -156,13 +164,16 @@ const handleConfirm = async () => { } const userInfoParams = buildUserInfoParams(); - if (state.data.auth_type === 'LOCAL') { // Only Local Auth Type Users can be updated - const existingMfa = state.data.mfa; - if (!!existingMfa?.options?.enforce !== mfaSettingFormState.isRequiredMfa) { + if (state.data?.auth_type === 'LOCAL') { // Only Local Auth Type Users can be updated + const existingMfa = state.data?.mfa; + if (!!existingMfa?.options?.enforce !== mfaSettingFormState.isRequiredMfa) { // switch required mfa state Case userInfoParams.enforce_mfa_state = mfaSettingFormState.isRequiredMfa ? MFA_STATE.ENABLED : MFA_STATE.DISABLED; - } - if (userInfoParams.enforce_mfa_state === MFA_STATE.ENABLED) { - userInfoParams.enforce_mfa_type = mfaSettingFormState.isRequiredMfa ? mfaSettingFormState.selectedMfaType : undefined; + if (userInfoParams.enforce_mfa_state === MFA_STATE.ENABLED) { + userInfoParams.enforce_mfa_type = mfaSettingFormState.isRequiredMfa ? mfaSettingFormState.selectedMfaType : undefined; + } + } else if (mfaSettingFormState.isRequiredMfa && existingMfa?.mfa_type !== mfaSettingFormState.selectedMfaType) { // switch mfa type Case (enforce mfa state is true) + userInfoParams.enforce_mfa_state = MFA_STATE.ENABLED; + userInfoParams.enforce_mfa_type = mfaSettingFormState.selectedMfaType; } } await SpaceConnector.clientV2.identity.user.update(userInfoParams); @@ -173,11 +184,12 @@ const handleConfirm = async () => { } catch (e: any) { ErrorHandler.handleRequestError(e, i18n.t('IAM.USER.MAIN.MODAL.ALT_E_UPDATE_USER')); } finally { + userPageStore.setSelectedUserForForm(undefined); state.loading = false; } }; const fetchRoleBinding = async (item?: AddModalMenuItem) => { - if (state.data.user_id === userStore.state.userId) return; + if (state.data?.user_id === userStore.state.userId) return; if (isEmpty(formState.role)) return; const roleParams = { @@ -191,7 +203,7 @@ const fetchRoleBinding = async (item?: AddModalMenuItem) => { await SpaceConnector.clientV2.identity.roleBinding.create({ ...roleParams, workspace_id: item?.name || '', - user_id: state.data.user_id || '', + user_id: state.data?.user_id || '', resource_group: RESOURCE_GROUP.DOMAIN, }); } else { @@ -218,7 +230,7 @@ const fetchDeleteRoleBinding = async () => { const fetchListRoleBindingInfo = async () => { const response = await SpaceConnector.clientV2.identity.roleBinding.list>({ - user_id: state.data.user_id || '', + user_id: state.data?.user_id || '', query: { filter: [{ k: 'role_type', v: ROLE_TYPE.DOMAIN_ADMIN, o: 'eq' }], }, @@ -237,14 +249,14 @@ const fetchListRoleBindingInfo = async () => { }; const updateUserEmail = async () => { await SpaceConnector.clientV2.identity.user.update({ - user_id: state.data.user_id || '', - email: state.data.email || '', + user_id: state.data?.user_id || '', + email: state.data?.email || '', }); }; const verifyUserEmail = async () => { await postUserValidationEmail({ - user_id: state.data.user_id || '', - email: state.data.email || '', + user_id: state.data?.user_id || '', + email: state.data?.email || '', }); }; @@ -274,6 +286,7 @@ watch(() => state.data?.mfa, (mfa) => { state.data?.mfa, (mfa) => { @change-input="handleChangeInputs" /> - (); + +const { data: userData, isLoading: isUserLoading } = useUserGetQuery({ + userId: computed(() => userPageState.selectedUserForForm?.user_id || ''), +}); + const state = reactive({ - data: computed(() => userPageState.selectedUser), + data: computed(() => userData.value), loading: false, isEdit: false, isCollapsed: true, @@ -42,7 +48,7 @@ const { invalidState, invalidTexts, } = useFormValidator({ - email: state.data.email, + email: state.data?.email || '', }, { email(value: string) { return !emailValidator(value) ? '' : i18n.t('IAM.USER.FORM.EMAIL_INVALID'); }, }); @@ -102,6 +108,7 @@ watch(() => state.data?.email_verified, (value) => {
(); + +const { data: userData, isLoading: isUserLoading } = useUserGetQuery({ + userId: computed(() => userPageState.selectedUserForForm?.user_id || ''), +}); + const state = reactive({ - data: computed(() => userPageStore.getters.selectedUsers[0]), + data: computed(() => userData.value), smtpEnabled: computed(() => config.get('SMTP_ENABLED')), passwordStatus: 0, passwordTypeArr: computed(() => { @@ -35,7 +42,7 @@ const state = reactive({ additionalItems.push({ name: PASSWORD_TYPE.RESET, label: i18n.t('COMMON.PROFILE.SEND_LINK'), - disabled: !state.data.email_verified, + disabled: !state.data?.email_verified, }); } return [ @@ -139,6 +146,7 @@ onMounted(() => { > + + diff --git a/apps/web/src/services/my-page/components/UserAccountMultiFactorAuthItems.vue b/apps/web/src/services/my-page/components/UserAccountMultiFactorAuthItems.vue index e052393020..e5df4e9569 100644 --- a/apps/web/src/services/my-page/components/UserAccountMultiFactorAuthItems.vue +++ b/apps/web/src/services/my-page/components/UserAccountMultiFactorAuthItems.vue @@ -41,7 +41,7 @@ const state = reactive({ }); const handleChange = async (isSelected: boolean, selected: MultiFactorAuthType) => { - if (props.readonlyMode) return; + if (props.readonlyMode || state.isEnforced) return; if (state.isEnforced && state.currentType !== selected) return; if (state.isVerified && state.currentType !== selected) { if (selected === MULTI_FACTOR_AUTH_TYPE.OTP) { @@ -85,7 +85,7 @@ const handleClickReSyncButton = async (type: MultiFactorAuthType, event: MouseEv -import { computed, reactive } from 'vue'; +import { + computed, onMounted, reactive, +} from 'vue'; import type { TranslateResult } from 'vue-i18n'; import { SpaceConnector } from '@cloudforet/core-lib/space-connector'; @@ -32,7 +34,7 @@ import UserAccountChangePassword from '@/services/my-page/components/UserAccount import UserAccountMultiFactorAuth from '@/services/my-page/components/UserAccountMultiFactorAuth.vue'; import UserAccountNotificationEmail from '@/services/my-page/components/UserAccountNotificationEmail.vue'; - +const PASSWORD_MIN_LENGTH = 8; const domainStore = useDomainStore(); const userStore = useUserStore(); @@ -78,6 +80,7 @@ const passwordFormState = reactive({ const passwordCheckFecher = getCancellableFetcher(SpaceConnector.clientV2.identity.token.issue); const handleConfirmPasswordCheckModal = async () => { + if (passwordFormState.password.length < PASSWORD_MIN_LENGTH) return; passwordFormState.loading = true; try { const result = await passwordCheckFecher({ @@ -126,6 +129,11 @@ const handleClickCancel = () => { passwordFormState.invalidText = ''; }; +// TODO: remove this after tanstack query is implemented +onMounted(async () => { + await userStore.getUserInfo(); +}); + diff --git a/packages/language-pack/en.json b/packages/language-pack/en.json index 613b1f6a31..70bb2a2e37 100644 --- a/packages/language-pack/en.json +++ b/packages/language-pack/en.json @@ -427,7 +427,7 @@ "ENFORCE_INFO_TEXT": "Multi-factor authentication (MFA) is required for\nyour account by your administrator.\nTo disable this feature, please contact your admin.", "ENTER_CODE": "Enter Code", "GO_BACK": "Back to Sign in", - "MFA_CONFIGURED": "\bMulti-Factor Authentication Configured.", + "MFA_CONFIGURED": "Multi-Factor Authentication Configured.", "MFA_CONFIGURED_DESC": "Please sign-in again using the authentication method you set.", "OTP_INFO": "Allows multi-factor authentication(MFA) for account logins. Enter the code that you see in the app.", "PROBLEM_TITLE": "Having problems?", From 1390345526931ebd1843bf5e9fdee1ce771b79ea Mon Sep 17 00:00:00 2001 From: "NaYeong,Kim" Date: Mon, 11 Aug 2025 14:01:47 +0900 Subject: [PATCH 16/21] feat: apply password validation at user add/update form (#6114) Signed-off-by: NaYeong,Kim --- .../components/UserManagementAddPassword.vue | 55 +++++++++++++++---- .../UserManagementFormPasswordForm.vue | 11 ++-- packages/language-pack/en.json | 1 + packages/language-pack/ja.json | 1 + packages/language-pack/ko.json | 1 + 5 files changed, 51 insertions(+), 18 deletions(-) diff --git a/apps/web/src/services/iam/components/UserManagementAddPassword.vue b/apps/web/src/services/iam/components/UserManagementAddPassword.vue index 3903365669..7ddb2b3713 100644 --- a/apps/web/src/services/iam/components/UserManagementAddPassword.vue +++ b/apps/web/src/services/iam/components/UserManagementAddPassword.vue @@ -1,11 +1,13 @@ @@ -64,8 +95,8 @@ const handleClickGenerate = () => {
@@ -74,10 +105,10 @@ const handleClickGenerate = () => { > {{ $t('IAM.USER.FORM.GENERATE') }} - Date: Thu, 14 Aug 2025 13:27:38 +0900 Subject: [PATCH 17/21] refactor: fix MFA related codes as vue query updated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 이승연 --- .../authenticator/local/template/ID_PW.vue | 55 ++++++++++--------- .../auth/pages/MultiFactorAuthSetUpPage.vue | 22 ++++---- apps/web/src/services/auth/routes/routes.ts | 1 + .../iam/components/UserManagementHeader.vue | 2 +- .../mfa/UserBulkMFASettingModal.vue | 11 +++- .../mfa/UserMFASecretKeyDeleteModal.vue | 8 ++- 6 files changed, 56 insertions(+), 43 deletions(-) diff --git a/apps/web/src/services/auth/authenticator/local/template/ID_PW.vue b/apps/web/src/services/auth/authenticator/local/template/ID_PW.vue index c830c3b63e..307ed385b2 100644 --- a/apps/web/src/services/auth/authenticator/local/template/ID_PW.vue +++ b/apps/web/src/services/auth/authenticator/local/template/ID_PW.vue @@ -17,7 +17,6 @@ import { useUserStore } from '@/store/user/user-store'; import config from '@/lib/config'; import { isMobile } from '@/lib/helper/cross-browsing-helper'; -import { showErrorMessage } from '@/lib/helper/notice-alert-helper'; import ErrorHandler from '@/common/composables/error/errorHandler'; @@ -85,21 +84,6 @@ const signIn = async () => { await loadAuth().signIn(credentials, 'LOCAL'); displayStore.setIsSignInFailed(false); - // console.log('userStore.state.requiredActions', userStore.state.requiredActions, userStore.state.mfa); - if (userStore.state.requiredActions?.includes(REQUIRED_ACTIONS.ENFORCE_MFA)) { - const MFA_TYPE = userStore.state.mfa?.mfa_type; - const isMFAEnforced = !!userStore.state.mfa?.options?.enforce && !!MFA_TYPE; - const needToEnableMFA = isMFAEnforced && userStore.state.mfa?.state !== 'ENABLED'; - if (needToEnableMFA) { // Enforced MFA by admin. Always has both `options.enforce` and `mfa_type`. - if (import.meta.env.DEV) console.debug(`[ID_PW.vue] MFA_TYPE: ${MFA_TYPE}`); - await router.push({ name: AUTH_ROUTE.SIGN_IN.MULTI_FACTOR_AUTH_SETUP._NAME, params: { mfaType: MFA_TYPE } }); - } else { - showErrorMessage('Something went wrong! Contact support.', 'MFA_NOT_SETUP'); - await router.push({ name: AUTH_ROUTE.SIGN_OUT._NAME }); - } - return; - } - if (userStore.state.requiredActions?.includes(REQUIRED_ACTIONS.UPDATE_PASSWORD)) { await router.push({ name: AUTH_ROUTE.PASSWORD.STATUS.RESET._NAME }); } else { @@ -109,15 +93,36 @@ const signIn = async () => { if (e.message.includes('MFA')) { const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g; const mfaTypeRegex = /mfa_type\s*=\s*(\w+)/; - await router.push({ - name: AUTH_ROUTE.SIGN_IN.MULTI_FACTOR_AUTH._NAME, - params: { - password: credentials.password, - mfaEmail: e.message.match(emailRegex)[0], - mfaType: e.message.match(mfaTypeRegex)[1], - userId: state.userId?.trim() as string, - }, - }); + const message = e.message || ''; + + const mfaType = message.match(mfaTypeRegex)?.[1]; + const mfaEmail = message.match(emailRegex)?.[0]; + + const isStateEnabled = message.includes('ENABLED'); + const isStateDisabled = message.includes('DISABLED'); + + if (message.includes('MFA is not activated.') && isStateDisabled && mfaType) { + const token = message.match(/access_token\s*=\s*([\w.-]+)/)?.[1]; + + await router.push({ + name: AUTH_ROUTE.SIGN_IN.MULTI_FACTOR_AUTH_SETUP._NAME, + params: { mfaType }, + query: { sso_access_token: token }, + }); + } else if (message.includes('required') || (message.includes('MFA is not activated.') && isStateEnabled)) { + await router.push({ + name: AUTH_ROUTE.SIGN_IN.MULTI_FACTOR_AUTH._NAME, + params: { + password: credentials.password, mfaEmail, mfaType, userId: state.userId?.trim() as string, + }, + }); + } else if (message.includes('Authenticate failure')) { + ErrorHandler.handleError(e); + displayStore.setIsSignInFailed(true); + } else { + ErrorHandler.handleRequestError(e, e.message); + await router.push({ name: AUTH_ROUTE.SIGN_OUT._NAME }); + } } else { displayStore.setSignInFailedMessage(e.message); ErrorHandler.handleError(e); diff --git a/apps/web/src/services/auth/pages/MultiFactorAuthSetUpPage.vue b/apps/web/src/services/auth/pages/MultiFactorAuthSetUpPage.vue index 34d1f4488f..2260ad8c92 100644 --- a/apps/web/src/services/auth/pages/MultiFactorAuthSetUpPage.vue +++ b/apps/web/src/services/auth/pages/MultiFactorAuthSetUpPage.vue @@ -14,8 +14,6 @@ import { MULTI_FACTOR_AUTH_TYPE } from '@/api-clients/identity/user-profile/sche import type { MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type'; import { i18n as _i18n } from '@/translations'; -import { ROOT_ROUTE } from '@/router/constant'; - import { useUserStore } from '@/store/user/user-store'; import { showErrorMessage } from '@/lib/helper/notice-alert-helper'; @@ -47,7 +45,7 @@ const { } = route.params as { mfaType: MultiFactorAuthType | undefined }; const state = reactive({ - isLocalLogin: computed(() => userStore.state.authType === 'LOCAL'), + // isLocalLogin: computed(() => userStore.state.authType === 'LOCAL'), myMFAType: computed(() => userStore.state.mfa?.mfa_type), isInvalidMfaType: computed(() => state.myMFAType !== mfaTypeRouteParam || !state.myMFAType || !mfaTypeRouteParam), needToEnableMFA: computed(() => { @@ -112,16 +110,16 @@ const handleClickConfirmButton = async () => { }); }; +const getSSOTokenFromUrl = (): string|undefined => { + const query = router.currentRoute.query; + return query.sso_access_token as string; +}; + onMounted(() => { - // Remove refresh token to prevent forced access to other pages - SpaceConnector.removeRefreshToken(); - - if (!SpaceConnector.getAccessToken() || !state.needToEnableMFA || state.isInvalidMfaType) { - router.push({ name: AUTH_ROUTE.SIGN_OUT._NAME }); - return; - } if (!state.isLocalLogin) { - router.push({ name: ROOT_ROUTE._NAME }); - } + const ssoAccessToken = getSSOTokenFromUrl(); + + if (!ssoAccessToken) return; + SpaceConnector.setToken(ssoAccessToken, ''); }); onBeforeUnmount(() => { diff --git a/apps/web/src/services/auth/routes/routes.ts b/apps/web/src/services/auth/routes/routes.ts index d924abe24e..73d36a9a3a 100644 --- a/apps/web/src/services/auth/routes/routes.ts +++ b/apps/web/src/services/auth/routes/routes.ts @@ -74,6 +74,7 @@ export default [ isCentered: true, }, props: (route) => ({ + ssoAccessToken: route.query.sso_access_token, previousPath: route.query.previousPath, redirectPath: route.query.redirectPath, }), diff --git a/apps/web/src/services/iam/components/UserManagementHeader.vue b/apps/web/src/services/iam/components/UserManagementHeader.vue index 36b719f26b..931756e73c 100644 --- a/apps/web/src/services/iam/components/UserManagementHeader.vue +++ b/apps/web/src/services/iam/components/UserManagementHeader.vue @@ -14,10 +14,10 @@ import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-worksp import { useQueryTags } from '@/common/composables/query-tags'; import { useUserListPaginationQuery } from '@/services/iam/composables/use-user-list-pagination-query'; +import { useUserListQuery } from '@/services/iam/composables/use-user-list-query'; import { USER_MODAL_TYPE, USER_SEARCH_HANDLERS } from '@/services/iam/constants/user-constant'; import { useUserPageStore } from '@/services/iam/store/user-page-store'; -import { useUserListQuery } from '../composables/use-user-list-query'; interface Props { diff --git a/apps/web/src/services/iam/components/mfa/UserBulkMFASettingModal.vue b/apps/web/src/services/iam/components/mfa/UserBulkMFASettingModal.vue index 9b46527e0b..5443f6ee50 100644 --- a/apps/web/src/services/iam/components/mfa/UserBulkMFASettingModal.vue +++ b/apps/web/src/services/iam/components/mfa/UserBulkMFASettingModal.vue @@ -18,6 +18,7 @@ import ErrorHandler from '@/common/composables/error/errorHandler'; import UserMFASettingFormLayout from '@/services/iam/components/mfa/UserMFASettingFormLayout.vue'; import { useUserUpdateMutation } from '@/services/iam/composables/mutations/use-user-update-mutation'; +import { useUserListQuery } from '@/services/iam/composables/use-user-list-query'; import { USER_MODAL_MAP } from '@/services/iam/constants/modal.constant'; import { MULTI_FACTOR_AUTH_ITEMS } from '@/services/iam/constants/user-constant'; import { useUserPageStore } from '@/services/iam/store/user-page-store'; @@ -33,19 +34,23 @@ const emit = defineEmits(); const userPageStore = useUserPageStore(); const userPageState = userPageStore.state; const userPageModalState = userPageStore.modalState; -const userPageGetters = userPageStore.getters; const userStore = useUserStore(); /* State */ const selectedMfaType = ref(MULTI_FACTOR_AUTH_ITEMS[0].type); const isRequiredMfa = ref(false); +const selectedUserIds = computed(() => userPageState.selectedUserIds); + +const { userListData: selectedUsers } = useUserListQuery(selectedUserIds); + + /* Computed */ // Only Local Auth Type Users can be updated -const selectedMFAControllableUsers = computed(() => userPageGetters.selectedUsers.filter((user) => user.auth_type === 'LOCAL')); +const selectedMFAControllableUsers = computed(() => selectedUsers.value?.filter((user) => user.auth_type === 'LOCAL') || []); // UI Conditions -const isIncludedExternalAuthTypeUser = computed(() => userPageGetters.selectedUsers.some((user) => user.auth_type !== 'LOCAL')); +const isIncludedExternalAuthTypeUser = computed(() => selectedUsers.value?.some((user) => user.auth_type !== 'LOCAL') || false); /* API */ // Store failed user IDs diff --git a/apps/web/src/services/iam/components/mfa/UserMFASecretKeyDeleteModal.vue b/apps/web/src/services/iam/components/mfa/UserMFASecretKeyDeleteModal.vue index 83142a312d..8d50d5add2 100644 --- a/apps/web/src/services/iam/components/mfa/UserMFASecretKeyDeleteModal.vue +++ b/apps/web/src/services/iam/components/mfa/UserMFASecretKeyDeleteModal.vue @@ -11,6 +11,7 @@ import ErrorHandler from '@/common/composables/error/errorHandler'; import { useUserMfaDisableMutation } from '@/services/iam/composables/mutations/use-user-mfa-disable-mutation'; +import { useUserListQuery } from '@/services/iam/composables/use-user-list-query'; import { USER_MODAL_MAP } from '@/services/iam/constants/modal.constant'; import { USER_MODAL_TYPE } from '@/services/iam/constants/user-constant'; import { useUserPageStore } from '@/services/iam/store/user-page-store'; @@ -24,12 +25,15 @@ const emit = defineEmits(); /* Store */ const userPageStore = useUserPageStore(); +const userPageState = userPageStore.state; const userPageModalState = userPageStore.modalState; -const userPageGetters = userPageStore.getters; /* Computed */ // Only Local Auth Type Users can be disabled (disable = delete secret key) -const selectedMFAEnabledUsers = computed(() => userPageGetters.selectedUsers.filter((user) => user.auth_type === 'LOCAL' && user.mfa?.state === 'ENABLED') || []); +const selectedUserIds = computed(() => userPageState.selectedUserIds); + +const { userListData: selectedUsers } = useUserListQuery(selectedUserIds); +const selectedMFAEnabledUsers = computed(() => selectedUsers.value?.filter((user) => user.auth_type === 'LOCAL' && user.mfa?.state === 'ENABLED') || []); /* API */ // Store failed user IDs From 8e60970055c288363bb82d43dbe0cbe17f54ea75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=89=E1=85=B3=E1=86=BC=E1=84=8B?= =?UTF-8?q?=E1=85=A7=E1=86=AB?= Date: Thu, 14 Aug 2025 16:24:52 +0900 Subject: [PATCH 18/21] fix: bugs about MFA updated at mypage and admin user page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 이승연 --- .../iam/composables/use-user-list-query.ts | 1 + .../UserAccountMultiFactorAuthItems.vue | 8 ++++---- ...erAccountMultiFactorAuthEmailDisableModal.vue | 16 ++++++++++++++-- ...serAccountMultiFactorAuthEmailEnableModal.vue | 16 ++++++++++++++-- ...UserAccountMultiFactorAuthOTPDisableModal.vue | 15 ++++++++++++++- .../UserAccountMultiFactorAuthOTPEnableModal.vue | 15 ++++++++++++++- .../services/my-page/pages/UserAccountPage.vue | 6 ++++-- 7 files changed, 65 insertions(+), 12 deletions(-) diff --git a/apps/web/src/services/iam/composables/use-user-list-query.ts b/apps/web/src/services/iam/composables/use-user-list-query.ts index 4a06f207af..89c4fdc545 100644 --- a/apps/web/src/services/iam/composables/use-user-list-query.ts +++ b/apps/web/src/services/iam/composables/use-user-list-query.ts @@ -53,6 +53,7 @@ export const useUserListQuery = (userIds?: Ref) => { }, ['WORKSPACE']); return { + userListKey, userListData, workspaceUserListData, }; diff --git a/apps/web/src/services/my-page/components/UserAccountMultiFactorAuthItems.vue b/apps/web/src/services/my-page/components/UserAccountMultiFactorAuthItems.vue index e5df4e9569..a672676dcd 100644 --- a/apps/web/src/services/my-page/components/UserAccountMultiFactorAuthItems.vue +++ b/apps/web/src/services/my-page/components/UserAccountMultiFactorAuthItems.vue @@ -37,11 +37,11 @@ const storeState = reactive({ const state = reactive({ isVerified: computed(() => storeState.mfa?.state === 'ENABLED'), currentType: computed(() => userStore.state.mfa?.mfa_type || undefined), - isEnforced: computed(() => !!userStore.state.mfa?.options?.enforce), + isEnforced: computed(() => userStore.state.mfa?.options?.enforce === true), }); const handleChange = async (isSelected: boolean, selected: MultiFactorAuthType) => { - if (props.readonlyMode || state.isEnforced) return; + if (props.readonlyMode) return; if (state.isEnforced && state.currentType !== selected) return; if (state.isVerified && state.currentType !== selected) { if (selected === MULTI_FACTOR_AUTH_TYPE.OTP) { @@ -85,7 +85,7 @@ const handleClickReSyncButton = async (type: MultiFactorAuthType, event: MouseEv {{ $t('MY_PAGE.MFA.RESYNC') }} diff --git a/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthEmailDisableModal.vue b/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthEmailDisableModal.vue index e3355b4fd0..bde8d0ab73 100644 --- a/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthEmailDisableModal.vue +++ b/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthEmailDisableModal.vue @@ -2,9 +2,12 @@ import { reactive, ref, computed } from 'vue'; import type { TranslateResult } from 'vue-i18n'; +import { useQueryClient } from '@tanstack/vue-query'; + import { PButtonModal } from '@cloudforet/mirinae'; import type { UserModel } from '@/api-clients/identity/user/schema/model'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; import { i18n } from '@/translations'; import { useUserStore } from '@/store/user/user-store'; @@ -17,7 +20,7 @@ import VerificationCodeForm from '@/common/components/mfa/components/Verificatio import { useUserProfileConfirmMfaMutation } from '@/common/components/mfa/composables/use-user-profile-confirm-mfa-mutation'; import ErrorHandler from '@/common/composables/error/errorHandler'; - +import { useUserListQuery } from '@/services/iam/composables/use-user-list-query'; import { useMultiFactorAuthStore } from '@/services/my-page/stores/multi-factor-auth-store'; interface Props { @@ -65,13 +68,22 @@ const closeModal = () => { multiFactorAuthStore.setEmailDisableModalVisible(false); } }; + +const queryClient = useQueryClient(); +const { userListKey: userListQueryKey } = useUserListQuery(); +const { key: userGetQueryKey } = useServiceQueryKey('identity', 'user', 'get', { + contextKey: computed(() => userStore.state.userId), +}); /* API */ const { mutateAsync: confirmMfa, isPending: isConfirmingMfa } = useUserProfileConfirmMfaMutation({ - onSuccess: (data: UserModel) => { + onSuccess: async (data: UserModel) => { showSuccessMessage(i18n.t('COMMON.MFA_MODAL.ALT_S_ENABLED'), ''); userStore.setMfa(data.mfa ?? {}); closeModal(); if (props.switch) multiFactorAuthStore.setOTPEnableModalVisible(true); + + await queryClient.invalidateQueries({ queryKey: userListQueryKey.value }); + await queryClient.invalidateQueries({ queryKey: userGetQueryKey.value }); }, onError: (error: Error) => { ErrorHandler.handleError(error); diff --git a/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthEmailEnableModal.vue b/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthEmailEnableModal.vue index 5a8fc32518..3e52c6f188 100644 --- a/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthEmailEnableModal.vue +++ b/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthEmailEnableModal.vue @@ -4,11 +4,14 @@ import { } from 'vue'; import type { TranslateResult } from 'vue-i18n'; +import { useQueryClient } from '@tanstack/vue-query'; + import { PButtonModal, } from '@cloudforet/mirinae'; import type { UserModel } from '@/api-clients/identity/user/schema/model'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; import { i18n } from '@/translations'; import { useUserStore } from '@/store/user/user-store'; @@ -21,7 +24,7 @@ import VerificationCodeForm from '@/common/components/mfa/components/Verificatio import { useUserProfileConfirmMfaMutation } from '@/common/components/mfa/composables/use-user-profile-confirm-mfa-mutation'; import ErrorHandler from '@/common/composables/error/errorHandler'; - +import { useUserListQuery } from '@/services/iam/composables/use-user-list-query'; import { useMultiFactorAuthStore } from '@/services/my-page/stores/multi-factor-auth-store'; interface Props { @@ -69,15 +72,24 @@ const closeModal = () => { } }; +const queryClient = useQueryClient(); +const { userListKey: userListQueryKey } = useUserListQuery(); +const { key: userGetQueryKey } = useServiceQueryKey('identity', 'user', 'get', { + contextKey: computed(() => userStore.state.userId), +}); + /* API */ const { mutateAsync: confirmMfa, isPending: isConfirmingMfa } = useUserProfileConfirmMfaMutation({ - onSuccess: (data: UserModel) => { + onSuccess: async (data: UserModel) => { showSuccessMessage(i18n.t('COMMON.MFA_MODAL.ALT_S_ENABLED'), ''); userStore.setMfa(data.mfa ?? {}); closeModal(); isSentCode.value = false; validationState.verificationCode = ''; if (props.reSync) multiFactorAuthStore.setEmailEnableModalVisible(true); + + await queryClient.invalidateQueries({ queryKey: userListQueryKey.value }); + await queryClient.invalidateQueries({ queryKey: userGetQueryKey.value }); }, onError: (error: Error) => { ErrorHandler.handleError(error); diff --git a/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthOTPDisableModal.vue b/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthOTPDisableModal.vue index dad9962fdd..84b8757fe0 100644 --- a/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthOTPDisableModal.vue +++ b/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthOTPDisableModal.vue @@ -2,11 +2,14 @@ import { computed, reactive } from 'vue'; import type { TranslateResult } from 'vue-i18n'; +import { useQueryClient } from '@tanstack/vue-query'; + import { PButtonModal, } from '@cloudforet/mirinae'; import type { UserModel } from '@/api-clients/identity/user/schema/model'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; import { i18n } from '@/translations'; import { useUserStore } from '@/store/user/user-store'; @@ -17,6 +20,7 @@ import VerificationCodeForm from '@/common/components/mfa/components/Verificatio import { useUserProfileConfirmMfaMutation } from '@/common/components/mfa/composables/use-user-profile-confirm-mfa-mutation'; import ErrorHandler from '@/common/composables/error/errorHandler'; +import { useUserListQuery } from '@/services/iam/composables/use-user-list-query'; import { useMultiFactorAuthStore } from '@/services/my-page/stores/multi-factor-auth-store'; @@ -64,13 +68,22 @@ const closeModal = () => { } }; +const queryClient = useQueryClient(); +const { userListKey: userListQueryKey } = useUserListQuery(); +const { key: userGetQueryKey } = useServiceQueryKey('identity', 'user', 'get', { + contextKey: computed(() => userStore.state.userId), +}); + /* API */ const { mutateAsync: confirmMfa, isPending: isConfirmingMfa } = useUserProfileConfirmMfaMutation({ - onSuccess: (data: UserModel) => { + onSuccess: async (data: UserModel) => { showSuccessMessage(i18n.t('COMMON.MFA_MODAL.ALT_S_DISABLED'), ''); userStore.setMfa(data.mfa ?? {}); closeModal(); if (props.switch) multiFactorAuthStore.setEmailEnableModalVisible(true); + + await queryClient.invalidateQueries({ queryKey: userListQueryKey.value }); + await queryClient.invalidateQueries({ queryKey: userGetQueryKey.value }); }, onError: (error: Error) => { ErrorHandler.handleError(error); diff --git a/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthOTPEnableModal.vue b/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthOTPEnableModal.vue index 7d768dd477..c606b4ebb6 100644 --- a/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthOTPEnableModal.vue +++ b/apps/web/src/services/my-page/components/mfa/UserAccountMultiFactorAuthOTPEnableModal.vue @@ -2,11 +2,14 @@ import { reactive, computed } from 'vue'; import type { TranslateResult } from 'vue-i18n'; +import { useQueryClient } from '@tanstack/vue-query'; + import { PButtonModal, } from '@cloudforet/mirinae'; import type { UserModel } from '@/api-clients/identity/user/schema/model'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; import { i18n } from '@/translations'; import { useUserStore } from '@/store/user/user-store'; @@ -18,6 +21,7 @@ import VerificationCodeForm from '@/common/components/mfa/components/Verificatio import { useUserProfileConfirmMfaMutation } from '@/common/components/mfa/composables/use-user-profile-confirm-mfa-mutation'; import ErrorHandler from '@/common/composables/error/errorHandler'; +import { useUserListQuery } from '@/services/iam/composables/use-user-list-query'; import { useMultiFactorAuthStore } from '@/services/my-page/stores/multi-factor-auth-store'; @@ -65,14 +69,23 @@ const closeModal = () => { } }; +const queryClient = useQueryClient(); +const { userListKey: userListQueryKey } = useUserListQuery(); +const { key: userGetQueryKey } = useServiceQueryKey('identity', 'user', 'get', { + contextKey: computed(() => userStore.state.userId), +}); + /* API */ const { mutateAsync: confirmMfa, isPending: isConfirmingMfa } = useUserProfileConfirmMfaMutation({ - onSuccess: (data: UserModel) => { + onSuccess: async (data: UserModel) => { showSuccessMessage(i18n.t('COMMON.MFA_MODAL.ALT_S_ENABLED'), ''); userStore.setMfa(data.mfa ?? {}); closeModal(); validationState.verificationCode = ''; if (props.reSync) multiFactorAuthStore.setOTPEnableModalVisible(true); + + await queryClient.invalidateQueries({ queryKey: userListQueryKey.value }); + await queryClient.invalidateQueries({ queryKey: userGetQueryKey.value }); }, onError: (error: Error) => { ErrorHandler.handleError(error); diff --git a/apps/web/src/services/my-page/pages/UserAccountPage.vue b/apps/web/src/services/my-page/pages/UserAccountPage.vue index d5d546f32c..a794584d14 100644 --- a/apps/web/src/services/my-page/pages/UserAccountPage.vue +++ b/apps/web/src/services/my-page/pages/UserAccountPage.vue @@ -90,7 +90,7 @@ const handleConfirmPasswordCheckModal = async () => { user_id: passwordFormState.userId, password: passwordFormState.password, }, - }, { skipAuthRefresh: true }); + }, { skipAuthRefresh: true } as any); if (result.status === 'succeed') { if (!!result.response.access_token && !!result.response.refresh_token) { passwordFormState.certifiedPassword = passwordFormState.password; @@ -104,7 +104,9 @@ const handleConfirmPasswordCheckModal = async () => { } } } catch (e: any) { - if (e.message.startsWith(' MFA is required.')) { // MFA activated CASE + const message = e.message || ''; + if (message.startsWith(' MFA is required.') || message.includes(' MFA is not activated.')) { + // MFA activated CASE OR MFA not activated CASE - both should proceed successfully passwordFormState.certifiedPassword = passwordFormState.password; passwordFormState.isTokenChecked = true; passwordFormState.invalidText = ''; From 733cb1d91a4891a298ca7ec8d5eb03c2433265ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=89=E1=85=B3=E1=86=BC=E1=84=8B?= =?UTF-8?q?=E1=85=A7=E1=86=AB?= Date: Thu, 14 Aug 2025 18:01:55 +0900 Subject: [PATCH 19/21] fix: major bug of not active of bulk MFA setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 이승연 --- .../mfa/UserBulkMFASettingModal.vue | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/apps/web/src/services/iam/components/mfa/UserBulkMFASettingModal.vue b/apps/web/src/services/iam/components/mfa/UserBulkMFASettingModal.vue index 5443f6ee50..e8b41cd9ea 100644 --- a/apps/web/src/services/iam/components/mfa/UserBulkMFASettingModal.vue +++ b/apps/web/src/services/iam/components/mfa/UserBulkMFASettingModal.vue @@ -1,13 +1,17 @@