mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 04:42:57 +00:00
Merge pull request #1260 from PR0M3TH3AN/bugfix/login-generate-cancel-8391081948034062422
Fix Create Account cancellation flow and ensure fresh keys on entry
This commit is contained in:
1 file changed
+251
-167
+251
-167
@@ -292,6 +292,7 @@ export default class LoginModalController {
|
||||
// Track modal close state to reset generated keys when the modal closes.
|
||||
this.modalCloseObserver = null;
|
||||
this.modalCloseIntervalId = null;
|
||||
this.isSelectionInProgress = false;
|
||||
|
||||
this.initializeRemoteSignerStatus();
|
||||
this.initialized = false;
|
||||
@@ -1696,30 +1697,76 @@ export default class LoginModalController {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate keys only once per modal session to avoid duplicates.
|
||||
if (!this.generatedKeypair) {
|
||||
// Do not generate keys immediately. Wait for user action.
|
||||
this.generatedKeypair = null;
|
||||
let npub = "";
|
||||
let nsec = "";
|
||||
let hexSk = "";
|
||||
|
||||
// Reset fields
|
||||
if (npubEl) {
|
||||
npubEl.textContent = "";
|
||||
}
|
||||
if (nsecEl instanceof HTMLInputElement) {
|
||||
nsecEl.value = "";
|
||||
}
|
||||
|
||||
// Insert "Generate" button
|
||||
const generateBtn = this.document.createElement("button");
|
||||
generateBtn.type = "button";
|
||||
generateBtn.className = "btn w-full mb-4";
|
||||
generateBtn.textContent = "Generate New Keys";
|
||||
// Insert before the first key field container, or at top of view?
|
||||
// view structure: h3, p, div(space-y-3)
|
||||
const contentContainer = view.querySelector(".space-y-3");
|
||||
if (contentContainer) {
|
||||
contentContainer.insertBefore(generateBtn, contentContainer.firstChild);
|
||||
}
|
||||
|
||||
const keyContainer = contentContainer
|
||||
? contentContainer.querySelector(".space-y-2")
|
||||
: null;
|
||||
if (keyContainer) {
|
||||
keyContainer.classList.add("hidden");
|
||||
}
|
||||
// Also hide the nsec container
|
||||
const nsecContainer = contentContainer
|
||||
? contentContainer.querySelectorAll(".space-y-2")[1]
|
||||
: null;
|
||||
if (nsecContainer) {
|
||||
nsecContainer.classList.add("hidden");
|
||||
}
|
||||
|
||||
generateBtn.addEventListener("click", () => {
|
||||
const sk = tools.generateSecretKey();
|
||||
const pk = tools.getPublicKey(sk);
|
||||
const npub = tools.nip19?.npubEncode ? tools.nip19.npubEncode(pk) : pk;
|
||||
const nsec = tools.nip19?.nsecEncode ? tools.nip19.nsecEncode(sk) : "";
|
||||
const hexSk = nsec
|
||||
? nsec
|
||||
const npubGen = tools.nip19?.npubEncode ? tools.nip19.npubEncode(pk) : pk;
|
||||
const nsecGen = tools.nip19?.nsecEncode ? tools.nip19.nsecEncode(sk) : "";
|
||||
const hexSkGen = nsecGen
|
||||
? nsecGen
|
||||
: tools.nip19?.bytesToHex
|
||||
? tools.nip19.bytesToHex(sk)
|
||||
: "";
|
||||
|
||||
this.generatedKeypair = {
|
||||
npub,
|
||||
nsec,
|
||||
hexSk,
|
||||
npub: npubGen,
|
||||
nsec: nsecGen,
|
||||
hexSk: hexSkGen,
|
||||
pubkey: pk,
|
||||
};
|
||||
}
|
||||
|
||||
const { npub, nsec, hexSk } = this.generatedKeypair;
|
||||
npub = npubGen;
|
||||
nsec = nsecGen;
|
||||
hexSk = hexSkGen;
|
||||
|
||||
// Display keys
|
||||
if (npubEl) npubEl.textContent = npub;
|
||||
if (nsecEl instanceof HTMLInputElement) nsecEl.value = nsec || "Error generating nsec";
|
||||
if (npubEl) npubEl.textContent = npub;
|
||||
if (nsecEl instanceof HTMLInputElement) nsecEl.value = nsec;
|
||||
|
||||
generateBtn.classList.add("hidden");
|
||||
if (keyContainer) keyContainer.classList.remove("hidden");
|
||||
if (nsecContainer) nsecContainer.classList.remove("hidden");
|
||||
if (loginBtn) loginBtn.disabled = true; // Still requires checkbox confirmation
|
||||
});
|
||||
|
||||
const elementsToHide = [];
|
||||
for (const child of Array.from(this.modalBody.children)) {
|
||||
@@ -1764,11 +1811,26 @@ export default class LoginModalController {
|
||||
|
||||
// Interactions
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener("click", () => finish(null), { once: true });
|
||||
cancelBtn.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
finish(null);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (loginBtn) {
|
||||
loginBtn.addEventListener("click", () => {
|
||||
if (!this.generatedKeypair) {
|
||||
return;
|
||||
}
|
||||
const { npub, nsec, hexSk } = this.generatedKeypair;
|
||||
|
||||
// Block generated accounts if whitelist-only access is enforced.
|
||||
const accessControl = this.services?.authService?.accessControl;
|
||||
if (accessControl && typeof accessControl.canAccess === "function") {
|
||||
@@ -2230,6 +2292,10 @@ export default class LoginModalController {
|
||||
}
|
||||
|
||||
async handleProviderSelection(providerId) {
|
||||
if (this.isSelectionInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = this.getProviderEntry(providerId);
|
||||
if (!entry) {
|
||||
devLogger.warn(
|
||||
@@ -2252,171 +2318,173 @@ export default class LoginModalController {
|
||||
return;
|
||||
}
|
||||
|
||||
safeInvoke(this.callbacks.onProviderSelected, providerId);
|
||||
|
||||
let providerOptions = {};
|
||||
if (entry.provider.id === "nsec") {
|
||||
try {
|
||||
const nsecOptions = await this.promptForNsecOptions();
|
||||
if (!nsecOptions) {
|
||||
devLogger.log(
|
||||
"[LoginModalController] Direct key login cancelled by the user.",
|
||||
);
|
||||
if (entry.button instanceof HTMLButtonElement) {
|
||||
entry.button.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
providerOptions = nsecOptions;
|
||||
} catch (promptError) {
|
||||
devLogger.error(
|
||||
"[LoginModalController] Failed to collect direct key credentials:",
|
||||
promptError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (entry.provider.id === "nip46") {
|
||||
try {
|
||||
const nip46Options = await this.promptForNip46Options();
|
||||
if (!nip46Options) {
|
||||
devLogger.log(
|
||||
"[LoginModalController] Remote signer login cancelled by the user.",
|
||||
);
|
||||
if (entry.button instanceof HTMLButtonElement) {
|
||||
entry.button.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
providerOptions = nip46Options;
|
||||
} catch (promptError) {
|
||||
devLogger.error(
|
||||
"[LoginModalController] Failed to collect remote signer connection:",
|
||||
promptError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (entry.provider.id === "generate") {
|
||||
try {
|
||||
const generateOptions = await this.promptForGenerateOptions();
|
||||
if (!generateOptions) {
|
||||
devLogger.log(
|
||||
"[LoginModalController] Account generation cancelled by the user.",
|
||||
);
|
||||
if (entry.button instanceof HTMLButtonElement) {
|
||||
entry.button.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
providerOptions = generateOptions;
|
||||
// Switch provider context to 'nsec' since we are logging in with a key
|
||||
providerId = "nsec";
|
||||
const nsecEntry = this.getProviderEntry("nsec");
|
||||
if (nsecEntry) {
|
||||
entry = nsecEntry;
|
||||
}
|
||||
} catch (promptError) {
|
||||
devLogger.error(
|
||||
"[LoginModalController] Failed to process account generation:",
|
||||
promptError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setLoadingState(providerId, true);
|
||||
devLogger.log(
|
||||
`[LoginModalController] Starting login for provider ${providerId}.`,
|
||||
);
|
||||
|
||||
const requestOptions = { providerId, ...providerOptions };
|
||||
|
||||
const extraOptions = await this.resolveNextRequestLoginOptions({
|
||||
provider: entry.provider.source || entry.provider,
|
||||
providerId,
|
||||
});
|
||||
if (extraOptions && typeof extraOptions === "object") {
|
||||
Object.assign(requestOptions, extraOptions);
|
||||
}
|
||||
|
||||
this.isSelectionInProgress = true;
|
||||
try {
|
||||
const result = await this.services.authService.requestLogin(requestOptions);
|
||||
safeInvoke(this.callbacks.onProviderSelected, providerId);
|
||||
|
||||
let providerOptions = {};
|
||||
if (entry.provider.id === "nsec") {
|
||||
try {
|
||||
const nsecOptions = await this.promptForNsecOptions();
|
||||
if (!nsecOptions) {
|
||||
devLogger.log(
|
||||
"[LoginModalController] Direct key login cancelled by the user.",
|
||||
);
|
||||
if (entry.button instanceof HTMLButtonElement) {
|
||||
entry.button.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
providerOptions = nsecOptions;
|
||||
} catch (promptError) {
|
||||
devLogger.error(
|
||||
"[LoginModalController] Failed to collect direct key credentials:",
|
||||
promptError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (entry.provider.id === "nip46") {
|
||||
try {
|
||||
const nip46Options = await this.promptForNip46Options();
|
||||
if (!nip46Options) {
|
||||
devLogger.log(
|
||||
"[LoginModalController] Remote signer login cancelled by the user.",
|
||||
);
|
||||
if (entry.button instanceof HTMLButtonElement) {
|
||||
entry.button.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
providerOptions = nip46Options;
|
||||
} catch (promptError) {
|
||||
devLogger.error(
|
||||
"[LoginModalController] Failed to collect remote signer connection:",
|
||||
promptError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (entry.provider.id === "generate") {
|
||||
try {
|
||||
const generateOptions = await this.promptForGenerateOptions();
|
||||
if (!generateOptions) {
|
||||
devLogger.log(
|
||||
"[LoginModalController] Account generation cancelled by the user.",
|
||||
);
|
||||
if (entry.button instanceof HTMLButtonElement) {
|
||||
entry.button.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
providerOptions = generateOptions;
|
||||
// Switch provider context to 'nsec' since we are logging in with a key
|
||||
providerId = "nsec";
|
||||
const nsecEntry = this.getProviderEntry("nsec");
|
||||
if (nsecEntry) {
|
||||
entry = nsecEntry;
|
||||
}
|
||||
} catch (promptError) {
|
||||
devLogger.error(
|
||||
"[LoginModalController] Failed to process account generation:",
|
||||
promptError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setLoadingState(providerId, true);
|
||||
devLogger.log(
|
||||
`[LoginModalController] Login resolved for ${providerId} with pubkey:`,
|
||||
result?.pubkey,
|
||||
`[LoginModalController] Starting login for provider ${providerId}.`,
|
||||
);
|
||||
|
||||
const consumed = this.resolvePendingTask(result, { type: "add-profile" });
|
||||
const shouldClose =
|
||||
result &&
|
||||
typeof result === "object" &&
|
||||
typeof result.pubkey === "string" &&
|
||||
result.pubkey.trim();
|
||||
const requestOptions = { providerId, ...providerOptions };
|
||||
|
||||
if (shouldClose) {
|
||||
this.helpers.closeModal();
|
||||
}
|
||||
|
||||
if (consumed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await safeInvokeAsync(this.callbacks.onLoginSuccess, {
|
||||
const extraOptions = await this.resolveNextRequestLoginOptions({
|
||||
provider: entry.provider.source || entry.provider,
|
||||
result,
|
||||
providerId,
|
||||
});
|
||||
} catch (error) {
|
||||
devLogger.error(
|
||||
`[LoginModalController] Login failed for ${providerId}:`,
|
||||
error,
|
||||
);
|
||||
if (extraOptions && typeof extraOptions === "object") {
|
||||
Object.assign(requestOptions, extraOptions);
|
||||
}
|
||||
|
||||
const message = this.helpers.describeLoginError(
|
||||
error,
|
||||
entry.provider.errorMessage,
|
||||
);
|
||||
try {
|
||||
const result =
|
||||
await this.services.authService.requestLogin(requestOptions);
|
||||
|
||||
if (
|
||||
!message ||
|
||||
(typeof message === "string" && !message.trim())
|
||||
) {
|
||||
devLogger.warn(
|
||||
`[LoginModalController] describeLoginError returned empty message for ${providerId}.`,
|
||||
devLogger.log(
|
||||
`[LoginModalController] Login resolved for ${providerId} with pubkey:`,
|
||||
result?.pubkey,
|
||||
);
|
||||
|
||||
const consumed = this.resolvePendingTask(result, { type: "add-profile" });
|
||||
const shouldClose =
|
||||
result &&
|
||||
typeof result === "object" &&
|
||||
typeof result.pubkey === "string" &&
|
||||
result.pubkey.trim();
|
||||
|
||||
if (shouldClose) {
|
||||
this.helpers.closeModal();
|
||||
}
|
||||
|
||||
if (consumed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await safeInvokeAsync(this.callbacks.onLoginSuccess, {
|
||||
provider: entry.provider.source || entry.provider,
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
devLogger.error(
|
||||
`[LoginModalController] Login failed for ${providerId}:`,
|
||||
error,
|
||||
);
|
||||
|
||||
const message = this.helpers.describeLoginError(
|
||||
error,
|
||||
entry.provider.errorMessage,
|
||||
);
|
||||
|
||||
if (!message || (typeof message === "string" && !message.trim())) {
|
||||
devLogger.warn(
|
||||
`[LoginModalController] describeLoginError returned empty message for ${providerId}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const rejectionError =
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error(message || "Failed to login. Please try again.");
|
||||
|
||||
if (message && message !== rejectionError.message) {
|
||||
rejectionError.message = message;
|
||||
}
|
||||
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
typeof error.code === "string" &&
|
||||
!rejectionError.code
|
||||
) {
|
||||
rejectionError.code = error.code;
|
||||
}
|
||||
|
||||
const consumed = this.rejectPendingTask(rejectionError, {
|
||||
type: "add-profile",
|
||||
});
|
||||
|
||||
if (consumed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await safeInvokeAsync(this.callbacks.onLoginError, {
|
||||
provider: entry.provider.source || entry.provider,
|
||||
error: rejectionError,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
const rejectionError =
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error(message || "Failed to login. Please try again.");
|
||||
|
||||
if (message && message !== rejectionError.message) {
|
||||
rejectionError.message = message;
|
||||
}
|
||||
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
typeof error.code === "string" &&
|
||||
!rejectionError.code
|
||||
) {
|
||||
rejectionError.code = error.code;
|
||||
}
|
||||
|
||||
const consumed = this.rejectPendingTask(rejectionError, {
|
||||
type: "add-profile",
|
||||
});
|
||||
|
||||
if (consumed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await safeInvokeAsync(this.callbacks.onLoginError, {
|
||||
provider: entry.provider.source || entry.provider,
|
||||
error: rejectionError,
|
||||
message,
|
||||
});
|
||||
} finally {
|
||||
this.isSelectionInProgress = false;
|
||||
this.setLoadingState(providerId, false);
|
||||
if (this.pendingNip46Cleanup) {
|
||||
try {
|
||||
@@ -2477,6 +2545,22 @@ export default class LoginModalController {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.modalCloseObserver) {
|
||||
try {
|
||||
this.modalCloseObserver.disconnect();
|
||||
} catch (error) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
this.modalCloseObserver = null;
|
||||
|
||||
if (this.modalCloseIntervalId) {
|
||||
if (this.window && typeof this.window.clearInterval === "function") {
|
||||
this.window.clearInterval(this.modalCloseIntervalId);
|
||||
}
|
||||
}
|
||||
this.modalCloseIntervalId = null;
|
||||
|
||||
if (this.pendingTask && typeof this.pendingTask.reject === "function") {
|
||||
try {
|
||||
this.pendingTask.reject(this.createCancellationError());
|
||||
|
||||
Reference in new issue
Block a user