From ded49bdb7b12e5a3579ac0184fbf68e6a6a1065d Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Tue, 7 Apr 2026 18:19:53 +0400 Subject: [PATCH] feat: replace browser confirm with proper key-change warning dialog When the relay's server key changes (e.g. after restart), show a styled in-app warning dialog instead of the ugly browser confirm(). The dialog shows old vs new fingerprints and lets the user accept the new key or cancel. Accepting updates the saved fingerprint and refreshes the relay button state. Co-Authored-By: Claude Opus 4.6 (1M context) --- desktop/index.html | 22 ++++++++++++ desktop/src/main.ts | 35 ++++++++++++++++-- desktop/src/style.css | 82 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 3 deletions(-) diff --git a/desktop/index.html b/desktop/index.html index 40be90c..ea99676 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -151,6 +151,28 @@ + + diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 9a73c85..4abde04 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -76,6 +76,13 @@ const sFingerprint = document.getElementById("s-fingerprint")!; const sRecentRooms = document.getElementById("s-recent-rooms")!; const sClearRecent = document.getElementById("s-clear-recent")!; +// Key warning dialog +const keyWarning = document.getElementById("key-warning")!; +const kwOldFp = document.getElementById("kw-old-fp")!; +const kwNewFp = document.getElementById("kw-new-fp")!; +const kwAccept = document.getElementById("kw-accept")!; +const kwCancel = document.getElementById("kw-cancel")!; + let statusInterval: number | null = null; let myFingerprint = ""; let userDisconnected = false; @@ -377,6 +384,28 @@ connectBtn.addEventListener("click", doConnect); el.addEventListener("keydown", (e) => { if (e.key === "Enter") doConnect(); }) ); +function showKeyWarning(oldFp: string, newFp: string): Promise { + return new Promise((resolve) => { + kwOldFp.textContent = oldFp; + kwNewFp.textContent = newFp; + keyWarning.classList.remove("hidden"); + + const cleanup = () => { + keyWarning.classList.add("hidden"); + kwAccept.removeEventListener("click", onAccept); + kwCancel.removeEventListener("click", onCancel); + keyWarning.removeEventListener("click", onBackdrop); + }; + const onAccept = () => { cleanup(); resolve(true); }; + const onCancel = () => { cleanup(); resolve(false); }; + const onBackdrop = (e: Event) => { if (e.target === keyWarning) { cleanup(); resolve(false); } }; + + kwAccept.addEventListener("click", onAccept); + kwCancel.addEventListener("click", onCancel); + keyWarning.addEventListener("click", onBackdrop); + }); +} + async function doConnect() { const relay = getSelectedRelay(); if (!relay) { connectError.textContent = "No relay selected"; return; } @@ -384,13 +413,13 @@ async function doConnect() { // Warn on fingerprint mismatch const ls = lockStatus(relay); if (ls === "changed") { - if (!confirm(`Server fingerprint has changed!\n\nKnown: ${relay.knownFingerprint}\nNew: ${relay.serverFingerprint}\n\nThis could indicate a man-in-the-middle attack. Continue?`)) { - return; - } + const accepted = await showKeyWarning(relay.knownFingerprint || "", relay.serverFingerprint || ""); + if (!accepted) return; // User accepted — update known fingerprint const s = loadSettings(); s.relays[s.selectedRelay].knownFingerprint = relay.serverFingerprint; saveSettingsObj(s); + renderRelayButton(); } // Don't block connect on offline — ping may have failed transiently diff --git a/desktop/src/style.css b/desktop/src/style.css index 02d3436..5d03216 100644 --- a/desktop/src/style.css +++ b/desktop/src/style.css @@ -652,6 +652,88 @@ button.primary:disabled { opacity: 0.5; cursor: not-allowed; } .secondary-btn:hover { border-color: var(--accent); color: var(--text); } +/* ── Key warning dialog ── */ +#key-warning { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(6px); + display: flex; + align-items: center; + justify-content: center; + z-index: 300; + padding: 20px; +} + +.key-warning-card { + max-width: 360px; + text-align: center; + gap: 16px; +} + +.key-warning-icon { + font-size: 48px; + color: var(--yellow); + line-height: 1; +} + +.key-warning-card h2 { + font-size: 18px; + font-weight: 600; +} + +.key-warning-text { + font-size: 13px; + color: var(--text-dim); + line-height: 1.5; +} + +.key-warning-fps { + display: flex; + flex-direction: column; + gap: 8px; + background: var(--surface); + border-radius: 8px; + padding: 12px; +} + +.key-fp-row { + display: flex; + flex-direction: column; + gap: 2px; + text-align: left; +} + +.key-fp-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-dim); +} + +.key-fp { + font-family: monospace; + font-size: 11px; + word-break: break-all; + color: var(--text); +} + +.key-warning-actions { + display: flex; + gap: 10px; +} + +.key-warning-actions .primary { + flex: 1; + background: var(--yellow); + color: #000; + font-weight: 600; +} + +.key-warning-actions .secondary-btn { + flex: 1; +} + /* ── Quality slider ── */ .quality-control { display: flex;