From f5246d544dc1fbc9c21dc9b601d0b1099f0eadda Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:11:22 -0700 Subject: [PATCH] feat: mfa reset - wip --- .../client/src/ee/mfa/services/mfa-service.ts | 7 +++ .../components/members-action-menu.tsx | 50 +++++++++++++++++-- .../components/workspace-members-table.tsx | 2 +- .../workspace/queries/workspace-query.ts | 27 ++++++++++ apps/server/src/ee | 2 +- 5 files changed, 82 insertions(+), 6 deletions(-) diff --git a/apps/client/src/ee/mfa/services/mfa-service.ts b/apps/client/src/ee/mfa/services/mfa-service.ts index bf49d2eb..68b0ce1a 100644 --- a/apps/client/src/ee/mfa/services/mfa-service.ts +++ b/apps/client/src/ee/mfa/services/mfa-service.ts @@ -59,3 +59,10 @@ export async function validateMfaAccess(): Promise return { valid: false }; } } + +export async function resetUserMfa( + userId: string, +): Promise<{ success: boolean }> { + const req = await api.post<{ success: boolean }>('/mfa/reset', { userId }); + return req.data; +} diff --git a/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx b/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx index 9c1b705b..72e02653 100644 --- a/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx +++ b/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx @@ -1,23 +1,41 @@ import { Menu, ActionIcon, Text } from "@mantine/core"; import React from "react"; -import { IconDots, IconTrash } from "@tabler/icons-react"; +import { IconDots, IconTrash, IconShieldOff } from "@tabler/icons-react"; import { modals } from "@mantine/modals"; -import { useDeleteWorkspaceMemberMutation } from "@/features/workspace/queries/workspace-query.ts"; +import { + useDeleteWorkspaceMemberMutation, + useResetUserMfaMutation +} from "@/features/workspace/queries/workspace-query.ts"; import { useTranslation } from "react-i18next"; import useUserRole from "@/hooks/use-user-role.tsx"; +import { useLicense } from "@/ee/hooks/use-license.tsx"; +import { isCloud } from "@/lib/config.ts"; +import { UserRole } from "@/lib/types.ts"; interface Props { userId: string; + userRole: string; } -export default function MemberActionMenu({ userId }: Props) { +export default function MemberActionMenu({ userId, userRole }: Props) { const { t } = useTranslation(); const deleteWorkspaceMemberMutation = useDeleteWorkspaceMemberMutation(); - const { isAdmin } = useUserRole(); + const resetUserMfaMutation = useResetUserMfaMutation(); + const { isAdmin, isOwner } = useUserRole(); + const { hasLicenseKey } = useLicense(); + + // Show MFA reset only for self-hosted enterprise edition + // Admins cannot reset MFA for owners + const canResetMfa = isOwner || (isAdmin && userRole !== UserRole.OWNER); + const showMfaReset = !isCloud() && hasLicenseKey && canResetMfa; const onRevoke = async () => { await deleteWorkspaceMemberMutation.mutateAsync({ userId }); }; + const onResetMfa = async () => { + await resetUserMfaMutation.mutateAsync({ userId }); + }; + const openRevokeModal = () => modals.openConfirmModal({ title: t("Delete member"), @@ -34,6 +52,22 @@ export default function MemberActionMenu({ userId }: Props) { onConfirm: onRevoke, }); + const openResetMfaModal = () => + modals.openConfirmModal({ + title: t("Reset MFA"), + children: ( + + {t( + "Are you sure you want to reset MFA for this user? They will need to set up MFA again.", + )} + + ), + centered: true, + labels: { confirm: t("Reset"), cancel: t("Cancel") }, + confirmProps: { color: "red" }, + onConfirm: onResetMfa, + }); + return ( <> + {showMfaReset && ( + } + > + {t("Reset MFA")} + + )} - {isAdmin && } + {isAdmin && } )) diff --git a/apps/client/src/features/workspace/queries/workspace-query.ts b/apps/client/src/features/workspace/queries/workspace-query.ts index 6bf35aec..b75a9d7a 100644 --- a/apps/client/src/features/workspace/queries/workspace-query.ts +++ b/apps/client/src/features/workspace/queries/workspace-query.ts @@ -18,6 +18,7 @@ import { getAppVersion, deleteWorkspaceMember, } from "@/features/workspace/services/workspace-service"; +import { resetUserMfa } from "@/ee/mfa"; import { IPagination, QueryParams } from "@/lib/types.ts"; import { notifications } from "@mantine/notifications"; import { @@ -192,3 +193,29 @@ export function useAppVersion( refetchOnMount: true, }); } + +export function useResetUserMfaMutation() { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation< + { success: boolean }, + Error, + { userId: string } + >({ + mutationFn: ({ userId }) => resetUserMfa(userId), + onSuccess: () => { + notifications.show({ + message: t("MFA has been reset successfully"), + color: "green" + }); + queryClient.invalidateQueries({ + queryKey: ["workspaceMembers"], + }); + }, + onError: (error) => { + const errorMessage = error["response"]?.data?.message || t("Failed to reset MFA"); + notifications.show({ message: errorMessage, color: "red" }); + }, + }); +} diff --git a/apps/server/src/ee b/apps/server/src/ee index c889c880..2f4ed496 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit c889c880019a24094543b9caadc566114b235ddf +Subproject commit 2f4ed496096d7f3058f973531b089d5f1e35f08f