Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/demos/2572-reset-password-demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ 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"
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,
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 (
<>
<Dropdown
Expand Down Expand Up @@ -128,6 +156,19 @@ export const Actions: React.FC<{
},
]
: []),
...(isMember && canResetPassword
? [
{
key: "reset_password",
label: "Reset password",
icon: <Key size={16} />,
onClick: (e: any) => {
e.domEvent.stopPropagation()
setGenerateResetLinkOpen(true)
},
},
]
: []),
...(canModifyRoles
? [
{
Expand All @@ -149,7 +190,7 @@ export const Actions: React.FC<{
onClick={(e) => e.stopPropagation()}
type="text"
icon={<MoreOutlined />}
loading={resendLoading}
loading={resendLoading || resetLoading}
/>
</Dropdown>

Expand All @@ -170,6 +211,21 @@ export const Actions: React.FC<{
placeholder="New username"
/>
</Modal>

<GenerateResetLinkModal
open={generateResetLinkOpen}
username={user.username}
onCancel={() => setGenerateResetLinkOpen(false)}
onOk={handleResetPassword}
confirmLoading={resetLoading}
/>

<PasswordResetLinkModal
open={resetLinkOpen}
username={user.username}
generatedLink={resetLink}
onCancel={() => setResetLinkOpen(false)}
/>
</>
)
}
Expand Down
13 changes: 13 additions & 0 deletions web/oss/src/services/profile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> => {
const base = getBaseUrl()
const url = new URL("api/profile/reset-password", base)
url.searchParams.set("user_id", userId)
return fetchJson<string>(url, {
method: "POST",
})
}
Comment on lines +60 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd 'index\.ts$' . | sed -n '1,80p' | rg 'web/oss/src/services/profile|resources|profile' || true

echo "== profile service excerpt =="
if [ -f web/oss/src/services/profile/index.ts ]; then
  wc -l web/oss/src/services/profile/index.ts
  sed -n '1,140p' web/oss/src/services/profile/index.ts | cat -n
fi

echo "== search resetPassword usages =="
rg -n "resetPassword|reset-password" web -S || true

echo "== profile Fern accessors/resources relevant symbols =="
rg -n "profile|Profile|resetPassword|queryParams|safeParseWithLogging|reset-password|fetchJson" web/packages/agenta-sdk web -S --glob '*.ts' --glob '*.tsx' | sed -n '1,220p' || true

echo "== package script / lint config if available =="
fd 'package\.json|eslint|prettier|AGENTS\.md' . | sed -n '1,60p'

Repository: Agenta-AI/agenta

Length of output: 29422


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== generated users reset-password excerpt =="
sed -n '210,300p' web/packages/agenta-api-client/src/generated/api/resources/users/client/Client.ts | cat -n || true

echo "== search resetPassword in generated package =="
rg -n "resetPassword|ResetPassword|reset-password|safeParseWithLogging|queryParams" web/packages/agenta-api-client -S --glob '*.ts' || true

echo "== referenced SDK resource/profile excerpts if present =="
if [ -f web/packages/agenta-sdk/src/resources/users.ts ]; then
  echo "== users.ts resetPassword? =="
  rg -n "resetPassword|ResetPassword|reset-password|queryParams" web/packages/agenta-sdk/src/resources/users.ts -S || true
fi

echo "== use the cell renderer excerpt =="
sed -n '90,125p' web/oss/src/pages/settings/cellRenderers.tsx 2>/dev/null || sed -n '90,125p' web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx | cat -n

echo "== AST/text check resetPassword generic at string type only =="
python3 - <<'PY'
from pathlib import Path
p = Path('web/oss/src/services/profile/index.ts')
text = p.read_text()
start = text.index('export const resetPassword')
end = text.index('\n\n/**', start)
snippet = text[start:end]
print(snippet)
print("uses fetchJson<string>:", "fetchJson<string>" in snippet)
print("uses queryParams:", "queryParams" in snippet)
print("imports safeParseWithLogging:", "safeParseWithLogging" in p.read_text())
PY

Repository: Agenta-AI/agenta

Length of output: 50372


Use the generated users.resetUserPassword accessor and validate the response.

The generated client already exposes resetUserPassword({ user_id }); use that Fern-style accessor instead of a manual new URL(...)?user_id=... request. Since the generated response remains unknown, validate the reset link with Zod and safeParseWithLogging before returning it to the UI so malformed JSON cannot be displayed as [object Object].

Source: Coding guidelines


/**
* Permanently delete the current user's account (EE only). Removes the user
* from the database (with the organizations they own), the auth provider,
Expand Down
Loading