feat(setup): reveal toggle for secret fields (the device web password)
Build desktop / desktop (push) Successful in 4m37s
Build & push images / images (push) Successful in 2m52s
CI / check (push) Successful in 44s

The admin needs the device web password (to reach a controller/camera's own web
UI), and it's already stored + sent to this admin-only view (redactSecrets strips
only the machine secrets relay/push pw, NOT webPassword — by design, per the
SECRET_CONFIG_KEYS comment). But the form rendered every `secret` field as a masked
password input with no way to unmask it, so the value was present yet unreadable.

Add a per-field show/hide eye toggle on `secret` inputs. No new exposure: the field
is already admin-gated and the value already reaches the client; this just makes the
intended-visible credential readable/copyable. Machine secrets are redacted
server-side and never arrive, so there's nothing there to reveal. i18n sq+en.
This commit is contained in:
2026-06-27 17:51:32 +02:00
parent 6d32e0fc0f
commit e4a17efd97
3 changed files with 40 additions and 1 deletions
+34 -1
View File
@@ -387,6 +387,13 @@ function DeviceForm({
const [printResult, setPrintResult] = useState<PrintTestResult | null>(null);
const [printTesting, setPrintTesting] = useState(false);
const [printError, setPrintError] = useState<string | null>(null);
// Which `secret` fields are currently unmasked. The device web password is an
// operational credential the admin legitimately needs (to reach the device's web
// UI) — it's stored + sent to this admin-only view; a per-field reveal toggle just
// makes the already-present value readable. (Machine secrets — relay/push pw — are
// redacted server-side and never reach here, so there's nothing to reveal.)
const [revealed, setRevealed] = useState<Record<string, boolean>>({});
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [found, setFound] = useState<DiscoveredDevice[] | null>(null);
@@ -688,10 +695,36 @@ function DeviceForm({
</option>
))}
</select>
) : f.type === "secret" ? (
// Secret field with a reveal toggle: the device web password is shown
// here (admin-only view) so an admin can read/copy it to reach the
// device's own web UI. Masked by default; click the eye to reveal.
<div className="flex gap-1">
<input
className="input flex-1"
type={revealed[f.key] ? "text" : "password"}
value={(config[f.key] ?? (f.default as string | number | undefined) ?? "") as string | number}
placeholder={f.help}
onChange={(e) => {
const v = e.target.value;
setConfig((c) => ({ ...c, [f.key]: v }));
resetStatus();
}}
/>
<button
type="button"
className="btn btn-sm"
aria-label={revealed[f.key] ? t("setup.hideSecret") : t("setup.revealSecret")}
title={revealed[f.key] ? t("setup.hideSecret") : t("setup.revealSecret")}
onClick={() => setRevealed((r) => ({ ...r, [f.key]: !r[f.key] }))}
>
{revealed[f.key] ? "🙈" : "👁"}
</button>
</div>
) : (
<input
className="input"
type={f.type === "secret" ? "password" : f.type === "number" || f.type === "port" ? "number" : "text"}
type={f.type === "number" || f.type === "port" ? "number" : "text"}
value={(config[f.key] ?? (f.default as string | number | undefined) ?? "") as string | number}
placeholder={f.help}
onChange={(e) => {