mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 00:42:30 +00:00
Switching to a saved nsec profile silently failed: switchProfile -> requestLogin -> the
nsec provider was called with no passphrase, so it threw "secret-required" and the switch
was caught+logged with no user feedback. It never prompted for the PIN.
Now handleProfileSwitchRequest detects an nsec target and, before switching, prepares the
stored-key unlock:
- evaluateStoredNsecSwitch(meta, target) (pure) gates on a persisted nsec key for THIS
account (via getStoredSessionActorMetadata); errors clearly otherwise ("log in with its
nsec", "different account's key saved").
- showPasswordPrompt (js/ui/promptDialog.js) collects the PIN/passphrase in a styled
dialog (Promise<string|null>, stacks above modals).
- switchProfile(pubkey, { unlockStored: true, passphrase }) — authService.switchProfile
now forwards unlockStored/passphrase to requestLogin, which routes to the nsec
provider's unlockStoredSessionActor to restore the signer and activate the account.
- Wrong PIN -> "Incorrect PIN/passphrase" toast (decrypt-failed).
So NIP-07 -> nsec switching works fluently. NIP-46 switching is unchanged (follow-up to
verify/extend); multiple-nsec accounts still share one stored slot (follow-up).
Tests: tests/stored-nsec-switch.test.mjs (6), tests/password-prompt.test.mjs (6). Build +
lint clean; auth suites 25/25.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
112 lines
3.7 KiB
JavaScript
112 lines
3.7 KiB
JavaScript
// Promise-based password/PIN prompt rendered with the site's modal styling (sibling of
|
|
// confirmDialog). Used to collect the passphrase that unlocks a stored nsec key — e.g.
|
|
// when switching to a saved nsec account. Resolves the entered string, or null if the
|
|
// user cancels. Builds its own `bv-modal modal-always-on-top` overlay so it stacks above
|
|
// other modals (and is typable thanks to the stacked-modal focus fix).
|
|
|
|
export function showPasswordPrompt(message, options = {}) {
|
|
const {
|
|
title = "",
|
|
confirmLabel = "Unlock",
|
|
cancelLabel = "Cancel",
|
|
placeholder = "PIN / passphrase",
|
|
} = options || {};
|
|
|
|
if (typeof document === "undefined" || !document.body) {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
const overlay = document.createElement("div");
|
|
overlay.className =
|
|
"bv-modal modal-always-on-top items-start justify-center md:items-center";
|
|
overlay.setAttribute("role", "dialog");
|
|
overlay.setAttribute("aria-modal", "true");
|
|
|
|
const backdrop = document.createElement("div");
|
|
backdrop.className = "bv-modal-backdrop";
|
|
overlay.appendChild(backdrop);
|
|
|
|
const sheet = document.createElement("div");
|
|
sheet.className = "modal-sheet w-full max-w-md flex flex-col";
|
|
sheet.tabIndex = -1;
|
|
overlay.appendChild(sheet);
|
|
|
|
if (title) {
|
|
const header = document.createElement("div");
|
|
header.className = "modal-header";
|
|
const heading = document.createElement("h2");
|
|
heading.className = "text-lg font-bold text-text";
|
|
heading.textContent = title;
|
|
header.appendChild(heading);
|
|
sheet.appendChild(header);
|
|
}
|
|
|
|
const body = document.createElement("div");
|
|
body.className = "p-5 space-y-3";
|
|
if (message) {
|
|
const text = document.createElement("p");
|
|
text.className = "text-sm text-muted-strong whitespace-pre-line";
|
|
text.textContent = String(message);
|
|
body.appendChild(text);
|
|
}
|
|
const input = document.createElement("input");
|
|
input.type = "password";
|
|
input.className = "input w-full";
|
|
input.placeholder = placeholder;
|
|
input.autocomplete = "off";
|
|
input.setAttribute("aria-label", placeholder);
|
|
body.appendChild(input);
|
|
sheet.appendChild(body);
|
|
|
|
const footer = document.createElement("div");
|
|
footer.className =
|
|
"flex items-center justify-end gap-3 border-t border-border/60 p-4";
|
|
const cancelBtn = document.createElement("button");
|
|
cancelBtn.type = "button";
|
|
cancelBtn.className = "btn-ghost focus-ring";
|
|
cancelBtn.textContent = cancelLabel;
|
|
const confirmBtn = document.createElement("button");
|
|
confirmBtn.type = "button";
|
|
confirmBtn.className = "btn focus-ring";
|
|
confirmBtn.textContent = confirmLabel;
|
|
footer.appendChild(cancelBtn);
|
|
footer.appendChild(confirmBtn);
|
|
sheet.appendChild(footer);
|
|
|
|
let settled = false;
|
|
const cleanup = (result) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
document.removeEventListener("keydown", onKeydown, true);
|
|
overlay.remove();
|
|
resolve(result);
|
|
};
|
|
const submit = () => cleanup(input.value);
|
|
function onKeydown(event) {
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
cleanup(null);
|
|
}
|
|
}
|
|
|
|
cancelBtn.addEventListener("click", () => cleanup(null));
|
|
backdrop.addEventListener("click", () => cleanup(null));
|
|
confirmBtn.addEventListener("click", submit);
|
|
input.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
submit();
|
|
}
|
|
});
|
|
document.addEventListener("keydown", onKeydown, true);
|
|
|
|
document.body.appendChild(overlay);
|
|
input.focus();
|
|
});
|
|
}
|
|
|
|
export default showPasswordPrompt;
|