mirror of
https://github.com/langgenius/dify.git
synced 2026-06-01 06:28:14 +08:00
fix(auth): use validity-returned token in ChangePasswordForm reset submit [cherry-pick → hotfix/1.14.1-fix.3]
Cherry-pick of #36415. The two-phase token flow (T1 → /validity → T2 with phase="reset" → /resets) was broken because ForgotPasswordValidity type was missing the `token` field, so T2 was discarded and T1 (from URL) was re-sent to /resets. Backend rejects T1 at /resets because it requires phase="reset". Fix: add `token` to the type and use verifyTokenRes.token in handleChangePassword. Adds a test that asserts T2 (validity response token) is submitted, not T1 (URL token). Fixes ENG-423.
This commit is contained in:
86
web/app/forgot-password/ChangePasswordForm.spec.tsx
Normal file
86
web/app/forgot-password/ChangePasswordForm.spec.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { changePasswordWithToken } from '@/service/common'
|
||||
import { useVerifyForgotPasswordToken } from '@/service/use-common'
|
||||
import ChangePasswordForm from './ChangePasswordForm'
|
||||
|
||||
const mockReplace = vi.fn()
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useSearchParams: () => new URLSearchParams('token=url-token-t1'),
|
||||
useRouter: () => ({ replace: mockReplace }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useVerifyForgotPasswordToken: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
changePasswordWithToken: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/var', () => ({ basePath: '' }))
|
||||
|
||||
type UseVerifyResult = ReturnType<typeof useVerifyForgotPasswordToken>
|
||||
const mockUseVerify = vi.mocked(useVerifyForgotPasswordToken)
|
||||
const mockChangePassword = vi.mocked(changePasswordWithToken)
|
||||
|
||||
const VALID_PASSWORD = 'ValidPass123!'
|
||||
|
||||
describe('ChangePasswordForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('when token is valid', () => {
|
||||
const T2 = 'verified-token-t2'
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseVerify.mockReturnValue({
|
||||
data: { result: 'success', is_valid: true, email: 'user@example.com', token: T2 },
|
||||
refetch: vi.fn(),
|
||||
} as unknown as UseVerifyResult)
|
||||
})
|
||||
|
||||
it('renders the password form', () => {
|
||||
render(<ChangePasswordForm />)
|
||||
expect(screen.getByText('login.changePassword')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('submits with T2 (from validity response), NOT T1 (from URL)', async () => {
|
||||
mockChangePassword.mockResolvedValue({ result: 'success' })
|
||||
|
||||
render(<ChangePasswordForm />)
|
||||
|
||||
const inputs = Array.from(document.querySelectorAll<HTMLInputElement>('input[type="password"]')) as [HTMLInputElement, HTMLInputElement]
|
||||
fireEvent.change(inputs[0], { target: { value: VALID_PASSWORD } })
|
||||
fireEvent.change(inputs[1], { target: { value: VALID_PASSWORD } })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.reset/ }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockChangePassword).toHaveBeenCalledWith({
|
||||
url: '/forgot-password/resets',
|
||||
body: {
|
||||
token: T2,
|
||||
new_password: VALID_PASSWORD,
|
||||
password_confirm: VALID_PASSWORD,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when token is invalid', () => {
|
||||
beforeEach(() => {
|
||||
mockUseVerify.mockReturnValue({
|
||||
data: { result: 'success', is_valid: false, email: '', token: '' },
|
||||
refetch: vi.fn(),
|
||||
} as unknown as UseVerifyResult)
|
||||
})
|
||||
|
||||
it('shows invalid token state and no form', () => {
|
||||
render(<ChangePasswordForm />)
|
||||
expect(screen.getByText('login.invalid')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /common\.operation\.reset/ })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -49,7 +49,7 @@ const ChangePasswordForm = () => {
|
||||
}, [password, confirmPassword, showErrorMessage, t])
|
||||
|
||||
const handleChangePassword = useCallback(async () => {
|
||||
const token = searchParams.get('token') || ''
|
||||
const resetToken = verifyTokenRes?.token ?? ''
|
||||
|
||||
if (!valid())
|
||||
return
|
||||
@ -57,7 +57,7 @@ const ChangePasswordForm = () => {
|
||||
await changePasswordWithToken({
|
||||
url: '/forgot-password/resets',
|
||||
body: {
|
||||
token,
|
||||
token: resetToken,
|
||||
new_password: password,
|
||||
password_confirm: confirmPassword,
|
||||
},
|
||||
@ -67,7 +67,7 @@ const ChangePasswordForm = () => {
|
||||
catch {
|
||||
await revalidateToken()
|
||||
}
|
||||
}, [confirmPassword, password, revalidateToken, searchParams, valid])
|
||||
}, [confirmPassword, password, revalidateToken, verifyTokenRes?.token, valid])
|
||||
|
||||
return (
|
||||
<div className={
|
||||
|
||||
@ -246,7 +246,7 @@ export const useLogout = () => {
|
||||
})
|
||||
}
|
||||
|
||||
type ForgotPasswordValidity = CommonResponse & { is_valid: boolean, email: string }
|
||||
type ForgotPasswordValidity = CommonResponse & { is_valid: boolean, email: string, token: string }
|
||||
export const useVerifyForgotPasswordToken = (token?: string | null) => {
|
||||
return useQuery<ForgotPasswordValidity>({
|
||||
queryKey: commonQueryKeys.forgotPasswordValidity(token),
|
||||
|
||||
Reference in New Issue
Block a user