feat(settings): gate debug options behind a confirmation (+ re-auth if secured)

Revealing the DEBUG OPTIONS dropdown now opens a confirmation modal with a
warning instead of expanding directly. If the wallet is secured, it also requires
re-authentication before the options appear:
- a quick-unlock PIN → verified against the vault (Argon2id derive, off-thread,
  no wallet side effects);
- otherwise an encrypted wallet → the passphrase, verified via walletpassphrase.
Unsecured wallets just confirm. Collapsing needs no gate; each expand re-gates.

App gains debugGateRequiresAuth() + verifyDebugCredential() (cb on the main
thread; secrets wiped). Adds a modal-debug-gate sweep surface. Verified 100% +
150%, dark + light.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 18:46:50 -05:00
parent aa587d1d84
commit 46fb0029b8
6 changed files with 160 additions and 1 deletions

View File

@@ -144,6 +144,13 @@ struct SettingsPageState {
std::set<std::string> debug_categories;
bool debug_cats_dirty = false;
bool debug_expanded = false;
// Debug-options gate: a confirmation + warning (and, when a PIN/passphrase is set, re-auth) is
// required before the debug dropdown reveals its options.
bool debug_gate_open = false;
bool debug_gate_verifying = false;
char debug_gate_buf[128] = {0};
std::string debug_gate_err;
float debug_gate_err_timer = 0.0f;
bool effects_expanded = false;
bool tools_expanded = false;
bool rpc_expanded = false; // Node & Security: reveal the RPC connection fields
@@ -2521,7 +2528,15 @@ void RenderSettingsPage(App* app) {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
if (ImGui::Button("##DebugToggle", ImVec2(availWidth, ImGui::GetFrameHeight()))) {
s_settingsState.debug_expanded = !s_settingsState.debug_expanded;
if (s_settingsState.debug_expanded) {
s_settingsState.debug_expanded = false; // collapsing needs no gate
} else {
// Expanding is gated: open the confirmation + (if a PIN/passphrase is set) re-auth.
s_settingsState.debug_gate_open = true;
s_settingsState.debug_gate_buf[0] = '\0';
s_settingsState.debug_gate_err.clear();
s_settingsState.debug_gate_verifying = false;
}
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand"));
ImGui::PopStyleColor(3);
@@ -2647,6 +2662,76 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon"));
}
}
// ---- Debug-options gate: confirmation + warning (+ re-auth if a PIN/passphrase is set) ----
if (s_settingsState.debug_gate_open) {
auto& st = s_settingsState;
if (st.debug_gate_err_timer > 0.0f) {
st.debug_gate_err_timer -= ImGui::GetIO().DeltaTime;
if (st.debug_gate_err_timer <= 0.0f) st.debug_gate_err.clear();
}
const bool needAuth = app->debugGateRequiresAuth();
const bool hasPin = app->hasPinVault();
const float gdp = Layout::dpiScale();
material::OverlayDialogSpec ov;
ov.title = TR("debug_gate_title");
ov.p_open = &st.debug_gate_open;
ov.style = material::OverlayStyle::BlurFloat;
ov.cardWidth = 480.0f;
ov.idSuffix = "debuggate";
if (material::BeginOverlayDialog(ov)) {
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) st.debug_gate_open = false;
ImGui::TextWrapped("%s", TR("debug_gate_warning"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
bool submit = false;
if (needAuth) {
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
hasPin ? TR("debug_gate_pin_prompt") : TR("debug_gate_pass_prompt"));
ImGui::SetNextItemWidth(-FLT_MIN);
ImGuiInputTextFlags f = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue;
if (hasPin) f |= ImGuiInputTextFlags_CharsDecimal; // PIN is numeric
if (ImGui::InputText("##debugGateSecret", st.debug_gate_buf, sizeof(st.debug_gate_buf), f))
submit = true;
if (!st.debug_gate_err.empty()) {
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
Type().textColored(TypeStyle::Caption, Error(), st.debug_gate_err.c_str());
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::BeginDisabled(st.debug_gate_verifying);
if (TactileButton(TR("debug_gate_confirm"), ImVec2(170.0f * gdp, 0)) || submit) {
if (!needAuth) {
st.debug_expanded = true;
st.debug_gate_open = false;
} else if (strlen(st.debug_gate_buf) > 0) {
st.debug_gate_verifying = true;
std::string secret = st.debug_gate_buf;
memset(st.debug_gate_buf, 0, sizeof(st.debug_gate_buf));
app->verifyDebugCredential(secret, [](bool ok) {
auto& s = s_settingsState;
s.debug_gate_verifying = false;
if (ok) { s.debug_expanded = true; s.debug_gate_open = false; }
else { s.debug_gate_err = TR("debug_gate_incorrect"); s.debug_gate_err_timer = 4.0f; }
});
if (!secret.empty()) memset(&secret[0], 0, secret.size());
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (TactileButton(TR("cancel"), ImVec2(120.0f * gdp, 0))) {
memset(st.debug_gate_buf, 0, sizeof(st.debug_gate_buf));
st.debug_gate_open = false;
}
if (st.debug_gate_verifying) {
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("debug_gate_verifying"));
}
material::EndOverlayDialog();
}
}
}
// --- Shader-based scroll fade: unbind (restore ImGui's default shader) ---
@@ -2923,5 +3008,14 @@ void RenderSettingsPage(App* app) {
}
void SweepOpenDebugGate(bool open) {
s_settingsState.debug_gate_open = open;
if (open) {
s_settingsState.debug_gate_buf[0] = '\0';
s_settingsState.debug_gate_err.clear();
s_settingsState.debug_gate_verifying = false;
}
}
} // namespace ui
} // namespace dragonx