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) => {

Reply via email to