From 3ce6232dabdba761a4e3782fa7cc3a41b3c460b4 Mon Sep 17 00:00:00 2001 From: Jiann Date: Tue, 14 Jul 2026 15:22:11 +0800 Subject: [PATCH] fix(plugin-auth): restore authenticator edit values (#10091) --- .../__tests__/AuthenticatorsPage.test.tsx | 129 +++++++++++++++++ .../client-v2/pages/AuthenticatorsPage.tsx | 130 +++++++++++++----- .../plugin-auth/src/locale/en-US.json | 3 +- .../plugin-auth/src/locale/zh-CN.json | 3 +- 4 files changed, 225 insertions(+), 40 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-auth/src/client-v2/__tests__/AuthenticatorsPage.test.tsx diff --git a/packages/plugins/@nocobase/plugin-auth/src/client-v2/__tests__/AuthenticatorsPage.test.tsx b/packages/plugins/@nocobase/plugin-auth/src/client-v2/__tests__/AuthenticatorsPage.test.tsx new file mode 100644 index 00000000000..f4db271b969 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-auth/src/client-v2/__tests__/AuthenticatorsPage.test.tsx @@ -0,0 +1,129 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { render, screen, waitFor } from '@testing-library/react'; +import { Form, Input } from 'antd'; +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +const holder = vi.hoisted(() => ({ + resource: { + create: vi.fn(), + get: vi.fn(), + update: vi.fn(), + }, + ctx: { + api: { + resource: () => holder.resource, + }, + }, +})); + +vi.mock('@nocobase/flow-engine', () => { + return { + randomId: () => 's_test', + useFlowContext: () => holder.ctx, + }; +}); + +vi.mock('@nocobase/client-v2', () => { + return { + DEFAULT_PAGE_SIZE: 20, + DrawerFormLayout: ({ children }: { children?: React.ReactNode }) => <>{children}, + Table: () => null, + }; +}); + +vi.mock('../locale', () => ({ + useT: () => (key: string) => key, + useAuthTranslation: () => ({ t: (key: string) => key }), +})); + +import type PluginAuthClientV2 from '../plugin'; +import { AuthenticatorFormView } from '../pages/AuthenticatorsPage'; + +function AdminSettingsBody() { + return ( + + + + ); +} + +describe('AuthenticatorFormView', () => { + it('loads the complete record and shows nested option values after the type-specific form loads', async () => { + holder.resource.get.mockResolvedValue({ + data: { + data: { + id: 1, + name: 'oidc', + authType: 'oidc', + options: { clientId: 'saved-client-id' }, + }, + }, + }); + const plugin = { + authTypes: { + get: () => ({ + adminSettingsFormLoader: () => Promise.resolve({ default: AdminSettingsBody }), + }), + }, + } as unknown as PluginAuthClientV2; + + render( + , + ); + + await waitFor(() => { + expect(holder.resource.get).toHaveBeenCalledWith({ filterByTk: 1 }); + expect(screen.getByLabelText('Client ID')).toHaveValue('saved-client-id'); + }); + }); + + it('shows the request error and keeps the list values as a fallback', async () => { + holder.resource.get.mockRejectedValue(new Error('Failed to load authenticator')); + const plugin = { + authTypes: { + get: () => ({ + adminSettingsFormLoader: () => Promise.resolve({ default: AdminSettingsBody }), + }), + }, + } as unknown as PluginAuthClientV2; + + render( + , + ); + + expect(await screen.findByRole('alert')).toHaveTextContent('Failed to load authenticator'); + expect(screen.getByLabelText('Client ID')).toHaveValue('fallback-client-id'); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-auth/src/client-v2/pages/AuthenticatorsPage.tsx b/packages/plugins/@nocobase/plugin-auth/src/client-v2/pages/AuthenticatorsPage.tsx index 86776f30d80..3766994d98d 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/client-v2/pages/AuthenticatorsPage.tsx +++ b/packages/plugins/@nocobase/plugin-auth/src/client-v2/pages/AuthenticatorsPage.tsx @@ -11,12 +11,13 @@ import { CheckOutlined, DeleteOutlined, DownOutlined, PlusOutlined } from '@ant- import { DEFAULT_PAGE_SIZE, DrawerFormLayout, Table } from '@nocobase/client-v2'; import { randomId, useFlowContext } from '@nocobase/flow-engine'; import { useMemoizedFn, useRequest } from 'ahooks'; -import { App, Button, Card, Checkbox, Dropdown, Form, Input, Select, Space, Spin, Tag, theme } from 'antd'; +import { Alert, App, Button, Card, Checkbox, Dropdown, Form, Input, Select, Space, Spin, Tag, theme } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { cloneDeep } from 'lodash'; import React, { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import { useAuthTranslation, useT } from '../locale'; -import PluginAuthClientV2, { type AuthOptions } from '../plugin'; +import type PluginAuthClientV2 from '../plugin'; +import type { AuthOptions } from '../plugin'; type AuthenticatorRecord = { id: number | string; @@ -31,6 +32,12 @@ type AuthenticatorRecord = { type AuthTypeOption = { name: string; title?: string }; +const AUTHENTICATOR_LIST_FIELDS = ['id', 'name', 'authType', 'title', 'description', 'enabled']; + +type AuthenticatorDetailResource = { + get: (options: { filterByTk: AuthenticatorRecord['id'] }) => Promise<{ data?: unknown }>; +}; + function recursiveTrim(value: any): any { if (typeof value === 'string') return value.trim(); if (Array.isArray(value)) return value.map(recursiveTrim); @@ -59,7 +66,25 @@ function useAuthTypesFromServer() { ); } -function AuthenticatorFormView(props: { +export async function loadAuthenticatorForEdit( + resource: AuthenticatorDetailResource, + record: AuthenticatorRecord, +): Promise { + const response = await resource.get({ filterByTk: record.id }); + const body = response?.data; + const detail = body && typeof body === 'object' && 'data' in body ? body.data : body; + if (!detail || typeof detail !== 'object' || Array.isArray(detail)) { + return cloneDeep(record); + } + const normalizedDetail = cloneDeep(detail as AuthenticatorRecord); + return { + ...cloneDeep(record), + ...normalizedDetail, + options: normalizedDetail.options ?? cloneDeep(record.options), + }; +} + +export function AuthenticatorFormView(props: { mode: 'create' | 'edit'; authType: string; authTypeOptions: AuthTypeOption[]; @@ -80,16 +105,33 @@ function AuthenticatorFormView(props: { const resource = useAuthenticatorsResource(); const [form] = Form.useForm(); const [submitting, setSubmitting] = useState(false); + const [loadError, setLoadError] = useState(null); + const shouldLoadDetail = props.mode === 'edit' && props.record?.id != null; + + const { data: detailRecord, loading: loadingDetail } = useRequest( + async () => { + if (!props.record) return undefined; + return loadAuthenticatorForEdit(resource, props.record); + }, + { + ready: shouldLoadDetail, + refreshDeps: [props.record?.id], + onBefore: () => setLoadError(null), + onError: (error) => setLoadError(error instanceof Error ? error : new Error(String(error))), + }, + ); + + const editRecord = detailRecord ?? props.record; const initialValues = useMemo(() => { - if (props.mode === 'edit') return cloneDeep(props.record || {}); + if (props.mode === 'edit') return cloneDeep(editRecord || {}); return { name: randomId('s_'), authType: props.authType, enabled: false, options: {}, }; - }, [props.authType, props.mode, props.record]); + }, [editRecord, props.authType, props.mode]); useEffect(() => { form.setFieldsValue(initialValues); @@ -152,42 +194,53 @@ function AuthenticatorFormView(props: { -
- - - - - - - - - - - - - {AdminSettingsBody ? ( - }> - - - ) : null} -
+ {loadError ? ( + + ) : null} + +
+ + + + + + + + + + + + + {AdminSettingsBody ? ( + }> + + + ) : null} +
+
); } @@ -225,6 +278,7 @@ export default function AuthenticatorsPage() { page, pageSize, sort: ['sort'], + fields: AUTHENTICATOR_LIST_FIELDS, appends: [], }); return normalizeListResponse(response); diff --git a/packages/plugins/@nocobase/plugin-auth/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-auth/src/locale/en-US.json index 2756a0e3fa1..74c89772276 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/locale/en-US.json +++ b/packages/plugins/@nocobase/plugin-auth/src/locale/en-US.json @@ -13,6 +13,7 @@ "Days": "Days", "Email channel not found": "Email channel not found", "Enable forget password": "Enable forget password", + "Failed to load authenticator": "Failed to load authenticator", "Expired token refresh limit": "Expired token refresh limit", "Forgot password": "Forgot password", "Go to login": "Go to login", @@ -81,4 +82,4 @@ "defaultResetPasswordEmailContentHTML": "

