Improve credential redaction handling (#23265)

* redact credentials in config endpoint with sentinel

* backend test

* frontend

* apply widget for credential fields

* i18n
This commit is contained in:
Josh Hawkins
2026-05-20 15:59:01 -06:00
committed by GitHub
parent 5ef8b9b924
commit 68e8afd35c
12 changed files with 126 additions and 17 deletions
@@ -24,6 +24,7 @@ const genai: SectionConfigOverrides = {
"ui:widget": "genaiRoles",
},
"*.api_key": {
"ui:widget": "password",
"ui:options": { size: "lg" },
},
"*.base_url": {
@@ -64,6 +64,7 @@ const mqtt: SectionConfigOverrides = {
liveValidate: true,
uiSchema: {
password: {
"ui:widget": "password",
"ui:options": { size: "xs" },
},
},
@@ -29,6 +29,9 @@ const onvif: SectionConfigOverrides = {
host: {
"ui:options": { size: "sm" },
},
password: {
"ui:widget": "password",
},
profile: {
"ui:widget": "onvifProfile",
},
@@ -18,6 +18,7 @@ const proxy: SectionConfigOverrides = {
"ui:options": { size: "lg" },
},
auth_secret: {
"ui:widget": "password",
"ui:options": { size: "md" },
},
header_map: {
@@ -3,8 +3,10 @@ import type { WidgetProps } from "@rjsf/utils";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { LuEye, LuEyeOff } from "react-icons/lu";
import { cn } from "@/lib/utils";
import { REDACTED_CREDENTIAL_SENTINEL } from "@/lib/const";
import { getSizedFieldClassName } from "../utils";
export function PasswordWidget(props: WidgetProps) {
@@ -21,17 +23,31 @@ export function PasswordWidget(props: WidgetProps) {
options,
} = props;
const { t } = useTranslation(["common"]);
const [showPassword, setShowPassword] = useState(false);
const fieldClassName = getSizedFieldClassName(options, "sm");
// When the backend returns the sentinel, hide it visually and prompt the
// user that a value is already saved. The value stays as the sentinel in
// form state — backend /config/set strips it so the saved YAML is
// preserved when the user doesn't touch the field.
const isRedacted = value === REDACTED_CREDENTIAL_SENTINEL;
const displayValue = isRedacted ? "" : (value ?? "");
const effectivePlaceholder = isRedacted
? t("credentialField.savedPlaceholder", {
ns: "common",
defaultValue: "Saved — leave blank to keep current",
})
: placeholder || "";
return (
<div className={cn("relative", fieldClassName)}>
<Input
id={id}
type={showPassword ? "text" : "password"}
value={value ?? ""}
value={displayValue}
disabled={disabled || readonly}
placeholder={placeholder || ""}
placeholder={effectivePlaceholder}
onChange={(e) =>
onChange(e.target.value === "" ? undefined : e.target.value)
}
@@ -46,7 +62,7 @@ export function PasswordWidget(props: WidgetProps) {
size="sm"
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
disabled={disabled}
disabled={disabled || isRedacted}
>
{showPassword ? (
<LuEyeOff className="h-4 w-4" />