feat: replace browser confirm with proper key-change warning dialog
Some checks failed
Mirror to GitHub / mirror (push) Failing after 35s
Build Release Binaries / build-amd64 (push) Failing after 1m57s

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) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-07 18:19:53 +04:00
parent 369347ce54
commit ded49bdb7b
3 changed files with 136 additions and 3 deletions

View File

@@ -151,6 +151,28 @@
</div>
</div>
</div>
<!-- Key changed warning dialog -->
<div id="key-warning" class="hidden">
<div class="settings-card key-warning-card">
<div class="key-warning-icon">&#9888;</div>
<h2>Server Key Changed</h2>
<p class="key-warning-text">The relay's identity has changed since you last connected. This usually happens when the server was restarted, but could also indicate a security issue.</p>
<div class="key-warning-fps">
<div class="key-fp-row">
<span class="key-fp-label">Previously known</span>
<code id="kw-old-fp" class="key-fp"></code>
</div>
<div class="key-fp-row">
<span class="key-fp-label">New key</span>
<code id="kw-new-fp" class="key-fp"></code>
</div>
</div>
<div class="key-warning-actions">
<button id="kw-accept" class="primary">Accept New Key</button>
<button id="kw-cancel" class="secondary-btn">Cancel</button>
</div>
</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>

View File

@@ -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<boolean> {
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

View File

@@ -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;