Hello {{$user.username}},

\n\n

We received a request to reset the password for your {{$systemSettings.title}} account.

\n\n

Please click the link below to set your new password:

\n\n

\n Reset Your Password\n

\n\n

\n If you did not request a password reset, please ignore this email. Your password will remain unchanged.\n

\n\n

\n Please note: For your security, this password reset link will expire in {{$resetLinkExpiration}} minutes.\n

\n\n

If you encounter any issues resetting your password, please contact our support team.

\n\n

\n Thanks,
\n The {{$systemSettings.title}} Team\n

", "defaultResetPasswordEmailContentText": "Hello {{$user.username}},\n\nWe received a request to reset the password for your {{$systemSettings.title}} account.\n\nPlease click the link below to set your new password:\n\n{{$resetLink}}\n\nIf you did not request a password reset, please ignore this email. Your password will remain unchanged.\n\nPlease note: For your security, this password reset link will expire in {{$resetLinkExpiration}} minutes.\n\nIf you encounter any issues resetting your password, please contact our support team.\n\nThanks, The {{$systemSettings.title}} Team", "defaultResetPasswordEmailSubject": "Reset your password for {{$systemSettings.title}}" -} \ No newline at end of file +} diff --git a/packages/plugins/@nocobase/plugin-auth/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-auth/src/locale/zh-CN.json index cfb96ff26b7..5ab9620d24a 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-auth/src/locale/zh-CN.json @@ -13,6 +13,7 @@ "Days": "天", "Email channel not found": "未找到邮件通道", "Enable forget password": "启用忘记密码功能", + "Failed to load authenticator": "加载认证器失败", "Expired token refresh limit": "过期 Token 刷新时限", "Forgot password": "忘记密码", "Go to login": "前往登录", @@ -81,4 +82,4 @@ "defaultResetPasswordEmailContentHTML": "

