diff --git a/docs/demos/2572-reset-password-demo.png b/docs/demos/2572-reset-password-demo.png new file mode 100644 index 0000000000..ddd081fdfc Binary files /dev/null and b/docs/demos/2572-reset-password-demo.png differ diff --git a/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx b/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx index 0bd19ebbdf..bf88a97cbf 100644 --- a/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx +++ b/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx @@ -3,7 +3,7 @@ import {useState} from "react" import type {User} from "@agenta/shared/types" import {message} from "@agenta/ui/app-message" import {EditOutlined, MoreOutlined, SyncOutlined} from "@ant-design/icons" -import {ArrowClockwise, Trash} from "@phosphor-icons/react" +import {ArrowClockwise, Key, Trash} from "@phosphor-icons/react" import {Button, Dropdown, Input, Modal, Space, Tag, Tooltip, Typography} from "antd" import AlertPopup from "@/oss/components/AlertPopup/AlertPopup" @@ -11,7 +11,7 @@ import {useWorkspacePermissions} from "@/oss/hooks/useWorkspacePermissions" import {isEmailInvitationsEnabled} from "@/oss/lib/helpers/isEE" import {snakeToTitle} from "@/oss/lib/helpers/utils" import {WorkspaceMember} from "@/oss/lib/Types" -import {updateUsername} from "@/oss/services/profile" +import {resetPassword, updateUsername} from "@/oss/services/profile" import { assignWorkspaceRole, removeFromWorkspace, @@ -22,6 +22,9 @@ import {useOrgData} from "@/oss/state/org" import {useProfileData} from "@/oss/state/profile" import {useWorkspaceRoles} from "@/oss/state/workspace" +import GenerateResetLinkModal from "./Modals/GenerateResetLinkModal" +import PasswordResetLinkModal from "./Modals/PasswordResetLinkModal" + export const Actions: React.FC<{ member: WorkspaceMember hidden?: boolean @@ -39,6 +42,13 @@ export const Actions: React.FC<{ const {refetch: refetchProfile} = useProfileData() const [renameOpen, setRenameOpen] = useState(false) const [renameValue, setRenameValue] = useState(user.username || "") + const [generateResetLinkOpen, setGenerateResetLinkOpen] = useState(false) + const [resetLinkOpen, setResetLinkOpen] = useState(false) + const [resetLink, setResetLink] = useState("") + const [resetLoading, setResetLoading] = useState(false) + + // OSS: invite permission is broadly available; EE: require invite or role-modify rights. + const canResetPassword = canInviteMembers || canModifyRoles if (hidden && !selfMenu) return null if (!selfMenu && !canInviteMembers && !canModifyRoles) return null @@ -91,6 +101,24 @@ export const Actions: React.FC<{ } } + const handleResetPassword = async () => { + setResetLoading(true) + try { + const link = await resetPassword(user.id) + setGenerateResetLinkOpen(false) + setResetLink(typeof link === "string" ? link : String(link)) + setResetLinkOpen(true) + } catch (error: any) { + const detail = + error?.response?.data?.detail || + error?.message || + "Unable to generate reset password link" + message.error(detail) + } finally { + setResetLoading(false) + } + } + return ( <> , + onClick: (e: any) => { + e.domEvent.stopPropagation() + setGenerateResetLinkOpen(true) + }, + }, + ] + : []), ...(canModifyRoles ? [ { @@ -149,7 +190,7 @@ export const Actions: React.FC<{ onClick={(e) => e.stopPropagation()} type="text" icon={} - loading={resendLoading} + loading={resendLoading || resetLoading} /> @@ -170,6 +211,21 @@ export const Actions: React.FC<{ placeholder="New username" /> + + setGenerateResetLinkOpen(false)} + onOk={handleResetPassword} + confirmLoading={resetLoading} + /> + + setResetLinkOpen(false)} + /> ) } diff --git a/web/oss/src/services/profile/index.ts b/web/oss/src/services/profile/index.ts index 18d6e28488..572aced3a2 100644 --- a/web/oss/src/services/profile/index.ts +++ b/web/oss/src/services/profile/index.ts @@ -57,6 +57,19 @@ export const changePassword = async (payload: { }) } +/** + * Generate a password reset link for a workspace member (admin action). + * Calls POST /api/profile/reset-password?user_id=... + */ +export const resetPassword = async (userId: string): Promise => { + const base = getBaseUrl() + const url = new URL("api/profile/reset-password", base) + url.searchParams.set("user_id", userId) + return fetchJson(url, { + method: "POST", + }) +} + /** * Permanently delete the current user's account (EE only). Removes the user * from the database (with the organizations they own), the auth provider,