This is an automated email from the ASF dual-hosted git repository.
young pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new d32c945a4 fix: mask sensitive fields (#3251)
d32c945a4 is described below
commit d32c945a43507f6bde4d0baaba1f4785072440f3
Author: Yuvraj Singh Chauhan <[email protected]>
AuthorDate: Fri Nov 21 07:45:29 2025 +0530
fix: mask sensitive fields (#3251)
* feat: implement password input component and update forms to use it
* fix: reorder import statements for consistency in i18n configuration
* test: add visibility toggle functionality for password input
---
e2e/tests/auth.spec.ts | 39 +++++++++++++++++++-
e2e/utils/test.ts | 3 +-
src/components/form-slice/FormPartSecret.tsx | 11 +++---
src/components/form/PasswordInput.tsx | 53 ++++++++++++++++++++++++++++
src/components/page/SettingsModal.tsx | 10 ++++--
5 files changed, 107 insertions(+), 9 deletions(-)
diff --git a/e2e/tests/auth.spec.ts b/e2e/tests/auth.spec.ts
index 80c9e7e68..ef911cbba 100644
--- a/e2e/tests/auth.spec.ts
+++ b/e2e/tests/auth.spec.ts
@@ -24,7 +24,7 @@ test.use({ storageState: { cookies: [], origins: [] } });
test('can auth with admin key', { tag: '@auth' }, async ({ page }) => {
const settingsModal = page.getByRole('dialog', { name: 'Settings' });
- const adminKeyInput = page.getByRole('textbox', { name: 'Admin Key' });
+ const adminKeyInput = page.getByLabel('Admin Key');
const failedMsg = page.getByText('failed to check token');
const checkSettingsModal = async () => {
@@ -64,3 +64,40 @@ test('can auth with admin key', { tag: '@auth' }, async ({
page }) => {
await expect(failedMsg).toBeHidden();
});
});
+
+test('password input can toggle visibility', { tag: '@auth' }, async ({ page
}) => {
+ const settingsModal = page.getByRole('dialog', { name: 'Settings' });
+ const adminKeyInput = page.getByLabel('Admin Key');
+ const testPassword = 'test-admin-key-12345';
+
+ await expect(settingsModal).toBeVisible();
+
+ await test.step('verify password input is initially masked', async () => {
+ await adminKeyInput.fill(testPassword);
+
+ await expect(adminKeyInput).toHaveAttribute('type', 'password');
+ });
+
+ await test.step('reveal password by clicking visibility toggle', async () =>
{
+ // Mantine PasswordInput has a button with class
mantine-PasswordInput-visibilityToggle
+ const toggleButton = settingsModal.locator(
+ '.mantine-PasswordInput-visibilityToggle'
+ );
+
+ await toggleButton.click();
+
+ await expect(adminKeyInput).toHaveAttribute('type', 'text');
+ await expect(adminKeyInput).toHaveValue(testPassword);
+ });
+
+ await test.step('hide password by clicking visibility toggle again', async
() => {
+ const toggleButton = settingsModal.locator(
+ '.mantine-PasswordInput-visibilityToggle'
+ );
+
+ await toggleButton.click();
+
+ await expect(adminKeyInput).toHaveAttribute('type', 'password');
+ await expect(adminKeyInput).toHaveValue(testPassword);
+ });
+});
diff --git a/e2e/utils/test.ts b/e2e/utils/test.ts
index 3e2a2d4c8..690d78de5 100644
--- a/e2e/utils/test.ts
+++ b/e2e/utils/test.ts
@@ -51,7 +51,8 @@ export const test = baseTest.extend<object, {
workerStorageState: string }>({
// we need to authenticate
const settingsModal = page.getByRole('dialog', { name: 'Settings' });
await expect(settingsModal).toBeVisible();
- const adminKeyInput = page.getByRole('textbox', { name: 'Admin Key' });
+ // PasswordInput renders with a label, use getByLabel instead
+ const adminKeyInput = page.getByLabel('Admin Key');
await adminKeyInput.clear();
await adminKeyInput.fill(adminKey);
await page
diff --git a/src/components/form-slice/FormPartSecret.tsx
b/src/components/form-slice/FormPartSecret.tsx
index 33aca159f..7b19aa024 100644
--- a/src/components/form-slice/FormPartSecret.tsx
+++ b/src/components/form-slice/FormPartSecret.tsx
@@ -18,6 +18,7 @@ import { Divider, InputWrapper } from '@mantine/core';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
+import { FormItemPasswordInput } from '@/components/form/PasswordInput';
import { FormItemSelect } from '@/components/form/Select';
import { FormItemSwitch } from '@/components/form/Switch';
import { FormItemTextInput } from '@/components/form/TextInput';
@@ -42,7 +43,7 @@ const VaultSecretForm = () => {
name="prefix"
label={t('form.secrets.vault.prefix')}
/>
- <FormItemTextInput
+ <FormItemPasswordInput
control={control}
name="token"
label={t('form.secrets.vault.token')}
@@ -62,17 +63,17 @@ const AWSSecretForm = () => {
return (
<>
- <FormItemTextInput
+ <FormItemPasswordInput
control={control}
name="access_key_id"
label={t('form.secrets.aws.access_key_id')}
/>
- <FormItemTextInput
+ <FormItemPasswordInput
control={control}
name="secret_access_key"
label={t('form.secrets.aws.secret_access_key')}
/>
- <FormItemTextInput
+ <FormItemPasswordInput
control={control}
name="session_token"
label={t('form.secrets.aws.session_token')}
@@ -114,7 +115,7 @@ const GCPSecretForm = () => {
name="auth_config.client_email"
label={t('form.secrets.gcp.client_email')}
/>
- <FormItemTextInput
+ <FormItemPasswordInput
control={control}
name="auth_config.private_key"
label={t('form.secrets.gcp.private_key')}
diff --git a/src/components/form/PasswordInput.tsx
b/src/components/form/PasswordInput.tsx
new file mode 100644
index 000000000..e9716c2fd
--- /dev/null
+++ b/src/components/form/PasswordInput.tsx
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { PasswordInput, type PasswordInputProps } from '@mantine/core';
+import {
+ type FieldValues,
+ useController,
+ type UseControllerProps,
+} from 'react-hook-form';
+
+import { genControllerProps } from './util';
+
+export type FormItemPasswordInputProps<T extends FieldValues> =
+ UseControllerProps<T> & PasswordInputProps;
+
+/**
+ * Form field component for sensitive data (passwords, tokens, keys).
+ * Renders input with masked characters by default with an option to reveal.
+ */
+export const FormItemPasswordInput = <T extends FieldValues>(
+ props: FormItemPasswordInputProps<T>
+) => {
+ const { controllerProps, restProps } = genControllerProps(props, '');
+ const {
+ field: { value, onChange: fOnChange, ...restField },
+ fieldState,
+ } = useController<T>(controllerProps);
+ return (
+ <PasswordInput
+ value={value}
+ error={fieldState.error?.message}
+ onChange={(e) => {
+ fOnChange(e);
+ restProps.onChange?.(e);
+ }}
+ {...restField}
+ {...restProps}
+ />
+ );
+};
diff --git a/src/components/page/SettingsModal.tsx
b/src/components/page/SettingsModal.tsx
index a9e582ee2..10cde2ce5 100644
--- a/src/components/page/SettingsModal.tsx
+++ b/src/components/page/SettingsModal.tsx
@@ -14,7 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Divider, InputWrapper, Modal, Text, TextInput } from '@mantine/core';
+import {
+ Divider,
+ InputWrapper,
+ Modal,
+ PasswordInput,
+ Text,
+} from '@mantine/core';
import { useAtom } from 'jotai';
import { useTranslation } from 'react-i18next';
@@ -27,7 +33,7 @@ const AdminKey = () => {
const [adminKey, setAdminKey] = useAtom(adminKeyAtom);
return (
- <TextInput
+ <PasswordInput
label={t('settings.adminKey')}
value={adminKey}
onChange={(e) => {