您好 {{$user.username}},

\n\n

我们收到了重置您 {{$systemSettings.title}} 账户密码的请求。

\n\n

请点击下面的链接设置您的新密码:

\n\n

\n 重置您的密码\n

\n\n

\n 如果您没有请求重置密码,请忽略此邮件。您的密码将保持不变。\n

\n\n

\n 请注意:为了您的安全,此密码重置链接将在 {{$resetLinkExpiration}} 分钟后过期。\n

\n\n

如果您在重置密码时遇到任何问题,请联系我们的支持团队。

\n\n

\n 谢谢,
\n {{$systemSettings.title}} 团队\n

", "defaultResetPasswordEmailContentText": "您好 {{$user.username}},\n\n我们收到了重置您 {{$systemSettings.title}} 账户密码的请求。\n\n请点击下面的链接设置您的新密码:\n\n{{$resetLink}}\n\n如果您没有请求重置密码,请忽略此邮件。您的密码将保持不变。\n\n请注意:为了您的安全,此密码重置链接将在 {{$resetLinkExpiration}} 分钟后过期。\n\n如果您在重置密码时遇到任何问题,请联系我们的支持团队。\n\n谢谢,{{$systemSettings.title}} 团队", "defaultResetPasswordEmailSubject": "重置您的 {{$systemSettings.title}} 密码" -} \ No newline at end of file +}