refactor(web): migrate console auth, account and workspace calls in service/common

Sign-in, setup/init, forgot/reset password, email-code login, invitation
activation, account deletion, email change, ownership transfer, member
removal, workspace rename, default model, model enable/disable and the
compliance download now call the console contract directly. Only the
web-app (public API) helpers and remote file upload remain in common.ts.

The generated response types exposed unreachable branches: console login
never returns an access token in the body, and email-code verification
and password-reset requests raise errors instead of returning failure
codes, so those branches are removed. Default model and parameter-rule
responses are normalized where they enter the model provider UI types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Stephen Zhou
2026-10-15 23:42:04 +08:00
co-authored by Claude Opus 5.5
parent 7915c5e5ff
commit bdf872c810
45 changed files with 623 additions and 702 deletions
+1 -6
View File
@@ -1814,11 +1814,6 @@
"count": 4
}
},
"web/app/components/header/account-setting/model-provider-page/hooks.ts": {
"@tanstack/query/prefer-query-options": {
"count": 1
}
},
"web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 2
@@ -4067,7 +4062,7 @@
"count": 1
},
"typescript/no-explicit-any": {
"count": 4
"count": 1
}
},
"web/service/datasets.ts": {
@@ -12,7 +12,7 @@ import { emailRegex } from '@/config'
import useDocumentTitle from '@/hooks/use-document-title'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { sendResetPasswordCode } from '@/service/common'
import { consoleClient } from '@/service/console'
export default function CheckCode() {
const { t } = useTranslation(['login'])
@@ -37,17 +37,13 @@ export default function CheckCode() {
return
}
setIsLoading(true)
const res = await sendResetPasswordCode(email, locale)
const res = await consoleClient.forgotPassword.post({ body: { email, language: locale } })
if (res.result === 'success') {
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(res.data))
params.set('token', encodeURIComponent(res.data ?? ''))
params.set('email', encodeURIComponent(email))
router.push(`/webapp-reset-password/check-code?${params.toString()}`)
} else if (res.code === 'account_not_found') {
toast.error(t(($) => $['error.registrationNotAllowed'], { ns: 'login' }))
} else {
toast.error(res.data)
}
} catch (error) {
console.error(error)
@@ -1,7 +1,7 @@
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { toast } from '@/app/notifications'
import { checkEmailExisted, resetEmail, sendVerifyCode, verifyEmail } from '@/service/common'
import { consoleClient } from '@/service/console'
import { createHttpError } from '@/service/http-error'
import EmailChangeModal from '../email-change-modal'
@@ -11,11 +11,24 @@ vi.mock('@/app/notifications', () => ({
toast: { error: vi.fn() },
}))
vi.mock('@/service/common', () => ({
checkEmailExisted: vi.fn(),
resetEmail: vi.fn(),
sendVerifyCode: vi.fn(),
verifyEmail: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
account: {
changeEmail: {
checkEmailUnique: {
post: vi.fn(),
},
reset: {
post: vi.fn(),
},
post: vi.fn(),
validity: {
post: vi.fn(),
},
},
},
},
}))
vi.mock('@/service/use-common', () => ({
@@ -25,14 +38,18 @@ vi.mock('@/service/use-common', () => ({
describe('EmailChangeModal', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(sendVerifyCode).mockResolvedValue({ result: 'success', data: 'email-token' })
vi.mocked(verifyEmail).mockResolvedValue({
vi.mocked(consoleClient.account.changeEmail.post).mockResolvedValue({
result: 'success',
data: 'email-token',
})
vi.mocked(consoleClient.account.changeEmail.validity.post).mockResolvedValue({
email: 'old@example.com',
is_valid: true,
token: 'verified-token',
})
vi.mocked(checkEmailExisted).mockResolvedValue({ result: 'success' })
vi.mocked(consoleClient.account.changeEmail.checkEmailUnique.post).mockResolvedValue({
result: 'success',
})
})
it('submits the labeled verification fields with Enter through both email phases', async () => {
@@ -46,10 +63,12 @@ describe('EmailChangeModal', () => {
name: 'accountSettings.account.changeEmail.codeLabel',
})
await user.type(originCode, '123456{Enter}')
expect(verifyEmail).toHaveBeenCalledWith({
email: 'old@example.com',
code: '123456',
token: 'email-token',
expect(consoleClient.account.changeEmail.validity.post).toHaveBeenCalledWith({
body: {
email: 'old@example.com',
code: '123456',
token: 'email-token',
},
})
const email = await screen.findByRole('textbox', {
@@ -62,10 +81,12 @@ describe('EmailChangeModal', () => {
).toBeEnabled(),
)
await user.type(email, '{Enter}')
expect(sendVerifyCode).toHaveBeenLastCalledWith({
email: 'new@example.com',
phase: 'new_email',
token: 'verified-token',
expect(consoleClient.account.changeEmail.post).toHaveBeenLastCalledWith({
body: {
email: 'new@example.com',
phase: 'new_email',
token: 'verified-token',
},
})
const newCode = await screen.findByRole('textbox', {
@@ -74,16 +95,18 @@ describe('EmailChangeModal', () => {
expect(newCode).toHaveValue('')
await user.type(newCode, '654321{Enter}')
await waitFor(() =>
expect(resetEmail).toHaveBeenCalledWith({
new_email: 'new@example.com',
token: 'verified-token',
expect(consoleClient.account.changeEmail.reset.post).toHaveBeenCalledWith({
body: {
new_email: 'new@example.com',
token: 'verified-token',
},
}),
)
expect(mockLogout).toHaveBeenCalledOnce()
})
it('associates an unavailable-email error and clears it when the user edits the address', async () => {
vi.mocked(checkEmailExisted).mockRejectedValue(
vi.mocked(consoleClient.account.changeEmail.checkEmailUnique.post).mockRejectedValue(
createHttpError(new Response(null, { status: 400 }), { code: 'email_already_in_use' }),
)
const user = userEvent.setup()
@@ -112,7 +135,7 @@ describe('EmailChangeModal', () => {
})
it('shows the server message when sending a verification code fails', async () => {
vi.mocked(sendVerifyCode).mockRejectedValueOnce(
vi.mocked(consoleClient.account.changeEmail.post).mockRejectedValueOnce(
createHttpError(new Response(null, { status: 429 }), {
code: 'rate_limit_exceeded',
message: 'Try again in one minute',
@@ -130,12 +153,12 @@ describe('EmailChangeModal', () => {
'Error sending verification code: Try again in one minute',
),
)
expect(verifyEmail).not.toHaveBeenCalled()
expect(consoleClient.account.changeEmail.validity.post).not.toHaveBeenCalled()
expect(mockLogout).not.toHaveBeenCalled()
})
it('preserves the verification step after an HTTP error and allows another attempt', async () => {
vi.mocked(verifyEmail).mockRejectedValueOnce(
vi.mocked(consoleClient.account.changeEmail.validity.post).mockRejectedValueOnce(
createHttpError(new Response(null, { status: 400 }), {
code: 'invalid_code',
message: 'Verification code has expired',
@@ -161,7 +184,7 @@ describe('EmailChangeModal', () => {
expect(
screen.queryByRole('textbox', { name: 'accountSettings.account.changeEmail.emailLabel' }),
).not.toBeInTheDocument()
expect(resetEmail).not.toHaveBeenCalled()
expect(consoleClient.account.changeEmail.reset.post).not.toHaveBeenCalled()
await user.type(originCode, '654321{Enter}')
@@ -170,10 +193,12 @@ describe('EmailChangeModal', () => {
name: 'accountSettings.account.changeEmail.emailLabel',
}),
).toBeVisible()
expect(verifyEmail).toHaveBeenLastCalledWith({
email: 'old@example.com',
code: '654321',
token: 'email-token',
expect(consoleClient.account.changeEmail.validity.post).toHaveBeenLastCalledWith({
body: {
email: 'old@example.com',
code: '654321',
token: 'email-token',
},
})
})
})
@@ -9,7 +9,7 @@ import { useDebounceFn } from 'ahooks'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { toast } from '@/app/notifications'
import { checkEmailExisted, resetEmail, sendVerifyCode, verifyEmail } from '@/service/common'
import { consoleClient } from '@/service/console'
import { useLogout } from '@/service/use-common'
import { getErrorMessage } from '@/utils/error-message'
@@ -67,10 +67,8 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
const sendEmail = async (email: string, isOrigin: boolean, token?: string) => {
try {
const res = await sendVerifyCode({
email,
phase: isOrigin ? 'old_email' : 'new_email',
token,
const res = await consoleClient.account.changeEmail.post({
body: { email, phase: isOrigin ? 'old_email' : 'new_email', token },
})
startCount()
if (res.data) setStepToken(res.data)
@@ -86,10 +84,8 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
callback?: (token: string) => void,
) => {
try {
const res = await verifyEmail({
email,
code,
token,
const res = await consoleClient.account.changeEmail.validity.post({
body: { email, code, token },
})
if (res.is_valid) {
setStepToken(res.token)
@@ -119,9 +115,10 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
const checkNewEmailExisted = async (email: string) => {
setIsCheckingEmail(true)
try {
await checkEmailExisted({
email,
})
await consoleClient.account.changeEmail.checkEmailUnique.post(
{ body: { email } },
{ context: { silent: true } },
)
if (latestEmailRef.current !== email) return
setNewEmailExited(false)
setUnAvailableEmail(false)
@@ -172,9 +169,8 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
const { mutateAsync: logout } = useLogout()
const updateEmail = async (lastToken: string) => {
try {
await resetEmail({
new_email: mail.trim(),
token: lastToken,
await consoleClient.account.changeEmail.reset.post({
body: { new_email: mail.trim(), token: lastToken },
})
await logout()
} catch (error) {
@@ -1,10 +1,6 @@
import { useMutation } from '@tanstack/react-query'
import { create } from 'zustand'
import {
sendDeleteAccountCode,
submitDeleteAccountFeedback,
verifyDeleteAccountCode,
} from '@/service/common'
import { consoleClient } from '@/service/console'
type State = {
sendEmailToken: string
@@ -20,7 +16,7 @@ export function useSendDeleteAccountEmail() {
const updateEmailToken = useAccountDeleteStore((state) => state.setSendEmailToken)
return useMutation({
mutationKey: ['delete-account'],
mutationFn: sendDeleteAccountCode,
mutationFn: () => consoleClient.account.delete.verify.get(),
onSuccess: (ret) => {
if (ret.result === 'success') updateEmailToken(ret.data)
},
@@ -30,13 +26,15 @@ export function useSendDeleteAccountEmail() {
export function useConfirmDeleteAccount() {
return useMutation({
mutationKey: ['confirm-delete-account'],
mutationFn: verifyDeleteAccountCode,
mutationFn: (body: { code: string; token: string }) =>
consoleClient.account.delete.post({ body }),
})
}
export function useDeleteAccountFeedback() {
return useMutation({
mutationKey: ['delete-account-feedback'],
mutationFn: submitDeleteAccountFeedback,
mutationFn: (body: { feedback: string; email: string }) =>
consoleClient.account.delete.feedback.post({ body }),
})
}
@@ -7,14 +7,23 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render as renderWithoutPricing, screen, waitFor } from '@testing-library/react'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import { toast } from '@/app/notifications'
import { getDocDownloadUrl } from '@/service/common'
import { consoleClient } from '@/service/console'
import { seedFeatures } from '@/test/console/query-data'
import { downloadUrl } from '@/utils/download'
import Compliance from '../compliance'
const onPricingUrlUpdate = vi.hoisted(() => vi.fn())
vi.mock('@/service/common', () => ({ getDocDownloadUrl: vi.fn() }))
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
compliance: {
download: {
get: vi.fn(),
},
},
},
}))
vi.mock('@/service/base', () => ({
request: vi.fn(() => new Promise(() => {})),
sseGeneratorPost: vi.fn(),
@@ -83,7 +92,9 @@ describe('Compliance', () => {
it('keeps GDPR downloadable while plan-dependent documents wait for features', async () => {
queryClient.clear()
vi.mocked(getDocDownloadUrl).mockResolvedValue({ url: 'https://example.com/gdpr.pdf' })
vi.mocked(consoleClient.compliance.download.get).mockResolvedValue({
url: 'https://example.com/gdpr.pdf',
})
openMenuAndRender()
expect(screen.queryByText('common.compliance.soc2Type1')).not.toBeInTheDocument()
expect(screen.queryByText('common.compliance.soc2Type2')).not.toBeInTheDocument()
@@ -94,7 +105,12 @@ describe('Compliance', () => {
)
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
fireEvent.click(screen.getByText('common.compliance.gdpr'))
await waitFor(() => expect(getDocDownloadUrl).toHaveBeenCalledWith('GDPR'))
await waitFor(() =>
expect(consoleClient.compliance.download.get).toHaveBeenCalledWith(
{ query: { doc_name: 'GDPR' } },
{ context: { silent: true } },
),
)
expect(onPricingUrlUpdate).not.toHaveBeenCalled()
})
@@ -150,7 +166,7 @@ describe('Compliance', () => {
it('should trigger download mutation successfully', async () => {
// Arrange
const mockUrl = 'http://example.com/doc.pdf'
vi.mocked(getDocDownloadUrl).mockResolvedValue({ url: mockUrl })
vi.mocked(consoleClient.compliance.download.get).mockResolvedValue({ url: mockUrl })
seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } })
// Act
@@ -160,7 +176,7 @@ describe('Compliance', () => {
// Assert
await waitFor(() => {
expect(getDocDownloadUrl).toHaveBeenCalled()
expect(consoleClient.compliance.download.get).toHaveBeenCalled()
expect(downloadUrl).toHaveBeenCalledWith({ url: mockUrl })
expect(toastSuccessSpy).toHaveBeenCalledWith('common.operation.downloadSuccess')
})
@@ -168,7 +184,9 @@ describe('Compliance', () => {
it('should handle download mutation error', async () => {
// Arrange
vi.mocked(getDocDownloadUrl).mockRejectedValue(new Error('Download failed'))
vi.mocked(consoleClient.compliance.download.get).mockRejectedValue(
new Error('Download failed'),
)
seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } })
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
@@ -179,7 +197,7 @@ describe('Compliance', () => {
// Assert
await waitFor(() => {
expect(getDocDownloadUrl).toHaveBeenCalled()
expect(consoleClient.compliance.download.get).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalledWith('common.operation.downloadFailed')
})
expect(consoleSpy).toHaveBeenCalled()
@@ -216,7 +234,7 @@ describe('Compliance', () => {
it('should show spinner and guard against duplicate download when isPending is true', async () => {
// Arrange
let resolveDownload: (value: { url: string }) => void
vi.mocked(getDocDownloadUrl).mockImplementation(
vi.mocked(consoleClient.compliance.download.get).mockImplementation(
() =>
new Promise((resolve) => {
resolveDownload = resolve
@@ -248,7 +266,7 @@ describe('Compliance', () => {
it('should not call downloadCompliance again while pending', async () => {
let resolveDownload: (value: { url: string }) => void
vi.mocked(getDocDownloadUrl).mockImplementation(
vi.mocked(consoleClient.compliance.download.get).mockImplementation(
() =>
new Promise((resolve) => {
resolveDownload = resolve
@@ -267,7 +285,7 @@ describe('Compliance', () => {
await waitFor(
() => {
expect(menuItem).toHaveAttribute('aria-disabled', 'true')
expect(getDocDownloadUrl).toHaveBeenCalledTimes(1)
expect(consoleClient.compliance.download.get).toHaveBeenCalledTimes(1)
},
{ timeout: 10000 },
)
@@ -283,7 +301,7 @@ describe('Compliance', () => {
{ timeout: 10000 },
)
// getDocDownloadUrl should still have only been called once
expect(getDocDownloadUrl).toHaveBeenCalledTimes(1)
expect(consoleClient.compliance.download.get).toHaveBeenCalledTimes(1)
}, 20000)
})
})
@@ -22,8 +22,7 @@ import {
settingsQueryParser,
} from '@/app/components/header/account-setting/query-params'
import { toast } from '@/app/notifications'
import { getDocDownloadUrl } from '@/service/common'
import { consoleQuery } from '@/service/console'
import { consoleClient, consoleQuery } from '@/service/console'
import { downloadUrl } from '@/utils/download'
import PremiumBadge from '../../base/premium-badge'
import { MenuItemContent } from './menu-item-content'
@@ -117,7 +116,10 @@ function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProp
mutationKey: ['downloadCompliance', docName],
mutationFn: async () => {
try {
const ret = await getDocDownloadUrl(docName)
const ret = await consoleClient.compliance.download.get(
{ query: { doc_name: docName } },
{ context: { silent: true } },
)
downloadUrl({ url: ret.url })
toast.success(t(($) => $['operation.downloadSuccess'], { ns: 'common' }))
} catch (error) {
@@ -3,8 +3,7 @@ import type { Member } from '@/models/common'
import { QueryClient } from '@tanstack/react-query'
import { screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { deleteMemberOrCancelInvitation } from '@/service/common'
import { consoleQuery } from '@/service/console'
import { consoleClient, consoleQuery } from '@/service/console'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import MemberMenu from '../member-menu'
@@ -21,8 +20,19 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
})
})
vi.mock('@/service/common', () => ({
deleteMemberOrCancelInvitation: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
workspaces: {
current: {
members: {
byMemberId: {
delete: vi.fn(),
},
},
},
},
},
}))
vi.mock('@/app/notifications', () => ({
toast: {
@@ -81,7 +91,10 @@ describe('MemberMenu', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUpdateRolesOfMember.mockResolvedValue(undefined)
vi.mocked(deleteMemberOrCancelInvitation).mockResolvedValue({ result: 'success' })
vi.mocked(consoleClient.workspaces.current.members.byMemberId.delete).mockResolvedValue({
result: 'success',
tenant_id: 'tenant-1',
})
vi.mocked(mockUpdateRolesOfMemberHook).mockReturnValue({
mutateAsync: mockUpdateRolesOfMember,
})
@@ -181,13 +194,13 @@ describe('MemberMenu', () => {
name: /workspaceMembers\.members\.removeFromTeamConfirmTitle:\{"memberName":"Member User"\}/i,
})
expect(dialog).toHaveTextContent('workspaceMembers.members.removeFromTeamConfirmDescription')
expect(deleteMemberOrCancelInvitation).not.toHaveBeenCalled()
expect(consoleClient.workspaces.current.members.byMemberId.delete).not.toHaveBeenCalled()
await user.click(within(dialog).getByRole('button', { name: /common\.operation\.confirm/i }))
await waitFor(() => {
expect(deleteMemberOrCancelInvitation).toHaveBeenCalledWith({
url: '/workspaces/current/members/member-1',
expect(consoleClient.workspaces.current.members.byMemberId.delete).toHaveBeenCalledWith({
params: { member_id: 'member-1' },
})
})
expect(queryClient.getQueryState(membersQueryKey)?.isInvalidated).toBe(true)
@@ -2,7 +2,7 @@ import type { ConsoleStateFixture } from '@/test/console/state-fixture'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vite-plus/test'
import { updateWorkspaceInfo } from '@/service/common'
import { consoleClient } from '@/service/console'
import { render } from '@/test/console/render'
import EditWorkspaceModal from '../index'
@@ -21,7 +21,16 @@ vi.mock('@/context/workspace-state', async () => {
return createWorkspaceStateModuleMock(() => mockConsoleState.current)
})
vi.mock('@/service/common')
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
workspaces: {
info: {
post: vi.fn(),
},
},
},
}))
vi.mock('@/app/notifications', () => ({
default: {
notify: (args: unknown) => toastMocks.mockNotify(args),
@@ -92,7 +101,7 @@ describe('EditWorkspaceModal', () => {
assign: mockAssign,
origin: 'http://localhost',
})
vi.mocked(updateWorkspaceInfo).mockResolvedValue({
vi.mocked(consoleClient.workspaces.info.post).mockResolvedValue({
result: 'success',
tenant: { id: 'workspace-id' },
})
@@ -105,8 +114,7 @@ describe('EditWorkspaceModal', () => {
await user.click(getSaveButton())
await waitFor(() => {
expect(updateWorkspaceInfo).toHaveBeenCalledWith({
url: '/workspaces/info',
expect(consoleClient.workspaces.info.post).toHaveBeenCalledWith({
body: { name: 'Renamed Workspace' },
})
expect(mockAssign).toHaveBeenCalledWith('http://localhost')
@@ -118,7 +126,7 @@ describe('EditWorkspaceModal', () => {
it('should expose the saving label and prevent duplicate form submission', async () => {
const user = userEvent.setup()
let rejectUpdate!: (reason: Error) => void
vi.mocked(updateWorkspaceInfo).mockImplementationOnce(
vi.mocked(consoleClient.workspaces.info.post).mockImplementationOnce(
() =>
new Promise((_, reject) => {
rejectUpdate = reject
@@ -134,7 +142,7 @@ describe('EditWorkspaceModal', () => {
const savingButton = screen.getByRole('button', { name: /operation\.saving/i })
expect(savingButton).toHaveAttribute('aria-disabled', 'true')
fireEvent.submit(savingButton.closest('form')!)
expect(updateWorkspaceInfo).toHaveBeenCalledOnce()
expect(consoleClient.workspaces.info.post).toHaveBeenCalledOnce()
rejectUpdate(new Error('update failed'))
await waitFor(() => {
@@ -145,7 +153,7 @@ describe('EditWorkspaceModal', () => {
it('should show error toast when update fails', async () => {
const user = userEvent.setup()
vi.mocked(updateWorkspaceInfo).mockRejectedValue(new Error('update failed'))
vi.mocked(consoleClient.workspaces.info.post).mockRejectedValue(new Error('update failed'))
renderModal()
@@ -193,7 +201,7 @@ describe('EditWorkspaceModal', () => {
fireEvent.submit(form!)
expect(updateWorkspaceInfo).not.toHaveBeenCalled()
expect(consoleClient.workspaces.info.post).not.toHaveBeenCalled()
expect(mockNotify).not.toHaveBeenCalled()
})
@@ -9,7 +9,7 @@ import { useId, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from '@/app/notifications'
import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state'
import { updateWorkspaceInfo } from '@/service/common'
import { consoleClient } from '@/service/console'
type IEditWorkspaceModalProps = {
onCancel: () => void
@@ -38,8 +38,7 @@ const EditWorkspaceModal = ({ onCancel }: IEditWorkspaceModalProps) => {
if (isSubmitting || isSaveUnavailable) return
setIsSubmitting(true)
try {
await updateWorkspaceInfo({
url: '/workspaces/info',
await consoleClient.workspaces.info.post({
body: {
name: normalizedName,
},
@@ -22,8 +22,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
import { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from '@/app/notifications'
import { deleteMemberOrCancelInvitation } from '@/service/common'
import { consoleQuery } from '@/service/console'
import { consoleClient, consoleQuery } from '@/service/console'
import { createSelectedRolePlaceholder } from '../workspace-role-list-query'
import AssignRolesModal from './assign-roles-modal'
@@ -95,7 +94,9 @@ const MemberMenu = ({
const handleRemove = useCallback(async () => {
setRemoving(true)
try {
await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` })
await consoleClient.workspaces.current.members.byMemberId.delete({
params: { member_id: member.id },
})
void queryClient.invalidateQueries({
queryKey: consoleQuery.workspaces.current.members.get.key(),
})
@@ -2,7 +2,7 @@ import type { ConsoleStateFixture } from '@/test/console/state-fixture'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vite-plus/test'
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
import { consoleClient } from '@/service/console'
import { createAccountProfileQueryWrapper } from '@/test/console/account-profile'
import { render } from '@/test/console/render'
import TransferOwnershipModal from '../index'
@@ -31,7 +31,28 @@ vi.mock('@/context/workspace-state', async () => {
return createWorkspaceStateModuleMock(() => mockConsoleState.current)
})
vi.mock('@/service/common')
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
workspaces: {
current: {
members: {
byMemberId: {
ownerTransfer: {
post: vi.fn(),
},
},
ownerTransferCheck: {
post: vi.fn(),
},
sendOwnerTransferConfirmEmail: {
post: vi.fn(),
},
},
},
},
},
}))
vi.mock('@/app/notifications', () => ({
default: {
notify: (args: unknown) => toastMocks.mockNotify(args),
@@ -101,15 +122,21 @@ describe('TransferOwnershipModal', () => {
isValid?: boolean
token?: string
} = {}) => {
vi.mocked(sendOwnerEmail).mockResolvedValue({
vi.mocked(
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
).mockResolvedValue({
data: 'step-token',
result: 'success',
} as unknown as Awaited<ReturnType<typeof sendOwnerEmail>>)
vi.mocked(verifyOwnerEmail).mockResolvedValue({
} as unknown as Awaited<
ReturnType<typeof consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post>
>)
vi.mocked(consoleClient.workspaces.current.members.ownerTransferCheck.post).mockResolvedValue({
is_valid: isValid,
token,
result: 'success',
} as unknown as Awaited<ReturnType<typeof verifyOwnerEmail>>)
} as unknown as Awaited<
ReturnType<typeof consoleClient.workspaces.current.members.ownerTransferCheck.post>
>)
}
const goToTransferStep = async (user: ReturnType<typeof userEvent.setup>) => {
@@ -129,9 +156,13 @@ describe('TransferOwnershipModal', () => {
it('should complete ownership transfer flow through all steps', async () => {
const user = userEvent.setup()
mockEmailVerification()
vi.mocked(ownershipTransfer).mockResolvedValue({
vi.mocked(
consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post,
).mockResolvedValue({
result: 'success',
} as unknown as Awaited<ReturnType<typeof ownershipTransfer>>)
} as unknown as Awaited<
ReturnType<typeof consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post>
>)
renderModal()
await goToTransferStep(user)
@@ -140,7 +171,12 @@ describe('TransferOwnershipModal', () => {
await waitFor(
() => {
expect(ownershipTransfer).toHaveBeenCalledWith('new-owner-id', { token: 'final-token' })
expect(
consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post,
).toHaveBeenCalledWith({
params: { member_id: 'new-owner-id' },
body: { token: 'final-token' },
})
expect(window.location.reload).toHaveBeenCalled()
},
{ timeout: 10000 },
@@ -149,10 +185,14 @@ describe('TransferOwnershipModal', () => {
it('should handle timer countdown and resend', async () => {
vi.useFakeTimers()
vi.mocked(sendOwnerEmail).mockResolvedValue({
vi.mocked(
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
).mockResolvedValue({
data: 'token',
result: 'success',
} as unknown as Awaited<ReturnType<typeof sendOwnerEmail>>)
} as unknown as Awaited<
ReturnType<typeof consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post>
>)
renderModal()
// Trigger the email send (which starts the timer)
@@ -181,7 +221,9 @@ describe('TransferOwnershipModal', () => {
await act(async () => {
fireEvent.click(resendBtn)
})
expect(sendOwnerEmail).toHaveBeenCalledTimes(2)
expect(
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
).toHaveBeenCalledTimes(2)
vi.useRealTimers()
})
@@ -205,7 +247,9 @@ describe('TransferOwnershipModal', () => {
it('should show error when verifying email throws an error', async () => {
const user = userEvent.setup()
mockEmailVerification()
vi.mocked(verifyOwnerEmail).mockRejectedValue(new Error('verification crash'))
vi.mocked(consoleClient.workspaces.current.members.ownerTransferCheck.post).mockRejectedValue(
new Error('verification crash'),
)
renderModal()
await goToTransferStep(user)
@@ -222,7 +266,9 @@ describe('TransferOwnershipModal', () => {
it('should not show a modal-level toast and should stay on start step when sending verification email fails', async () => {
const user = userEvent.setup()
vi.mocked(sendOwnerEmail).mockRejectedValue(new Error('network error'))
vi.mocked(
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
).mockRejectedValue(new Error('network error'))
renderModal()
await user.click(
screen.getByRole('button', { name: /members\.transferModal\.sendVerifyCode/i }),
@@ -231,7 +277,9 @@ describe('TransferOwnershipModal', () => {
// The base service layer surfaces the real backend error. The modal itself
// must NOT show an additional toast (e.g. "Error sending verification code: undefined").
await waitFor(() => {
expect(sendOwnerEmail).toHaveBeenCalled()
expect(
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
).toHaveBeenCalled()
})
expect(mockNotify).not.toHaveBeenCalled()
// Should remain on the start step instead of advancing to the verify step.
@@ -243,7 +291,9 @@ describe('TransferOwnershipModal', () => {
it('should show error when ownership transfer fails', async () => {
const user = userEvent.setup()
mockEmailVerification()
vi.mocked(ownershipTransfer).mockRejectedValue(new Error('transfer failed'))
vi.mocked(
consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post,
).mockRejectedValue(new Error('transfer failed'))
renderModal()
await goToTransferStep(user)
await selectNewOwnerAndSubmit(user)
@@ -260,10 +310,14 @@ describe('TransferOwnershipModal', () => {
it('should handle sendOwnerEmail returning null data', async () => {
const user = userEvent.setup()
vi.mocked(sendOwnerEmail).mockResolvedValue({
vi.mocked(
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
).mockResolvedValue({
data: null,
result: 'success',
} as unknown as Awaited<ReturnType<typeof sendOwnerEmail>>)
} as unknown as Awaited<
ReturnType<typeof consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post>
>)
renderModal()
await user.click(
@@ -278,7 +332,9 @@ describe('TransferOwnershipModal', () => {
it('should swallow null rejection from sendOwnerEmail without showing a modal-level toast', async () => {
const user = userEvent.setup()
vi.mocked(sendOwnerEmail).mockRejectedValue(null)
vi.mocked(
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
).mockRejectedValue(null)
renderModal()
await user.click(
@@ -286,7 +342,9 @@ describe('TransferOwnershipModal', () => {
)
await waitFor(() => {
expect(sendOwnerEmail).toHaveBeenCalled()
expect(
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
).toHaveBeenCalled()
})
expect(mockNotify).not.toHaveBeenCalled()
expect(
@@ -297,7 +355,9 @@ describe('TransferOwnershipModal', () => {
it('should show fallback error prefix when verifyOwnerEmail throws null', async () => {
const user = userEvent.setup()
mockEmailVerification()
vi.mocked(verifyOwnerEmail).mockRejectedValue(null)
vi.mocked(consoleClient.workspaces.current.members.ownerTransferCheck.post).mockRejectedValue(
null,
)
renderModal()
await goToTransferStep(user)
@@ -315,7 +375,9 @@ describe('TransferOwnershipModal', () => {
it('should show fallback error prefix when ownershipTransfer throws null', async () => {
const user = userEvent.setup()
mockEmailVerification()
vi.mocked(ownershipTransfer).mockRejectedValue(null)
vi.mocked(
consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post,
).mockRejectedValue(null)
renderModal()
await goToTransferStep(user)
@@ -9,7 +9,7 @@ import { Trans, useTranslation } from 'react-i18next'
import { toast } from '@/app/notifications'
import { currentWorkspaceAtom } from '@/context/workspace-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
import { consoleClient } from '@/service/console'
import { getErrorMessage } from '@/utils/error-message'
import MemberSelector from './member-selector'
@@ -62,15 +62,16 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
)
}
const sendEmail = async () => {
const res = await sendOwnerEmail({})
const res = await consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post({
body: {},
})
startCount()
if (res.data) setStepToken(res.data)
}
const verifyEmailAddress = async (code: string, token: string, callback?: () => void) => {
try {
const res = await verifyOwnerEmail({
code,
token,
const res = await consoleClient.workspaces.current.members.ownerTransferCheck.post({
body: { code, token },
})
if (res.is_valid) {
setStepToken(res.token)
@@ -97,8 +98,9 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
const handleTransfer = async () => {
setIsTransfer(true)
try {
await ownershipTransfer(newOwner, {
token: stepToken,
await consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post({
params: { member_id: newOwner },
body: { token: stepToken },
})
globalThis.location.reload()
} catch (error) {
@@ -9,7 +9,6 @@ import type {
} from '../declarations'
import { act, renderHook } from '@testing-library/react'
import { useLocale } from '#i18n'
import { fetchDefaultModal } from '@/service/common'
import { consoleQuery } from '@/service/console'
import {
ConfigurationMethodEnum,
@@ -49,10 +48,6 @@ vi.mock('@tanstack/react-query', () => ({
})),
}))
vi.mock('@/service/common', () => ({
fetchDefaultModal: vi.fn(),
}))
vi.mock('@/service/use-common', () => ({
commonQueryKeys: {
modelProviders: ['model-providers'],
@@ -166,6 +161,9 @@ describe('hooks', () => {
provider: {
provider: 'openai',
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
label: { en_US: 'Provider', zh_Hans: 'Provider' },
supported_model_types: [],
tenant_id: 'tenant-1',
},
model,
model_type: ModelTypeEnum.textGeneration,
@@ -193,6 +191,9 @@ describe('hooks', () => {
provider: {
provider: 'anthropic',
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
label: { en_US: 'Provider', zh_Hans: 'Provider' },
supported_model_types: [],
tenant_id: 'tenant-1',
},
model: 'claude-3',
model_type: ModelTypeEnum.textGeneration,
@@ -290,15 +291,6 @@ describe('hooks', () => {
expect(result.current.data).toEqual(mockDefaultModel)
expect(result.current.isLoading).toBe(false)
// Coverage for queryFn
const queryCall = (useQuery as Mock).mock.calls.find(
(call) => Array.isArray(call[0].queryKey) && call[0].queryKey[0] === 'default-model',
)
if (queryCall) {
await queryCall[0].queryFn()
expect(fetchDefaultModal).toHaveBeenCalled()
}
})
it('should return undefined when data is not available', () => {
@@ -327,7 +319,9 @@ describe('hooks', () => {
expect(useQuery).toHaveBeenCalledWith(
expect.objectContaining({
enabled: false,
queryKey: ['default-model', ModelTypeEnum.textEmbedding],
queryKey: consoleQuery.workspaces.current.defaultModel.get.queryKey({
input: { query: { model_type: ModelTypeEnum.textEmbedding } },
}),
}),
)
expect(result.current.isLoading).toBe(false)
@@ -686,7 +680,9 @@ describe('hooks', () => {
})
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: ['default-model', ModelTypeEnum.textGeneration],
queryKey: consoleQuery.workspaces.current.defaultModel.get.queryKey({
input: { query: { model_type: ModelTypeEnum.textGeneration } },
}),
})
})
@@ -1,3 +1,4 @@
import type { GetWorkspacesCurrentDefaultModelResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { Field } from '@/app/components/workflow/nodes/llm/types'
export type FormValue = Record<string, any>
@@ -264,14 +265,7 @@ export type ModelProvider = {
allow_custom_token?: boolean
}
export type DefaultModelResponse = {
model: string
model_type: ModelTypeEnum
provider: {
provider: string
icon_small: TypeWithI18N
}
}
export type DefaultModelResponse = NonNullable<GetWorkspacesCurrentDefaultModelResponse['data']>
export type DefaultModel = {
provider: string
@@ -292,7 +286,7 @@ export type ModelParameterRule = {
max?: number
name: string
precision?: number
required: false
required?: boolean
type: string
use_template?: string
options?: string[]
@@ -23,7 +23,6 @@ import {
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { useModalContextSelector } from '@/context/modal-context'
import { getModelLanguage } from '@/i18n/metadata'
import { fetchDefaultModal } from '@/service/common'
import { consoleQuery } from '@/service/console'
import { commonQueryKeys, modelProviderDetailsQueryOptions } from '@/service/use-common'
import { useExpandModelProviderList } from './atoms'
@@ -95,14 +94,16 @@ export const useDefaultModel = (
type: ModelTypeEnum,
{ enabled = true }: ModelQueryOptions = {},
) => {
const { data, refetch, isPending } = useQuery({
queryKey: commonQueryKeys.defaultModel(type),
queryFn: () => fetchDefaultModal(`/workspaces/current/default-model?model_type=${type}`),
enabled,
})
const { data, refetch, isPending } = useQuery(
consoleQuery.workspaces.current.defaultModel.get.queryOptions({
input: { query: { model_type: type } },
enabled,
}),
)
return {
data: data?.data,
// An unset default model comes back as null.
data: data?.data ?? undefined,
mutate: refetch,
isLoading: enabled && isPending,
}
@@ -208,7 +209,11 @@ export const useInvalidateDefaultModel = () => {
return useCallback(
(type: ModelTypeEnum) => {
queryClient.invalidateQueries({ queryKey: commonQueryKeys.defaultModel(type) })
queryClient.invalidateQueries({
queryKey: consoleQuery.workspaces.current.defaultModel.get.queryKey({
input: { query: { model_type: type } },
}),
})
},
[queryClient],
)
@@ -263,7 +263,13 @@ describe('ModelSelector', () => {
defaultModel={{
model: 'missing-model',
model_type: ModelTypeEnum.textGeneration,
provider: { provider, icon_small: { en_US: '', zh_Hans: '' } },
provider: {
provider,
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: provider, zh_Hans: provider },
supported_model_types: [],
tenant_id: 'tenant-1',
},
}}
/>,
)
@@ -1,7 +1,7 @@
import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen'
import type { ModelItem, ModelProvider } from '../../declarations'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { disableModel, enableModel } from '@/service/common'
import { consoleClient } from '@/service/console'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
import { render } from '@/test/console/render'
import { ModelStatusEnum } from '../../declarations'
@@ -28,9 +28,26 @@ vi.mock('@/context/permission-state', async () => {
}))
})
vi.mock('@/service/common', () => ({
enableModel: vi.fn(),
disableModel: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
workspaces: {
current: {
modelProviders: {
byProvider: {
models: {
enable: {
patch: vi.fn(),
},
disable: {
patch: vi.fn(),
},
},
},
},
},
},
},
}))
vi.mock('../../hooks', () => ({
@@ -119,7 +136,9 @@ describe('ModelListItem', () => {
await waitFor(
() => {
expect(disableModel).toHaveBeenCalled()
expect(
consoleClient.workspaces.current.modelProviders.byProvider.models.disable.patch,
).toHaveBeenCalled()
expect(onChange).toHaveBeenCalledWith('test-provider')
},
{ timeout: 2000 },
@@ -142,7 +161,9 @@ describe('ModelListItem', () => {
await waitFor(
() => {
expect(enableModel).toHaveBeenCalled()
expect(
consoleClient.workspaces.current.modelProviders.byProvider.models.enable.patch,
).toHaveBeenCalled()
expect(onChange).toHaveBeenCalledWith('test-provider')
},
{ timeout: 2000 },
@@ -292,8 +313,12 @@ describe('ModelListItem', () => {
expect(statusSwitch).toHaveAttribute('aria-disabled', 'true')
fireEvent.click(statusSwitch)
expect(statusSwitch).toHaveAttribute('aria-checked', 'false')
expect(enableModel).not.toHaveBeenCalled()
expect(disableModel).not.toHaveBeenCalled()
expect(
consoleClient.workspaces.current.modelProviders.byProvider.models.enable.patch,
).not.toHaveBeenCalled()
expect(
consoleClient.workspaces.current.modelProviders.byProvider.models.disable.patch,
).not.toHaveBeenCalled()
})
// isConfigurable=true: hover class on row
@@ -11,8 +11,7 @@ import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { deploymentEditionAtom } from '@/features/system-features/state'
import { disableModel, enableModel } from '@/service/common'
import { consoleQuery } from '@/service/console'
import { consoleClient, consoleQuery } from '@/service/console'
import { hasPermission } from '@/utils/permission'
import { ModelStatusEnum } from '../declarations'
import { useUpdateModelList } from '../hooks'
@@ -67,15 +66,15 @@ const ModelListItem = ({
const toggleModelEnablingStatus = useCallback(
async (enabled: boolean) => {
if (enabled)
await enableModel(
`/workspaces/current/model-providers/${provider.provider}/models/enable`,
{ model: model.model, model_type: model.model_type },
)
await consoleClient.workspaces.current.modelProviders.byProvider.models.enable.patch({
params: { provider: provider.provider },
body: { model: model.model, model_type: model.model_type },
})
else
await disableModel(
`/workspaces/current/model-providers/${provider.provider}/models/disable`,
{ model: model.model, model_type: model.model_type },
)
await consoleClient.workspaces.current.modelProviders.byProvider.models.disable.patch({
params: { provider: provider.provider },
body: { model: model.model, model_type: model.model_type },
})
queryClient.invalidateQueries({
queryKey: modelProviderModelListQueryKey,
@@ -135,6 +135,9 @@ const mockModel: DefaultModelResponse = {
provider: {
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'Provider', zh_Hans: 'Provider' },
supported_model_types: [],
tenant_id: 'tenant-1',
},
}
@@ -4,12 +4,14 @@ import type {
ModelStatus,
ModelType,
ModelWithProviderEntityResponse,
ParameterRule,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { StaticImageData } from 'next/image'
import type {
CredentialFormSchemaSelect,
CredentialFormSchemaTextInput,
ModelItem,
ModelParameterRule,
TypeWithI18N,
} from './declarations'
import { ModelProviderQuotaGetPaid } from '@/types/model-provider'
@@ -197,3 +199,24 @@ const normalizeModelProviderModel = (model: ModelWithProviderEntityResponse): Mo
export const normalizeModelProviderModelsResponse = (response: {
data: ModelWithProviderEntityResponse[]
}): ModelItem[] => response.data.map(normalizeModelProviderModel)
const isParameterDefault = (value: unknown): value is ModelParameterRule['default'] =>
typeof value === 'number' ||
typeof value === 'string' ||
typeof value === 'boolean' ||
(Array.isArray(value) && value.every((item) => typeof item === 'string'))
export const normalizeModelParameterRule = (rule: ParameterRule): ModelParameterRule => ({
name: rule.name,
type: rule.type,
label: normalizeModelLabel(rule.label),
help: rule.help ? normalizeModelLabel(rule.help) : undefined,
// `default` is untyped JSON in the console API response.
default: isParameterDefault(rule.default) ? rule.default : undefined,
min: rule.min ?? undefined,
max: rule.max ?? undefined,
precision: rule.precision ?? undefined,
required: rule.required,
use_template: rule.use_template ?? undefined,
options: rule.options,
})
@@ -1,7 +1,7 @@
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import useDocumentTitle from '@/hooks/use-document-title'
import { changePasswordWithToken } from '@/service/common'
import { consoleClient } from '@/service/console'
import { useVerifyForgotPasswordToken } from '@/service/use-common'
import ChangePasswordForm from './ChangePasswordForm'
@@ -15,8 +15,15 @@ vi.mock('@/service/use-common', () => ({
useVerifyForgotPasswordToken: vi.fn(),
}))
vi.mock('@/service/common', () => ({
changePasswordWithToken: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
forgotPassword: {
resets: {
post: vi.fn(),
},
},
},
}))
vi.mock('@/hooks/use-document-title', () => ({
@@ -28,7 +35,7 @@ vi.mock('@/utils/var', () => ({ basePath: '' }))
type UseVerifyResult = ReturnType<typeof useVerifyForgotPasswordToken>
const mockUseVerify = vi.mocked(useVerifyForgotPasswordToken)
const mockChangePassword = vi.mocked(changePasswordWithToken)
const mockChangePassword = vi.mocked(consoleClient.forgotPassword.resets.post)
const mockUseDocumentTitle = vi.mocked(useDocumentTitle)
const VALID_PASSWORD = 'ValidPass123!'
@@ -79,7 +86,6 @@ describe('ChangePasswordForm', () => {
await waitFor(() => {
expect(mockChangePassword).toHaveBeenCalledWith({
url: '/forgot-password/resets',
body: {
token: T2,
new_password: VALID_PASSWORD,
@@ -165,7 +171,7 @@ describe('ChangePasswordForm', () => {
it('prevents duplicate submissions while the password is changing', async () => {
const user = userEvent.setup()
let resolveRequest:
| ((value: Awaited<ReturnType<typeof changePasswordWithToken>>) => void)
| ((value: Awaited<ReturnType<typeof consoleClient.forgotPassword.resets.post>>) => void)
| undefined
mockChangePassword.mockReturnValue(
new Promise((resolve) => {
@@ -17,7 +17,7 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
import { validPassword } from '@/config'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSearchParams } from '@/next/navigation'
import { changePasswordWithToken } from '@/service/common'
import { consoleClient } from '@/service/console'
import { useVerifyForgotPasswordToken } from '@/service/use-common'
import { basePath } from '@/utils/var'
@@ -56,8 +56,7 @@ const ChangePasswordForm = () => {
setIsSubmitting(true)
try {
await changePasswordWithToken({
url: '/forgot-password/resets',
await consoleClient.forgotPassword.resets.post({
body: {
token: resetToken,
new_password: formValues.password,
@@ -1,18 +1,23 @@
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
import type { InitValidateStatusResponse } from '@/models/common'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import useDocumentTitle from '@/hooks/use-document-title'
import {
fetchInitValidateStatus,
fetchSetupStatus,
sendForgotPasswordEmail,
} from '@/service/common'
import { consoleClient } from '@/service/console'
import ForgotPasswordForm from './ForgotPasswordForm'
vi.mock('@/service/common', () => ({
fetchSetupStatus: vi.fn(),
fetchInitValidateStatus: vi.fn(),
sendForgotPasswordEmail: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
setup: {
get: vi.fn(),
},
init: {
get: vi.fn(),
},
forgotPassword: {
post: vi.fn(),
},
},
}))
vi.mock('@/hooks/use-document-title', () => ({
@@ -20,13 +25,13 @@ vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
}))
const mockFetchSetupStatus = vi.mocked(fetchSetupStatus)
const mockFetchInitValidateStatus = vi.mocked(fetchInitValidateStatus)
const mockSendForgotPasswordEmail = vi.mocked(sendForgotPasswordEmail)
const mockFetchSetupStatus = vi.mocked(consoleClient.setup.get)
const mockFetchInitValidateStatus = vi.mocked(consoleClient.init.get)
const mockSendForgotPasswordEmail = vi.mocked(consoleClient.forgotPassword.post)
const mockUseDocumentTitle = vi.mocked(useDocumentTitle)
const prepareLoadedState = () => {
mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' } as SetupStatusResponse)
mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' })
mockFetchInitValidateStatus.mockResolvedValue({
status: 'finished',
} as InitValidateStatusResponse)
@@ -90,7 +95,6 @@ describe('ForgotPasswordForm', () => {
await waitFor(() => {
expect(mockSendForgotPasswordEmail).toHaveBeenCalledWith({
url: '/forgot-password',
body: { email: 'test@example.com' },
})
})
@@ -115,7 +119,6 @@ describe('ForgotPasswordForm', () => {
await waitFor(() => {
expect(mockSendForgotPasswordEmail).toHaveBeenCalledWith({
url: '/forgot-password',
body: { email: 'test@example.com' },
})
})
+4 -12
View File
@@ -1,5 +1,4 @@
'use client'
import type { InitValidateStatusResponse } from '@/models/common'
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field'
@@ -11,11 +10,7 @@ import * as z from 'zod'
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
import useDocumentTitle from '@/hooks/use-document-title'
import Link from '@/next/link'
import {
fetchInitValidateStatus,
fetchSetupStatus,
sendForgotPasswordEmail,
} from '@/service/common'
import { consoleClient } from '@/service/console'
import { basePath } from '@/utils/var'
type ForgotPasswordFormValues = {
@@ -43,10 +38,7 @@ const ForgotPasswordForm = () => {
setIsSubmitting(true)
try {
const res = await sendForgotPasswordEmail({
url: '/forgot-password',
body: { email },
})
const res = await consoleClient.forgotPassword.post({ body: { email } })
if (res.result === 'success') setIsEmailSent(true)
else console.error('Email verification failed')
} catch (error) {
@@ -57,8 +49,8 @@ const ForgotPasswordForm = () => {
}
useEffect(() => {
fetchSetupStatus().then(() => {
fetchInitValidateStatus().then((res: InitValidateStatusResponse) => {
consoleClient.setup.get().then(() => {
consoleClient.init.get().then((res) => {
if (res.status === 'not_started') window.location.href = `${basePath}/init`
})
+3 -4
View File
@@ -1,5 +1,4 @@
'use client'
import type { InitValidateStatusResponse } from '@/models/common'
import { Button } from '@langgenius/dify-ui/button'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -7,7 +6,7 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
import { toast } from '@/app/notifications'
import useDocumentTitle from '@/hooks/use-document-title'
import { useRouter } from '@/next/navigation'
import { fetchInitValidateStatus, initValidate } from '@/service/common'
import { consoleClient } from '@/service/console'
import { basePath } from '@/utils/var'
const InitPasswordPopup = () => {
@@ -23,7 +22,7 @@ const InitPasswordPopup = () => {
const handleValidation = async () => {
setLoading(true)
try {
const response = await initValidate({ body: { password } })
const response = await consoleClient.init.post({ body: { password } })
if (response.result === 'success') {
setValidated(true)
router.push('/install') // or render setup form
@@ -37,7 +36,7 @@ const InitPasswordPopup = () => {
}
useEffect(() => {
fetchInitValidateStatus().then((res: InitValidateStatusResponse) => {
consoleClient.init.get().then((res) => {
if (res.status === 'finished') window.location.href = `${basePath}/install`
else setLoading(false)
})
+22 -14
View File
@@ -1,8 +1,8 @@
import type { ReactElement } from 'react'
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
import type { InitValidateStatusResponse } from '@/models/common'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
import { consoleClient } from '@/service/console'
import { expectLoadingButton } from '@/test/button'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import { encryptPassword } from '@/utils/encryption'
@@ -21,20 +21,29 @@ vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
}))
vi.mock('@/service/common', () => ({
fetchSetupStatus: vi.fn(),
fetchInitValidateStatus: vi.fn(),
setup: vi.fn(),
login: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
setup: {
get: vi.fn(),
post: vi.fn(),
},
init: {
get: vi.fn(),
},
login: {
post: vi.fn(),
},
},
}))
const mockFetchSetupStatus = vi.mocked(fetchSetupStatus)
const mockFetchInitValidateStatus = vi.mocked(fetchInitValidateStatus)
const mockSetup = vi.mocked(setup)
const mockLogin = vi.mocked(login)
const mockFetchSetupStatus = vi.mocked(consoleClient.setup.get)
const mockFetchInitValidateStatus = vi.mocked(consoleClient.init.get)
const mockSetup = vi.mocked(consoleClient.setup.post)
const mockLogin = vi.mocked(consoleClient.login.post)
const prepareLoadedState = () => {
mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' } as SetupStatusResponse)
mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' })
mockFetchInitValidateStatus.mockResolvedValue({
status: 'finished',
} as InitValidateStatusResponse)
@@ -161,7 +170,6 @@ describe('InstallForm', () => {
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith({
url: '/login',
body: {
email: 'admin@example.com',
password: encryptPassword('Password123'),
@@ -232,7 +240,7 @@ describe('InstallForm', () => {
})
it('should redirect to sign in when setup is finished', async () => {
mockFetchSetupStatus.mockResolvedValue({ step: 'finished' } as SetupStatusResponse)
mockFetchSetupStatus.mockResolvedValue({ step: 'finished' })
render(<InstallForm />)
+5 -8
View File
@@ -1,5 +1,4 @@
'use client'
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
import { zPostSetupBody } from '@dify/contracts/api/console/setup/zod.gen'
import { Button } from '@langgenius/dify-ui/button'
import {
@@ -24,8 +23,7 @@ import { LICENSE_LINK } from '@/constants/link'
import useDocumentTitle from '@/hooks/use-document-title'
import Link from '@/next/link'
import { useRouter } from '@/next/navigation'
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
import { consoleQuery } from '@/service/console'
import { consoleClient, consoleQuery } from '@/service/console'
import { encryptPassword as encodePassword } from '@/utils/encryption'
const accountFormSchema = zPostSetupBody.pick({ email: true, name: true, password: true }).extend({
@@ -52,7 +50,7 @@ const InstallForm = () => {
setIsSubmitting(true)
try {
// First, setup the admin account
await setup({
await consoleClient.setup.post({
body: {
...value,
language: i18n.language,
@@ -60,8 +58,7 @@ const InstallForm = () => {
})
// Then, automatically login with the same credentials
const loginRes = await login({
url: '/login',
const loginRes = await consoleClient.login.post({
body: {
email: value.email,
password: encodePassword(value.password),
@@ -82,11 +79,11 @@ const InstallForm = () => {
}
useEffect(() => {
fetchSetupStatus().then((res: SetupStatusResponse) => {
consoleClient.setup.get().then((res) => {
if (res.step === 'finished') {
push('/signin')
} else {
fetchInitValidateStatus().then((res: InitValidateStatusResponse) => {
consoleClient.init.get().then((res) => {
if (res.status === 'not_started') push('/init')
})
}
+17 -7
View File
@@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useLocale } from '#i18n'
import { useRouter, useSearchParams } from '@/next/navigation'
import { sendResetPasswordCode } from '@/service/common'
import { consoleClient } from '@/service/console'
import ResetPasswordPage from '../page'
const mockPush = vi.fn()
@@ -27,8 +27,13 @@ vi.mock('@/next/navigation', () => ({
useSearchParams: vi.fn(),
}))
vi.mock('@/service/common', () => ({
sendResetPasswordCode: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
forgotPassword: {
post: vi.fn(),
},
},
}))
describe('ResetPasswordPage', () => {
@@ -41,7 +46,10 @@ describe('ResetPasswordPage', () => {
vi.mocked(useSearchParams).mockReturnValue(
new URLSearchParams() as unknown as ReturnType<typeof useSearchParams>,
)
vi.mocked(sendResetPasswordCode).mockResolvedValue({ result: 'success', data: 'reset-token' })
vi.mocked(consoleClient.forgotPassword.post).mockResolvedValue({
result: 'success',
data: 'reset-token',
})
})
it('labels the email field and submits it with Enter', async () => {
@@ -54,7 +62,9 @@ describe('ResetPasswordPage', () => {
await user.type(emailInput, 'user@example.com{Enter}')
await waitFor(() => {
expect(sendResetPasswordCode).toHaveBeenCalledWith('user@example.com', 'en-US')
expect(consoleClient.forgotPassword.post).toHaveBeenCalledWith({
body: { email: 'user@example.com', language: 'en-US' },
})
})
expect(mockPush).toHaveBeenCalledWith(
'/reset-password/check-code?token=reset-token&email=user%2540example.com',
@@ -73,7 +83,7 @@ describe('ResetPasswordPage', () => {
expect(emailInput).toHaveAttribute('aria-invalid', 'true')
expect(emailInput).toHaveAccessibleDescription('login.error.emailInValid')
expect(emailInput).toHaveFocus()
expect(sendResetPasswordCode).not.toHaveBeenCalled()
expect(consoleClient.forgotPassword.post).not.toHaveBeenCalled()
await user.clear(emailInput)
await user.click(screen.getByRole('button', { name: 'login.sendVerificationCode' }))
@@ -84,6 +94,6 @@ describe('ResetPasswordPage', () => {
expect(emailInput).toHaveAttribute('aria-invalid', 'true')
expect(emailInput).toHaveAccessibleDescription('login.error.emailEmpty')
expect(emailInput).toHaveFocus()
expect(sendResetPasswordCode).not.toHaveBeenCalled()
expect(consoleClient.forgotPassword.post).not.toHaveBeenCalled()
})
})
@@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useLocale } from '#i18n'
import { useRouter, useSearchParams } from '@/next/navigation'
import { verifyResetPasswordCode } from '@/service/common'
import { consoleClient } from '@/service/console'
import CheckCodePage from '../page'
const mockPush = vi.fn()
@@ -25,9 +25,16 @@ vi.mock('@/next/navigation', () => ({
useSearchParams: vi.fn(),
}))
vi.mock('@/service/common', () => ({
sendResetPasswordCode: vi.fn(),
verifyResetPasswordCode: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
forgotPassword: {
post: vi.fn(),
validity: {
post: vi.fn(),
},
},
},
}))
describe('ResetPasswordCheckCodePage', () => {
@@ -43,8 +50,8 @@ describe('ResetPasswordCheckCodePage', () => {
token: 'email-token',
}) as unknown as ReturnType<typeof useSearchParams>,
)
vi.mocked(verifyResetPasswordCode).mockResolvedValue({
result: 'success',
vi.mocked(consoleClient.forgotPassword.validity.post).mockResolvedValue({
email: 'user@example.com',
is_valid: true,
token: 'verified-token',
})
@@ -63,10 +70,12 @@ describe('ResetPasswordCheckCodePage', () => {
await user.type(codeInput, '123456{Enter}')
await waitFor(() => {
expect(verifyResetPasswordCode).toHaveBeenCalledWith({
code: '123456',
email: 'user@example.com',
token: 'email-token',
expect(consoleClient.forgotPassword.validity.post).toHaveBeenCalledWith({
body: {
code: '123456',
email: 'user@example.com',
token: 'email-token',
},
})
})
expect(mockPush).toHaveBeenCalledWith(
@@ -87,6 +96,6 @@ describe('ResetPasswordCheckCodePage', () => {
expect(codeInput).toHaveAttribute('aria-invalid', 'true')
expect(codeInput).toHaveAccessibleDescription(error.textContent ?? '')
expect(codeInput).toHaveFocus()
expect(verifyResetPasswordCode).not.toHaveBeenCalled()
expect(consoleClient.forgotPassword.validity.post).not.toHaveBeenCalled()
})
})
+4 -4
View File
@@ -10,7 +10,7 @@ import Countdown from '@/app/components/signin/countdown'
import { toast } from '@/app/notifications'
import useDocumentTitle from '@/hooks/use-document-title'
import { useRouter, useSearchParams } from '@/next/navigation'
import { sendResetPasswordCode, verifyResetPasswordCode } from '@/service/common'
import { consoleClient } from '@/service/console'
type CheckCodeFormValues = {
code: string
@@ -31,7 +31,7 @@ export default function CheckCode() {
if (loading) return
try {
setLoading(true)
const ret = await verifyResetPasswordCode({ email, code, token })
const ret = await consoleClient.forgotPassword.validity.post({ body: { email, code, token } })
if (ret.is_valid) {
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(ret.token))
@@ -46,10 +46,10 @@ export default function CheckCode() {
const resendCode = async () => {
try {
const res = await sendResetPasswordCode(email, locale)
const res = await consoleClient.forgotPassword.post({ body: { email, language: locale } })
if (res.result === 'success') {
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(res.data))
params.set('token', encodeURIComponent(res.data ?? ''))
router.replace(`/reset-password/check-code?${params.toString()}`)
}
} catch (error) {
+3 -3
View File
@@ -11,7 +11,7 @@ import { emailRegex } from '@/config'
import useDocumentTitle from '@/hooks/use-document-title'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { sendResetPasswordCode } from '@/service/common'
import { consoleClient } from '@/service/console'
import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '../components/signin/storage'
export default function CheckCode() {
@@ -28,11 +28,11 @@ export default function CheckCode() {
if (loading) return
try {
setLoading(true)
const res = await sendResetPasswordCode(email, locale)
const res = await consoleClient.forgotPassword.post({ body: { email, language: locale } })
if (res.result === 'success') {
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(res.data))
params.set('token', encodeURIComponent(res.data ?? ''))
params.set('email', encodeURIComponent(email))
router.push(`/reset-password/check-code?${params.toString()}`)
} else {
@@ -2,7 +2,7 @@ import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import useDocumentTitle from '@/hooks/use-document-title'
import { useRouter, useSearchParams } from '@/next/navigation'
import { changePasswordWithToken } from '@/service/common'
import { consoleClient } from '@/service/console'
import ChangePasswordForm from '../page'
const countdownState = vi.hoisted(() => ({
@@ -29,8 +29,15 @@ vi.mock('@/next/link', () => ({
),
}))
vi.mock('@/service/common', () => ({
changePasswordWithToken: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
forgotPassword: {
resets: {
post: vi.fn(),
},
},
},
}))
vi.mock('@/hooks/use-document-title', () => ({
@@ -40,7 +47,7 @@ vi.mock('@/hooks/use-document-title', () => ({
const mockReplace = vi.fn()
const mockUseRouter = vi.mocked(useRouter)
const mockUseSearchParams = vi.mocked(useSearchParams)
const mockChangePasswordWithToken = vi.mocked(changePasswordWithToken)
const mockChangePasswordWithToken = vi.mocked(consoleClient.forgotPassword.resets.post)
const mockUseDocumentTitle = vi.mocked(useDocumentTitle)
const redirectUrl = '/apps?template-id=template-1&utm_source=dify_blog'
@@ -108,7 +115,6 @@ describe('Reset Password Set Password Page', () => {
await waitFor(() => {
expect(mockChangePasswordWithToken).toHaveBeenCalledWith({
url: '/forgot-password/resets',
body: {
token: 'reset-token',
new_password: 'ValidPass123!',
+2 -3
View File
@@ -19,7 +19,7 @@ import { validPassword } from '@/config'
import useDocumentTitle from '@/hooks/use-document-title'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { changePasswordWithToken } from '@/service/common'
import { consoleClient } from '@/service/console'
type PasswordFormValues = {
password: string
@@ -75,8 +75,7 @@ const ChangePasswordForm = () => {
if (isSubmitting) return
setIsSubmitting(true)
try {
await changePasswordWithToken({
url: '/forgot-password/resets',
await consoleClient.forgotPassword.resets.post({
body: {
token,
new_password: formValues.password,
@@ -4,7 +4,7 @@ import { noop, QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
import { consoleClient } from '@/service/console'
import { createHttpError } from '@/service/http-error'
import { seedSystemFeatures } from '@/test/console/query-data'
import CheckCode from '../page'
@@ -76,9 +76,16 @@ vi.mock('@/next/navigation', () => ({
vi.mock('@/service/base', () => serviceBaseMocks)
vi.mock('@/service/common', () => ({
emailLoginWithCode: vi.fn(),
sendEMailLoginCode: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
emailCodeLogin: {
validity: {
post: vi.fn(),
},
post: vi.fn(),
},
},
}))
vi.mock('@/utils/encryption', () => ({
@@ -139,8 +146,10 @@ const accountProfile: GetAccountProfileResponse = {
describe('CheckCode', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(emailLoginWithCode).mockReset().mockResolvedValue({ result: 'success' })
vi.mocked(sendEMailLoginCode).mockReset()
vi.mocked(consoleClient.emailCodeLogin.validity.post)
.mockReset()
.mockResolvedValue({ result: 'success' })
vi.mocked(consoleClient.emailCodeLogin.post).mockReset()
turnstileWidgets.clear()
navigationMocks.searchParams = new URLSearchParams({
email: 'user@example.com',
@@ -225,18 +234,13 @@ describe('CheckCode', () => {
expect(codeInput).toHaveAttribute('aria-invalid', 'true')
expect(codeInput).toHaveAccessibleDescription('login.checkCode.invalidCode')
expect(codeInput).toHaveFocus()
expect(emailLoginWithCode).not.toHaveBeenCalled()
expect(consoleClient.emailCodeLogin.validity.post).not.toHaveBeenCalled()
})
it('keeps Community verification independent of Turnstile', async () => {
const user = userEvent.setup()
const queryClient = createQueryClient()
vi.mocked(emailLoginWithCode).mockResolvedValue({
code: 'invalid_code',
data: '',
message: 'Invalid code',
result: 'fail',
})
vi.mocked(consoleClient.emailCodeLogin.validity.post).mockResolvedValue({ result: 'fail' })
render(
<QueryClientProvider client={queryClient}>
<CheckCode />
@@ -246,12 +250,14 @@ describe('CheckCode', () => {
await user.type(screen.getByLabelText('login.checkCode.verificationCode'), '123456')
await user.click(screen.getByRole('button', { name: 'login.checkCode.verify' }))
expect(emailLoginWithCode).toHaveBeenCalledWith({
code: '123456',
email: 'user@example.com',
language: expect.any(String),
timezone: 'Asia/Singapore',
token: 'email-login-token',
expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenCalledWith({
body: {
code: '123456',
email: 'user@example.com',
language: expect.any(String),
timezone: 'Asia/Singapore',
token: 'email-login-token',
},
})
expect(turnstileMocks.render).not.toHaveBeenCalled()
})
@@ -259,8 +265,11 @@ describe('CheckCode', () => {
it('does not resend while verification is in progress', async () => {
const user = userEvent.setup()
const queryClient = createQueryClient()
const verificationRequest = createDeferred<Awaited<ReturnType<typeof emailLoginWithCode>>>()
vi.mocked(emailLoginWithCode).mockReturnValue(verificationRequest.promise)
const verificationRequest =
createDeferred<Awaited<ReturnType<typeof consoleClient.emailCodeLogin.validity.post>>>()
vi.mocked(consoleClient.emailCodeLogin.validity.post).mockReturnValue(
verificationRequest.promise,
)
render(
<QueryClientProvider client={queryClient}>
<CheckCode />
@@ -270,22 +279,17 @@ describe('CheckCode', () => {
await user.type(screen.getByLabelText('login.checkCode.verificationCode'), '123456')
await user.click(screen.getByRole('button', { name: 'login.checkCode.verify' }))
await waitFor(() => {
expect(emailLoginWithCode).toHaveBeenCalledOnce()
expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenCalledOnce()
})
const resendButton = screen.getByRole('button', { name: 'resend-code' })
expect(resendButton).toBeDisabled()
resendButton.removeAttribute('disabled')
fireEvent.click(resendButton)
expect(sendEMailLoginCode).not.toHaveBeenCalled()
expect(consoleClient.emailCodeLogin.post).not.toHaveBeenCalled()
act(() => {
verificationRequest.resolve({
code: 'invalid_code',
data: '',
message: 'Invalid code',
result: 'fail',
})
verificationRequest.resolve({ result: 'fail' })
})
await waitFor(() => {
expect(screen.getByRole('button', { name: 'login.checkCode.verify' })).toBeEnabled()
@@ -295,8 +299,9 @@ describe('CheckCode', () => {
it('does not verify while resend is in progress', async () => {
const user = userEvent.setup()
const queryClient = createQueryClient()
const resendRequest = createDeferred<Awaited<ReturnType<typeof sendEMailLoginCode>>>()
vi.mocked(sendEMailLoginCode).mockReturnValue(resendRequest.promise)
const resendRequest =
createDeferred<Awaited<ReturnType<typeof consoleClient.emailCodeLogin.post>>>()
vi.mocked(consoleClient.emailCodeLogin.post).mockReturnValue(resendRequest.promise)
render(
<QueryClientProvider client={queryClient}>
<CheckCode />
@@ -306,7 +311,7 @@ describe('CheckCode', () => {
await user.type(screen.getByLabelText('login.checkCode.verificationCode'), '123456')
await user.click(screen.getByRole('button', { name: 'resend-code' }))
await waitFor(() => {
expect(sendEMailLoginCode).toHaveBeenCalledOnce()
expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledOnce()
})
const verifyButton = screen.getByRole('button', { name: 'login.checkCode.verify' })
@@ -314,7 +319,7 @@ describe('CheckCode', () => {
const form = verifyButton.closest('form')
if (!form) throw new Error('Verification form is missing')
fireEvent.submit(form)
expect(emailLoginWithCode).not.toHaveBeenCalled()
expect(consoleClient.emailCodeLogin.validity.post).not.toHaveBeenCalled()
act(() => {
resendRequest.resolve({ data: '', result: 'fail' })
@@ -330,14 +335,9 @@ describe('CheckCode', () => {
turnstileMocks.siteKey = 'cloud-site-key'
const queryClient = createQueryClient()
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.mocked(emailLoginWithCode)
vi.mocked(consoleClient.emailCodeLogin.validity.post)
.mockRejectedValueOnce(new Error('invalid verification code'))
.mockResolvedValueOnce({
code: 'invalid_code',
data: '',
message: 'Invalid code',
result: 'fail',
})
.mockResolvedValueOnce({ result: 'fail' })
render(
<QueryClientProvider client={queryClient}>
@@ -365,11 +365,11 @@ describe('CheckCode', () => {
await user.click(verifyButton)
await waitFor(() => {
expect(emailLoginWithCode).toHaveBeenCalledWith(
expect.objectContaining({
expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenCalledWith({
body: expect.objectContaining({
turnstile_token: 'signin_code_verify-token-1',
}),
)
})
})
expect(codeInput).toHaveValue('123456')
await waitFor(() => {
@@ -387,11 +387,11 @@ describe('CheckCode', () => {
await user.click(verifyButton)
await waitFor(() => {
expect(emailLoginWithCode).toHaveBeenLastCalledWith(
expect.objectContaining({
expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenLastCalledWith({
body: expect.objectContaining({
turnstile_token: 'signin_code_verify-token-2',
}),
)
})
})
expect(codeInput).toHaveValue('123456')
})
@@ -401,7 +401,10 @@ describe('CheckCode', () => {
turnstileMocks.deploymentEdition = 'CLOUD'
turnstileMocks.siteKey = 'cloud-site-key'
const queryClient = createQueryClient()
vi.mocked(sendEMailLoginCode).mockResolvedValue({ result: 'success', data: 'new-login-token' })
vi.mocked(consoleClient.emailCodeLogin.post).mockResolvedValue({
result: 'success',
data: 'new-login-token',
})
render(
<QueryClientProvider client={queryClient}>
@@ -434,11 +437,13 @@ describe('CheckCode', () => {
await user.click(await screen.findByRole('button', { name: 'verify-turnstile-signin_code' }))
await waitFor(() => {
expect(sendEMailLoginCode).toHaveBeenCalledWith(
'user@example.com',
expect.any(String),
'signin_code-token-2',
)
expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledWith({
body: {
email: 'user@example.com',
language: expect.any(String),
turnstile_token: 'signin_code-token-2',
},
})
})
await waitFor(() => {
expect(
@@ -456,7 +461,10 @@ describe('CheckCode', () => {
turnstileMocks.deploymentEdition = 'CLOUD'
turnstileMocks.siteKey = 'cloud-site-key'
const queryClient = createQueryClient()
vi.mocked(sendEMailLoginCode).mockResolvedValue({ result: 'success', data: 'new-login-token' })
vi.mocked(consoleClient.emailCodeLogin.post).mockResolvedValue({
result: 'success',
data: 'new-login-token',
})
Object.defineProperty(window, 'turnstile', {
configurable: true,
value: undefined,
@@ -491,11 +499,13 @@ describe('CheckCode', () => {
await user.click(await screen.findByRole('button', { name: 'verify-turnstile-signin_code' }))
await waitFor(() => {
expect(sendEMailLoginCode).toHaveBeenCalledWith(
'user@example.com',
expect.any(String),
'signin_code-token-1',
)
expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledWith({
body: {
email: 'user@example.com',
language: expect.any(String),
turnstile_token: 'signin_code-token-1',
},
})
})
})
@@ -503,7 +513,10 @@ describe('CheckCode', () => {
const user = userEvent.setup()
const queryClient = createQueryClient()
turnstileMocks.siteKey = 'site-key-not-used-outside-cloud'
vi.mocked(sendEMailLoginCode).mockResolvedValue({ result: 'success', data: 'new-login-token' })
vi.mocked(consoleClient.emailCodeLogin.post).mockResolvedValue({
result: 'success',
data: 'new-login-token',
})
render(
<QueryClientProvider client={queryClient}>
@@ -516,11 +529,9 @@ describe('CheckCode', () => {
expect(resendButton).toBeEnabled()
await user.click(resendButton)
expect(sendEMailLoginCode).toHaveBeenCalledWith(
'user@example.com',
expect.any(String),
undefined,
)
expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledWith({
body: { email: 'user@example.com', language: expect.any(String) },
})
})
describe('Post-login profile bootstrap', () => {
+17 -16
View File
@@ -10,13 +10,12 @@ import { useLocale } from '#i18n'
import { trackEvent } from '@/app/components/base/amplitude'
import Countdown from '@/app/components/signin/countdown'
import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage'
import { toast } from '@/app/notifications'
import { TURNSTILE_SITE_KEY } from '@/config'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import useDocumentTitle from '@/hooks/use-document-title'
import { useRouter, useSearchParams } from '@/next/navigation'
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
import { consoleClient } from '@/service/console'
import { encryptVerificationCode } from '@/utils/encryption'
import { replaceLoginRedirect } from '@/utils/login-redirect.client'
import { getBrowserTimezone } from '@/utils/timezone'
@@ -62,13 +61,15 @@ export default function CheckCode() {
setLoading(true)
shouldResetTurnstile = isTurnstileRequired
const ret = await emailLoginWithCode({
email,
code: encryptVerificationCode(code),
token,
language,
timezone: getBrowserTimezone(),
...(isTurnstileRequired ? { turnstile_token: verifyTurnstileToken } : {}),
const ret = await consoleClient.emailCodeLogin.validity.post({
body: {
email,
code: encryptVerificationCode(code),
token,
language,
timezone: getBrowserTimezone(),
...(isTurnstileRequired ? { turnstile_token: verifyTurnstileToken } : {}),
},
})
if (ret.result === 'success') {
// Track login success event
@@ -85,8 +86,6 @@ export default function CheckCode() {
await queryClient.query(profileQueryOptions)
replaceLoginRedirect(resolvePostLoginRedirect(searchParams), router.replace, basePath)
}
} else {
toast.error(ret.data)
}
} catch (error) {
console.error(error)
@@ -106,11 +105,13 @@ export default function CheckCode() {
const resendCode = async (turnstileToken?: string) => {
setIsResending(true)
try {
const ret = await sendEMailLoginCode(
email,
locale,
isTurnstileRequired ? turnstileToken : undefined,
)
const ret = await consoleClient.emailCodeLogin.post({
body: {
email,
language: locale,
...(isTurnstileRequired ? { turnstile_token: turnstileToken } : {}),
},
})
if (ret.result === 'success') {
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
setCountdownGeneration((value) => value + 1)
@@ -69,8 +69,13 @@ vi.mock('@/config', async () => {
}
})
vi.mock('@/service/common', () => ({
sendEMailLoginCode: (...args: unknown[]) => mocks.sendEMailLoginCode(...args),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
emailCodeLogin: {
post: (...args: unknown[]) => mocks.sendEMailLoginCode(...args),
},
},
}))
describe('MailAndCodeAuth', () => {
@@ -149,11 +154,9 @@ describe('MailAndCodeAuth', () => {
await user.click(screen.getByRole('button', { name: 'login.signup.verifyMail' }))
await waitFor(() => {
expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith(
'user@example.com',
'en-US',
'turnstile-token',
)
expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith({
body: { email: 'user@example.com', language: 'en-US', turnstile_token: 'turnstile-token' },
})
})
expect(mocks.push).toHaveBeenCalledWith(expect.stringContaining('/signin/check-code?'))
})
@@ -177,11 +180,13 @@ describe('MailAndCodeAuth', () => {
await user.click(continueButton)
await waitFor(() => {
expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith(
'user@example.com',
'en-US',
'consumed-turnstile-token',
)
expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith({
body: {
email: 'user@example.com',
language: 'en-US',
turnstile_token: 'consumed-turnstile-token',
},
})
})
await waitFor(() => {
expect(continueButton).toBeDisabled()
@@ -195,11 +200,13 @@ describe('MailAndCodeAuth', () => {
await user.click(continueButton)
await waitFor(() => {
expect(mocks.sendEMailLoginCode).toHaveBeenLastCalledWith(
'user@example.com',
'en-US',
'fresh-turnstile-token',
)
expect(mocks.sendEMailLoginCode).toHaveBeenLastCalledWith({
body: {
email: 'user@example.com',
language: 'en-US',
turnstile_token: 'fresh-turnstile-token',
},
})
})
expect(mocks.push).toHaveBeenCalledWith(expect.stringContaining('/signin/check-code?'))
})
@@ -10,7 +10,7 @@ import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/si
import { emailRegex, TURNSTILE_SITE_KEY } from '@/config'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useRouter, useSearchParams } from '@/next/navigation'
import { sendEMailLoginCode } from '@/service/common'
import { consoleClient } from '@/service/console'
import Turnstile from './turnstile'
type MailAndCodeAuthProps = {
@@ -39,11 +39,13 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
try {
setLoading(true)
shouldResetTurnstile = isTurnstileRequired
const ret = await sendEMailLoginCode(
email,
locale,
isTurnstileRequired ? turnstileToken : undefined,
)
const ret = await consoleClient.emailCodeLogin.post({
body: {
email,
language: locale,
...(isTurnstileRequired ? { turnstile_token: turnstileToken } : {}),
},
})
if (ret.result === 'success') {
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
const params = new URLSearchParams(searchParams)
@@ -13,9 +13,7 @@ import { toast } from '@/app/notifications'
import { emailRegex } from '@/config'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { login } from '@/service/common'
import { consoleQuery } from '@/service/console'
import { setWebAppAccessToken } from '@/service/webapp-auth'
import { consoleClient, consoleQuery } from '@/service/console'
import { encryptPassword } from '@/utils/encryption'
import { replaceLoginRedirect } from '@/utils/login-redirect.client'
import { basePath } from '@/utils/var'
@@ -52,15 +50,8 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup }: MailAndP
}
if (isInvite)
loginData.invite_token = decodeURIComponent(searchParams.get('invite_token') as string)
const res = await login({
url: '/login',
body: loginData,
})
const res = await consoleClient.login.post({ body: loginData })
if (res.result === 'success') {
if (res?.data?.access_token) {
// Track login success event
setWebAppAccessToken(res.data.access_token)
}
trackEvent('user_login_success', {
method: 'email_password',
is_invite: isInvite,
@@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import { useLocale } from '#i18n'
import { useRouter, useSearchParams } from '@/next/navigation'
import { activateMember } from '@/service/common'
import { consoleClient } from '@/service/console'
import { useInvitationCheck } from '@/service/use-common'
import { getBrowserTimezone } from '@/utils/timezone'
import InviteSettingsPage from '../page'
@@ -62,8 +62,13 @@ vi.mock('@/next/navigation', () => ({
useSearchParams: vi.fn(),
}))
vi.mock('@/service/common', () => ({
activateMember: vi.fn(),
vi.mock('@/service/console', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/console')>()),
consoleClient: {
activate: {
post: vi.fn(),
},
},
}))
vi.mock('@/service/use-common', () => ({
@@ -84,7 +89,7 @@ const mockRefetch = vi.fn()
const mockUseLocale = useLocale as unknown as MockedFunction<typeof useLocale>
const mockUseRouter = useRouter as unknown as MockedFunction<typeof useRouter>
const mockUseSearchParams = useSearchParams as unknown as MockedFunction<typeof useSearchParams>
const mockActivateMember = activateMember as unknown as MockedFunction<typeof activateMember>
const mockActivateMember = vi.mocked(consoleClient.activate.post)
const mockUseQuery = vi.mocked(useQuery)
const mockUseInvitationCheck = useInvitationCheck as unknown as MockedFunction<
typeof useInvitationCheck
@@ -182,7 +187,6 @@ describe('InviteSettingsPage', () => {
await waitFor(() => {
expect(mockActivateMember).toHaveBeenCalledWith({
url: '/activate',
body: {
token: 'invite-token',
name: 'Invitee',
@@ -204,7 +208,6 @@ describe('InviteSettingsPage', () => {
await waitFor(() => {
expect(mockActivateMember).toHaveBeenCalledWith({
url: '/activate',
body: {
token: 'invite-token',
name: 'Invitee',
@@ -238,7 +241,6 @@ describe('InviteSettingsPage', () => {
await waitFor(() => {
expect(mockActivateMember).toHaveBeenCalledWith({
url: '/activate',
body: {
token: 'invite-token',
},
@@ -268,7 +270,6 @@ describe('InviteSettingsPage', () => {
await waitFor(() => {
expect(mockActivateMember).toHaveBeenCalledWith({
url: '/activate',
body: {
token: 'invite-token',
},
@@ -299,7 +300,6 @@ describe('InviteSettingsPage', () => {
await waitFor(() => {
expect(mockActivateMember).toHaveBeenCalledWith({
url: '/activate',
body: {
token: 'invite-token',
name: 'Invitee',
+2 -6
View File
@@ -28,8 +28,7 @@ import { setLocaleOnClient } from '@/i18n/client'
import { languages } from '@/i18n/language'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { activateMember } from '@/service/common'
import { consoleQuery } from '@/service/console'
import { consoleClient, consoleQuery } from '@/service/console'
import { isUnauthorizedError } from '@/service/http-error'
import { useInvitationCheck } from '@/service/use-common'
import { replaceLoginRedirect } from '@/utils/login-redirect.client'
@@ -156,10 +155,7 @@ export default function InviteSettingsPage() {
: {
token,
}
const res = await activateMember({
url: '/activate',
body,
})
const res = await consoleClient.activate.post({ body })
if (res.result === 'success') {
// Tokens are now stored in cookies by the backend
if (requiresAccountSetup) await setLocaleOnClient(language!, false)
+2 -8
View File
@@ -10,7 +10,7 @@ import { userProfileQueryOptions } from '@/features/account-profile/client'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { invitationCheck } from '@/service/common'
import { consoleClient } from '@/service/console'
import { isUnauthorizedError } from '@/service/http-error'
import { replaceLoginRedirect } from '@/utils/login-redirect.client'
import { basePath } from '@/utils/var'
@@ -58,13 +58,7 @@ function NormalForm() {
isError: isInviteCheckError,
} = useQuery({
queryKey: ['signin', 'invite-check', inviteToken],
queryFn: () =>
invitationCheck({
url: '/activate/check',
params: {
token: inviteToken,
},
}),
queryFn: () => consoleClient.activate.check.get({ query: { token: inviteToken } }),
enabled: isInviteLink,
retry: false,
refetchOnWindowFocus: false,
-58
View File
@@ -8,7 +8,6 @@ import {
REGISTRATION_SUCCESS_STORAGE_KEY,
} from '@/app/components/base/amplitude/registration-session-state'
import { clearPageLeaveGuards, registerPageLeaveGuard } from '@/utils/page-leave-guard'
import { emailLoginWithCode, sendEMailLoginCode } from './common'
import { useLogout } from './use-common'
const mocks = vi.hoisted(() => ({
@@ -34,63 +33,6 @@ vi.mock('@/utils/var', async (importOriginal) => ({
},
}))
describe('sendEMailLoginCode', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('includes the Turnstile token when provided', async () => {
await sendEMailLoginCode('user@example.com', 'en-US', 'turnstile-token')
expect(mocks.post).toHaveBeenCalledWith('/email-code-login', {
body: {
email: 'user@example.com',
language: 'en-US',
turnstile_token: 'turnstile-token',
},
})
})
it('omits the Turnstile token when it is not provided', async () => {
await sendEMailLoginCode('user@example.com', 'en-US')
expect(mocks.post).toHaveBeenCalledWith('/email-code-login', {
body: {
email: 'user@example.com',
language: 'en-US',
},
})
})
})
describe('emailLoginWithCode', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('includes the verification-specific Turnstile token when provided', async () => {
await emailLoginWithCode({
code: 'encrypted-code',
email: 'user@example.com',
language: 'en-US',
timezone: 'Asia/Singapore',
token: 'email-login-token',
turnstile_token: 'verify-turnstile-token',
})
expect(mocks.post).toHaveBeenCalledWith('/email-code-login/validity', {
body: {
code: 'encrypted-code',
email: 'user@example.com',
language: 'en-US',
timezone: 'Asia/Singapore',
token: 'email-login-token',
turnstile_token: 'verify-turnstile-token',
},
})
})
})
describe('useLogout', () => {
const wrapper = ({ children }: { children: ReactNode }) =>
createElement(QueryClientProvider, { client: new QueryClient() }, children)
+2 -223
View File
@@ -1,19 +1,5 @@
import type { EmailCodeLoginPayload } from '@dify/contracts/api/console/email-code-login/types.gen'
import type {
PostWorkspacesInfoData,
PostWorkspacesInfoResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type {
DefaultModelResponse,
ModelParameterRule,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type {
CommonResponse,
InitValidateStatusResponse,
SetupStatusResponse,
} from '@/models/common'
import { del, get, patch, post } from './base'
import type { CommonResponse } from '@/models/common'
import { post } from './base'
type LoginSuccess = {
result: 'success'
@@ -26,15 +12,6 @@ type LoginFail = {
message: string
}
type LoginResponse = LoginSuccess | LoginFail
export const login = ({
url,
body,
}: {
url: string
body: Record<string, any>
}): Promise<LoginResponse> => {
return post<LoginResponse>(url, { body })
}
export const webAppLogin = ({
url,
body,
@@ -45,133 +22,6 @@ export const webAppLogin = ({
return post<LoginResponse>(url, { body }, { isPublicAPI: true })
}
export const setup = ({ body }: { body: Record<string, any> }): Promise<CommonResponse> => {
return post<CommonResponse>('/setup', { body })
}
export const initValidate = ({ body }: { body: Record<string, any> }): Promise<CommonResponse> => {
return post<CommonResponse>('/init', { body })
}
export const fetchInitValidateStatus = (): Promise<InitValidateStatusResponse> => {
return get<InitValidateStatusResponse>('/init')
}
export const fetchSetupStatus = (): Promise<SetupStatusResponse> => {
return get<SetupStatusResponse>('/setup')
}
export const deleteMemberOrCancelInvitation = ({
url,
}: {
url: string
}): Promise<CommonResponse> => {
return del<CommonResponse>(url)
}
export const sendOwnerEmail = (body: {
language?: string
}): Promise<CommonResponse & { data: string }> =>
post<CommonResponse & { data: string }>(
'/workspaces/current/members/send-owner-transfer-confirm-email',
{ body },
)
export const verifyOwnerEmail = (body: {
code: string
token: string
}): Promise<CommonResponse & { is_valid: boolean; email: string; token: string }> =>
post<CommonResponse & { is_valid: boolean; email: string; token: string }>(
'/workspaces/current/members/owner-transfer-check',
{ body },
)
export const ownershipTransfer = (
memberID: string,
body: { token: string },
): Promise<CommonResponse & { is_valid: boolean; email: string; token: string }> =>
post<CommonResponse & { is_valid: boolean; email: string; token: string }>(
`/workspaces/current/members/${memberID}/owner-transfer`,
{ body },
)
export const updateWorkspaceInfo = ({
url,
body,
}: {
url: PostWorkspacesInfoData['url']
body: PostWorkspacesInfoData['body']
}): Promise<PostWorkspacesInfoResponse> => {
return post<PostWorkspacesInfoResponse>(url, { body })
}
type InvitationCheckData = {
workspace_name: string
email: string
workspace_id: string
account_status?: string
requires_setup?: boolean
}
type ActivateMemberBody = {
token: string
name?: string
interface_language?: string
timezone?: string
}
export const invitationCheck = ({
url,
params,
}: {
url: string
params: { workspace_id?: string; email?: string; token: string }
}): Promise<CommonResponse & { is_valid: boolean; data: InvitationCheckData }> => {
return get<CommonResponse & { is_valid: boolean; data: InvitationCheckData }>(url, { params })
}
export const activateMember = ({
url,
body,
}: {
url: string
body: ActivateMemberBody
}): Promise<LoginResponse> => {
return post<LoginResponse>(url, { body })
}
export const fetchDefaultModal = (url: string): Promise<{ data: DefaultModelResponse }> => {
return get<{ data: DefaultModelResponse }>(url)
}
export const fetchModelParameterRules = (url: string): Promise<{ data: ModelParameterRule[] }> => {
return get<{ data: ModelParameterRule[] }>(url)
}
export const enableModel = (
url: string,
body: { model: string; model_type: ModelTypeEnum },
): Promise<CommonResponse> => patch<CommonResponse>(url, { body })
export const disableModel = (
url: string,
body: { model: string; model_type: ModelTypeEnum },
): Promise<CommonResponse> => patch<CommonResponse>(url, { body })
export const sendForgotPasswordEmail = ({
url,
body,
}: {
url: string
body: { email: string }
}): Promise<CommonResponse & { data: string }> =>
post<CommonResponse & { data: string }>(url, { body })
export const changePasswordWithToken = ({
url,
body,
}: {
url: string
body: { token: string; new_password: string; password_confirm: string }
}): Promise<CommonResponse> => post<CommonResponse>(url, { body })
export const changeWebAppPasswordWithToken = ({
url,
body,
@@ -193,37 +43,6 @@ export const uploadRemoteFileInfo = (
)
}
export const sendEMailLoginCode = (
email: string,
language = 'en-US',
turnstileToken?: string,
): Promise<CommonResponse & { data: string }> =>
post<CommonResponse & { data: string }>('/email-code-login', {
body: {
email,
language,
...(turnstileToken === undefined ? {} : { turnstile_token: turnstileToken }),
},
})
export const emailLoginWithCode = (data: EmailCodeLoginPayload): Promise<LoginResponse> =>
post<LoginResponse>('/email-code-login/validity', { body: data })
export const sendResetPasswordCode = (
email: string,
language = 'en-US',
): Promise<CommonResponse & { data: string; message?: string; code?: string }> =>
post<CommonResponse & { data: string; message?: string; code?: string }>('/forgot-password', {
body: { email, language },
})
export const verifyResetPasswordCode = (body: {
email: string
code: string
token: string
}): Promise<CommonResponse & { is_valid: boolean; token: string }> =>
post<CommonResponse & { is_valid: boolean; token: string }>('/forgot-password/validity', { body })
export const sendWebAppEMailLoginCode = (
email: string,
language = 'en-US',
@@ -262,46 +81,6 @@ export const verifyWebAppResetPasswordCode = (body: {
{ isPublicAPI: true },
)
export const sendDeleteAccountCode = (): Promise<CommonResponse & { data: string }> =>
get<CommonResponse & { data: string }>('/account/delete/verify')
export const verifyDeleteAccountCode = (body: {
code: string
token: string
}): Promise<CommonResponse & { is_valid: boolean }> =>
post<CommonResponse & { is_valid: boolean }>('/account/delete', { body })
export const submitDeleteAccountFeedback = (body: {
feedback: string
email: string
}): Promise<CommonResponse> => post<CommonResponse>('/account/delete/feedback', { body })
export const getDocDownloadUrl = (doc_name: string): Promise<{ url: string }> =>
get<{ url: string }>('/compliance/download', { params: { doc_name } }, { silent: true })
export const sendVerifyCode = (body: {
email: string
phase: string
token?: string
}): Promise<CommonResponse & { data: string }> =>
post<CommonResponse & { data: string }>('/account/change-email', { body })
export const verifyEmail = (body: {
email: string
code: string
token: string
}): Promise<CommonResponse & { is_valid: boolean; email: string; token: string }> =>
post<CommonResponse & { is_valid: boolean; email: string; token: string }>(
'/account/change-email/validity',
{ body },
)
export const resetEmail = (body: { new_email: string; token: string }): Promise<CommonResponse> =>
post<CommonResponse>('/account/change-email/reset', { body })
export const checkEmailExisted = (body: { email: string }): Promise<CommonResponse> =>
post<CommonResponse>('/account/change-email/check-email-unique', { body }, { silent: true })
export const getAvatar = async ({
avatar,
}: {
+8 -4
View File
@@ -2,6 +2,7 @@ import type {
FormValue,
ModelParameterRule,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { normalizeModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/utils'
export const mergeValidCompletionParams = (
oldParams: FormValue | undefined,
@@ -82,10 +83,13 @@ export const fetchModelParameterRulesForModel = async (
provider: string,
modelId: string,
): Promise<ModelParameterRule[]> => {
const { fetchModelParameterRules } = await import('@/service/common')
const url = `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}`
const { data: parameterRules } = await fetchModelParameterRules(url)
return parameterRules ?? []
const { consoleClient } = await import('@/service/console')
const { data: parameterRules } =
await consoleClient.workspaces.current.modelProviders.byProvider.models.parameterRules.get({
params: { provider },
query: { model: modelId },
})
return (parameterRules ?? []).map(normalizeModelParameterRule)
}
export const fetchAndMergeValidCompletionParams = async (