This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 53382a7f fix: prevent unsupported SSL settings from implying success
(#1048)
53382a7f is described below
commit 53382a7f627384a6d608c27cc57dbf5516332f28
Author: aias00 <[email protected]>
AuthorDate: Wed Aug 5 23:14:51 2026 -0700
fix: prevent unsupported SSL settings from implying success (#1048)
---
web/src/i18n/translations.ts | 5 +
web/src/pages/studio/SslSettings.tsx | 278 +--------------------
.../pages/studio/__tests__/SslSettings.test.tsx | 157 ++----------
3 files changed, 30 insertions(+), 410 deletions(-)
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index d15ccb37..488f5d4d 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1055,6 +1055,11 @@ const translations: Record<string, Record<Lang, string>>
= {
zh: '配置 SSL/TLS 设置以实现安全通信,更改后需重启服务器。',
en: 'Configure SSL/TLS for secure communication. Changes require server
restart.',
},
+ 'ssl.unavailable': { zh: 'SSL/TLS 配置暂不可用', en: 'SSL/TLS configuration is
unavailable' },
+ 'ssl.unavailableDesc': {
+ zh: 'Studio 尚未提供用于验证、持久化或应用 KeyStore/TrustStore
配置的服务端接口。为避免产生未生效的安全配置,此页面暂不支持保存或上传。',
+ en: 'Studio does not yet provide a server API to validate, persist, or
apply KeyStore/TrustStore configuration. Saving and uploading are unavailable
to avoid implying that security settings are active.',
+ },
'ssl.enabled': { zh: '启用 SSL/TLS', en: 'Enable SSL/TLS' },
'ssl.protocol': { zh: 'SSL 协议', en: 'SSL Protocol' },
'ssl.selectProtocol': { zh: '请选择协议', en: 'Select protocol' },
diff --git a/web/src/pages/studio/SslSettings.tsx
b/web/src/pages/studio/SslSettings.tsx
index 9ed498e1..43fb03af 100644
--- a/web/src/pages/studio/SslSettings.tsx
+++ b/web/src/pages/studio/SslSettings.tsx
@@ -15,100 +15,12 @@
* limitations under the License.
*/
-import { useState } from 'react';
-import {
- Card,
- Form,
- Input,
- Button,
- Switch,
- Select,
- Upload,
- Descriptions,
- Tag,
- Space,
- Alert,
- Divider,
- Row,
- Col,
- App,
-} from 'antd';
-import { ShieldCheck, Lock, CheckCircle, XCircle, UploadSimple } from
'@phosphor-icons/react';
+import { Alert, Card, Space } from 'antd';
+import { ShieldCheck } from '@phosphor-icons/react';
import { useLang } from '../../i18n/LangContext';
-// ─── Types ──────────────────────────────────────────────────────
-interface SslConfig {
- enabled: boolean;
- protocol: string;
- keyStoreType: string;
- keyStorePath: string;
- keyStorePassword: string;
- trustStoreType: string;
- trustStorePath: string;
- trustStorePassword: string;
- clientAuth: string;
- certificateExpiry: string | null;
- certificateIssuer: string | null;
-}
-
-interface FormValues {
- enabled: boolean;
- protocol: string;
- clientAuth: string;
- keyStoreType: string;
- keyStorePath: string;
- keyStorePassword: string;
- trustStoreType?: string;
- trustStorePath?: string;
- trustStorePassword?: string;
-}
-
-// ─── Component ──────────────────────────────────────────────────
const SslSettingsPage = () => {
- const [form] = Form.useForm<FormValues>();
- const [sslConfig] = useState<SslConfig>({
- enabled: false,
- protocol: 'TLSv1.3',
- keyStoreType: 'JKS',
- keyStorePath: '',
- keyStorePassword: '',
- trustStoreType: 'JKS',
- trustStorePath: '',
- trustStorePassword: '',
- clientAuth: 'none',
- certificateExpiry: '2025-12-31',
- certificateIssuer: "Let's Encrypt",
- });
- const sslEnabled = Form.useWatch('enabled', form) ?? sslConfig.enabled;
- const clientAuth = Form.useWatch('clientAuth', form) ?? sslConfig.clientAuth;
const { t } = useLang();
- const { message } = App.useApp();
-
- const handleSave = () => {
- message.error(t('ssl.saveUnavailable'));
- };
-
- const uploadProps = {
- name: 'certificate',
- multiple: false,
- beforeUpload: (file: File) => {
- const isCert =
- file.name.endsWith('.pem') ||
- file.name.endsWith('.crt') ||
- file.name.endsWith('.jks') ||
- file.name.endsWith('.p12');
- if (!isCert) {
- message.error(t('ssl.invalidCertFormat'));
- return false;
- }
- return false;
- },
- onChange: (info: { file: { status?: string } }) => {
- if (info.file.status === 'removed') {
- message.info(t('ssl.certRemoved'));
- }
- },
- };
return (
<div style={{ padding: 0 }}>
@@ -123,191 +35,13 @@ const SslSettingsPage = () => {
style={{ borderRadius: 8, boxShadow: '0 1px 6px rgba(0,0,0,0.04)' }}
>
<Alert
- message={t('ssl.info')}
- description={t('ssl.infoDesc')}
- type="info"
+ message={t('ssl.unavailable')}
+ description={t('ssl.unavailableDesc')}
+ type="warning"
showIcon
- style={{ marginBottom: 24 }}
+ data-testid="ssl-settings-unavailable"
/>
-
- <Form form={form} layout="vertical" onFinish={handleSave}
initialValues={sslConfig}>
- <Form.Item name="enabled" label={t('ssl.enabled')}
valuePropName="checked">
- <Switch
- checkedChildren={<CheckCircle size={12} />}
- unCheckedChildren={<XCircle size={12} />}
- />
- </Form.Item>
-
- {sslEnabled && (
- <>
- <Divider />
-
- <Row gutter={16}>
- <Col span={12}>
- <Form.Item
- name="protocol"
- label={t('ssl.protocol')}
- rules={[{ required: true, message: t('ssl.selectProtocol')
}]}
- >
- <Select
- options={[
- { value: 'TLSv1.3', label: 'TLS 1.3' },
- { value: 'TLSv1.2', label: 'TLS 1.2' },
- { value: 'TLSv1.1', label: 'TLS 1.1 (Deprecated)' },
- { value: 'TLSv1', label: 'TLS 1.0 (Deprecated)' },
- ]}
- />
- </Form.Item>
- </Col>
- <Col span={12}>
- <Form.Item
- name="clientAuth"
- label={t('ssl.clientAuth')}
- rules={[{ required: true }]}
- >
- <Select
- options={[
- { value: 'none', label: t('ssl.none') },
- { value: 'want', label: t('ssl.want') },
- { value: 'need', label: t('ssl.need') },
- ]}
- />
- </Form.Item>
- </Col>
- </Row>
-
- <Divider orientation="left">{t('ssl.keystoreConfig')}</Divider>
-
- <Row gutter={16}>
- <Col span={12}>
- <Form.Item
- name="keyStoreType"
- label={t('ssl.keystoreType')}
- rules={[{ required: true }]}
- >
- <Select
- options={[
- { value: 'JKS', label: 'JKS' },
- { value: 'PKCS12', label: 'PKCS12' },
- ]}
- />
- </Form.Item>
- </Col>
- <Col span={12}>
- <Form.Item
- name="keyStorePath"
- label={t('ssl.keystorePath')}
- rules={[{ required: true, message:
t('ssl.keystorePathPlaceholder') }]}
- >
- <Input placeholder="/path/to/keystore.jks" />
- </Form.Item>
- </Col>
- </Row>
-
- <Form.Item
- name="keyStorePassword"
- label={t('ssl.keystorePassword')}
- rules={[{ required: true, message:
t('ssl.keystorePasswordPlaceholder') }]}
- >
- <Input.Password
placeholder={t('ssl.keystorePasswordPlaceholder')} />
- </Form.Item>
-
- <Form.Item label={t('ssl.uploadKeystore')}>
- <Upload {...uploadProps}>
- <Button icon={<UploadSimple size={14}
/>}>{t('ssl.upload')}</Button>
- </Upload>
- </Form.Item>
-
- {clientAuth !== 'none' && (
- <>
- <Divider
orientation="left">{t('ssl.truststoreConfig')}</Divider>
-
- <Row gutter={16}>
- <Col span={12}>
- <Form.Item
- name="trustStoreType"
- label={t('ssl.truststoreType')}
- rules={[{ required: true }]}
- >
- <Select
- options={[
- { value: 'JKS', label: 'JKS' },
- { value: 'PKCS12', label: 'PKCS12' },
- ]}
- />
- </Form.Item>
- </Col>
- <Col span={12}>
- <Form.Item
- name="trustStorePath"
- label={t('ssl.truststorePath')}
- rules={[{ required: true, message:
t('ssl.truststorePathPlaceholder') }]}
- >
- <Input placeholder="/path/to/truststore.jks" />
- </Form.Item>
- </Col>
- </Row>
-
- <Form.Item
- name="trustStorePassword"
- label={t('ssl.truststorePassword')}
- rules={[{ required: true, message:
t('ssl.truststorePasswordPlaceholder') }]}
- >
- <Input.Password
placeholder={t('ssl.truststorePasswordPlaceholder')} />
- </Form.Item>
-
- <Form.Item label={t('ssl.uploadTruststore')}>
- <Upload {...uploadProps}>
- <Button icon={<UploadSimple size={14}
/>}>{t('ssl.upload')}</Button>
- </Upload>
- </Form.Item>
- </>
- )}
- </>
- )}
-
- <Divider />
-
- <Form.Item>
- <Space>
- <Button type="primary" htmlType="submit">
- {t('ssl.save')}
- </Button>
- <Button onClick={() =>
form.setFieldsValue(sslConfig)}>{t('common.reset')}</Button>
- </Space>
- </Form.Item>
- </Form>
</Card>
-
- {sslEnabled && sslConfig.certificateExpiry && (
- <Card
- title={
- <Space>
- <Lock size={18} style={{ color: '#1677ff' }} />
- <span>{t('ssl.certInfo')}</span>
- </Space>
- }
- bordered={false}
- style={{ marginTop: 16, borderRadius: 8, boxShadow: '0 1px 6px
rgba(0,0,0,0.04)' }}
- >
- <Descriptions bordered column={2}>
- <Descriptions.Item label={t('ssl.issuer')}>
- <Tag color="blue">{sslConfig.certificateIssuer}</Tag>
- </Descriptions.Item>
- <Descriptions.Item label={t('ssl.expiryDate')}>
- <Tag color="green">{sslConfig.certificateExpiry}</Tag>
- </Descriptions.Item>
- <Descriptions.Item label={t('ssl.protocol')}>
- <Tag>{sslConfig.protocol}</Tag>
- </Descriptions.Item>
- <Descriptions.Item label={t('common.status')}>
- <Tag color="success" icon={<CheckCircle size={12} />}>
- {t('ssl.active')}
- </Tag>
- </Descriptions.Item>
- </Descriptions>
- </Card>
- )}
</div>
);
};
diff --git a/web/src/pages/studio/__tests__/SslSettings.test.tsx
b/web/src/pages/studio/__tests__/SslSettings.test.tsx
index dac2ac1d..876528f9 100644
--- a/web/src/pages/studio/__tests__/SslSettings.test.tsx
+++ b/web/src/pages/studio/__tests__/SslSettings.test.tsx
@@ -15,156 +15,37 @@
* limitations under the License.
*/
-import { describe, it, expect, vi, beforeAll } from 'vitest';
-import { render, screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
+import { describe, expect, it } from 'vitest';
+import { render, screen } from '@testing-library/react';
import { App } from 'antd';
import { LangProvider } from '../../../i18n/LangContext';
import SslSettings from '../SslSettings';
-// Mock matchMedia for antd responsive components
-beforeAll(() => {
- Object.defineProperty(window, 'matchMedia', {
- writable: true,
- value: vi.fn().mockImplementation((query: string) => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: vi.fn(),
- removeListener: vi.fn(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn(),
- dispatchEvent: vi.fn(),
- })),
- });
-});
-
-// Mock react-router-dom
-vi.mock('react-router-dom', () => ({
- useNavigate: () => vi.fn(),
- useParams: () => ({}),
-}));
-
-const renderWithProviders = (ui: React.ReactElement) => {
- return render(
+const renderWithProviders = () =>
+ render(
<App>
- <LangProvider>{ui}</LangProvider>
+ <LangProvider>
+ <SslSettings />
+ </LangProvider>
</App>,
);
-};
describe('SslSettings Page', () => {
- it('should render the page title', () => {
- renderWithProviders(<SslSettings />);
- expect(screen.getByText('SSL/TLS 设置')).toBeInTheDocument();
- });
-
- it('should render the info alert', () => {
- renderWithProviders(<SslSettings />);
- expect(screen.getByText('SSL/TLS 配置')).toBeInTheDocument();
- });
-
- it('should render the SSL enable switch label', () => {
- renderWithProviders(<SslSettings />);
- expect(screen.getByText('启用 SSL/TLS')).toBeInTheDocument();
- });
+ it('explains that SSL settings cannot be saved before backend support
exists', () => {
+ renderWithProviders();
- it('should not show SSL config fields when SSL is disabled', () => {
- renderWithProviders(<SslSettings />);
- expect(screen.queryByText('KeyStore 配置')).not.toBeInTheDocument();
+ expect(screen.getByTestId('ssl-settings-unavailable')).toHaveTextContent(
+ 'SSL/TLS 配置暂不可用',
+ );
+ expect(screen.getByText(/暂不支持保存或上传/)).toBeInTheDocument();
});
- it('should show SSL config fields after toggling SSL switch', async () => {
- const user = userEvent.setup();
- renderWithProviders(<SslSettings />);
- const switchEl = screen.getByRole('switch');
- await user.click(switchEl);
- // After toggling, SSL config fields should appear (may appear multiple
times in labels + options)
- const protocolLabels = screen.getAllByText('SSL 协议');
- expect(protocolLabels.length).toBeGreaterThan(0);
- expect(screen.getByText('客户端认证')).toBeInTheDocument();
- expect(screen.getByText('KeyStore 配置')).toBeInTheDocument();
- });
-
- it('should show KeyStore fields after enabling SSL', async () => {
- const user = userEvent.setup();
- renderWithProviders(<SslSettings />);
- const switchEl = screen.getByRole('switch');
- await user.click(switchEl);
- expect(screen.getByText('KeyStore 类型')).toBeInTheDocument();
- expect(screen.getByText('KeyStore 路径')).toBeInTheDocument();
- expect(screen.getByText('KeyStore 密码')).toBeInTheDocument();
- });
-
- it('should restore the saved SSL state when resetting the form', async () =>
{
- const user = userEvent.setup();
- renderWithProviders(<SslSettings />);
- const switchEl = screen.getByRole('switch');
-
- await user.click(switchEl);
- expect(switchEl).toBeChecked();
- expect(screen.getByText('KeyStore 配置')).toBeInTheDocument();
-
- await user.click(screen.getByRole('button', { name: /重\s*置/ }));
-
- expect(switchEl).not.toBeChecked();
- expect(screen.queryByText('KeyStore 配置')).not.toBeInTheDocument();
- });
-
- it('should keep form actions available after disabling SSL', async () => {
- const user = userEvent.setup();
- renderWithProviders(<SslSettings />);
- const switchEl = screen.getByRole('switch');
-
- await user.click(switchEl);
- await user.click(switchEl);
-
- expect(switchEl).not.toBeChecked();
- expect(screen.getByRole('button', { name: /保\s*存/ })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: /重\s*置/ })).toBeInTheDocument();
- });
-
- it('does not persist SSL changes when the backend API is unavailable', async
() => {
- const user = userEvent.setup();
- renderWithProviders(<SslSettings />);
-
- const switchEl = screen.getByRole('switch');
- await user.click(switchEl);
- await user.type(screen.getByLabelText('KeyStore 路径'),
'/etc/rocketmq/keystore.jks');
- await user.type(screen.getByLabelText('KeyStore 密码'), 'changeit');
-
- await user.click(screen.getByRole('button', { name: /保\s*存/ }));
-
- expect(await screen.findByText('SSL
配置保存功能尚未接入真实后端接口')).toBeInTheDocument();
- expect(screen.queryByText('SSL 配置保存成功')).not.toBeInTheDocument();
-
- await user.click(screen.getByRole('button', { name: /重\s*置/ }));
-
- expect(switchEl).not.toBeChecked();
- expect(screen.queryByText('KeyStore 配置')).not.toBeInTheDocument();
- });
-
- it('should show TrustStore fields when client authentication is required',
async () => {
- const user = userEvent.setup();
- renderWithProviders(<SslSettings />);
-
- await user.click(screen.getByRole('switch'));
- expect(screen.queryByText('TrustStore 配置')).not.toBeInTheDocument();
-
- await user.click(screen.getByLabelText('客户端认证'));
- await user.click(await screen.findByText('必需'));
-
- expect(await screen.findByText('TrustStore 配置')).toBeInTheDocument();
- expect(screen.getByText('TrustStore 类型')).toBeInTheDocument();
- expect(screen.getByText('TrustStore 路径')).toBeInTheDocument();
- expect(screen.getByText('TrustStore 密码')).toBeInTheDocument();
-
- await user.click(screen.getByLabelText('客户端认证'));
- await user.click(await screen.findByText('无'));
- await waitFor(() => expect(screen.queryByText('TrustStore
配置')).not.toBeInTheDocument());
+ it('does not expose local-only controls that imply TLS is configured', () =>
{
+ renderWithProviders();
- await user.click(screen.getByLabelText('客户端认证'));
- await user.click(await screen.findByText('可选'));
- expect(await screen.findByText('TrustStore 配置')).toBeInTheDocument();
+ expect(screen.queryByRole('switch')).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /保\s*存/
})).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /上\s*传/
})).not.toBeInTheDocument();
+ expect(screen.queryByText('证书信息')).not.toBeInTheDocument();
});
});