From b5d2d044018029cf884e540ae14e26b2b32073dd Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:31:51 -0500 Subject: [PATCH 1/3] Add access control check to login --- js/services/authService.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/js/services/authService.js b/js/services/authService.js index 69714205..dafec531 100644 --- a/js/services/authService.js +++ b/js/services/authService.js @@ -500,6 +500,35 @@ export default class AuthService { } } + if (control && typeof control.canAccess === "function") { + let canAccess = true; + try { + canAccess = control.canAccess(candidateNpub || nextPubkey); + } catch (error) { + this.log("[AuthService] accessControl.canAccess threw", error); + canAccess = Boolean(canAccess); + } + + if (!canAccess) { + let isBlocked = false; + if (typeof control.isBlacklisted === "function") { + try { + isBlocked = control.isBlacklisted(candidateNpub || nextPubkey); + } catch (error) { + this.log("[AuthService] accessControl.isBlacklisted threw", error); + isBlocked = false; + } + } + + const accessError = new Error( + isBlocked + ? "Your account has been blocked on this platform." + : "Access restricted to admins and moderators users only.", + ); + throw accessError; + } + } + if (normalized) { setPubkey(normalized); if (this.nostrClient && typeof this.nostrClient === "object") { From 4da17853cd04d172ce6a93b2b8ac816f6e7a8d9d Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:53:36 -0500 Subject: [PATCH 2/3] Guard generate key reuse until modal closes --- js/ui/loginModalController.js | 105 ++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 6 deletions(-) diff --git a/js/ui/loginModalController.js b/js/ui/loginModalController.js index eaeb75d4..038f7bba 100644 --- a/js/ui/loginModalController.js +++ b/js/ui/loginModalController.js @@ -287,6 +287,9 @@ export default class LoginModalController { this.nextRequestLoginOptionsResolver = null; this.pendingTask = null; this.modalPrepared = false; + this.generatedKeypair = null; + this.modalCloseObserver = null; + this.modalCloseIntervalId = null; this.initializeRemoteSignerStatus(); this.initialized = false; @@ -310,6 +313,7 @@ export default class LoginModalController { this.template = this.resolveTemplate(); this.renderProviders(); + this.initializeModalCloseTracking(); if (typeof this.lastRemoteSignerStatus !== "undefined") { this.applyRemoteSignerStatus(this.lastRemoteSignerStatus); @@ -319,6 +323,53 @@ export default class LoginModalController { this.initialized = true; } + initializeModalCloseTracking() { + const modal = this.modalElement; + if (!(modal instanceof HTMLElement)) { + return; + } + + const handleClose = () => { + if (!this.isModalOpen()) { + this.resetGeneratedKeypair(); + } + }; + + if (typeof MutationObserver === "function") { + if (this.modalCloseObserver) { + return; + } + try { + const observer = new MutationObserver(handleClose); + observer.observe(modal, { + attributes: true, + attributeFilter: ["data-open", "class"], + }); + this.modalCloseObserver = observer; + return; + } catch (error) { + devLogger.warn( + "[LoginModalController] Failed to observe login modal close state:", + error, + ); + } + } + + if ( + this.modalCloseIntervalId || + !this.window || + typeof this.window.setInterval !== "function" + ) { + return; + } + + this.modalCloseIntervalId = this.window.setInterval(handleClose, 500); + } + + resetGeneratedKeypair() { + this.generatedKeypair = null; + } + ensureModalPrepared() { if (!(this.modalElement instanceof HTMLElement)) { return null; @@ -339,6 +390,7 @@ export default class LoginModalController { } this.modalPrepared = true; + this.initializeModalCloseTracking(); return this.modalElement instanceof HTMLElement ? this.modalElement : null; } @@ -1636,12 +1688,25 @@ export default class LoginModalController { return null; } - // Generate keys - 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 : tools.nip19?.bytesToHex ? tools.nip19.bytesToHex(sk) : ""; // Fallback if nsecEncode missing? unlikely with recent tools + if (!this.generatedKeypair) { + 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 + : tools.nip19?.bytesToHex + ? tools.nip19.bytesToHex(sk) + : ""; + this.generatedKeypair = { + npub, + nsec, + hexSk, + pubkey: pk, + }; + } + + const { npub, nsec, hexSk } = this.generatedKeypair; // Display keys if (npubEl) npubEl.textContent = npub; @@ -1695,6 +1760,34 @@ export default class LoginModalController { if (loginBtn) { loginBtn.addEventListener("click", () => { + const accessControl = this.services?.authService?.accessControl; + if (accessControl && typeof accessControl.canAccess === "function") { + let canAccess = true; + try { + canAccess = accessControl.canAccess(npub); + } catch (error) { + devLogger.warn( + "[LoginModalController] accessControl.canAccess threw:", + error, + ); + canAccess = Boolean(canAccess); + } + + if (!canAccess && typeof accessControl.whitelistMode === "function") { + const isWhitelistMode = accessControl.whitelistMode(); + if (isWhitelistMode) { + const accessError = new Error( + "Access restricted to admins and moderators users only.", + ); + safeInvoke(this.callbacks.onLoginError, { + message: accessError.message, + error: accessError, + }); + return; + } + } + } + // Return credentials compatible with nsec provider finish({ secret: nsec || hexSk, persist: false }); }); From 6724166f9c286043bc68dc6f46417e5b2b71880f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:58:20 -0500 Subject: [PATCH 3/3] Add comments for login modal generation guard --- js/ui/loginModalController.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/js/ui/loginModalController.js b/js/ui/loginModalController.js index 038f7bba..b64292fd 100644 --- a/js/ui/loginModalController.js +++ b/js/ui/loginModalController.js @@ -287,7 +287,9 @@ export default class LoginModalController { this.nextRequestLoginOptionsResolver = null; this.pendingTask = null; this.modalPrepared = false; + // Track generated keys so we only create one keypair per modal session. this.generatedKeypair = null; + // Track modal close state to reset generated keys when the modal closes. this.modalCloseObserver = null; this.modalCloseIntervalId = null; @@ -313,6 +315,7 @@ export default class LoginModalController { this.template = this.resolveTemplate(); this.renderProviders(); + // Start tracking modal close events to reset per-session key generation state. this.initializeModalCloseTracking(); if (typeof this.lastRemoteSignerStatus !== "undefined") { @@ -331,10 +334,12 @@ export default class LoginModalController { const handleClose = () => { if (!this.isModalOpen()) { + // Reset generated keys once the modal is closed to allow a new keypair later. this.resetGeneratedKeypair(); } }; + // Prefer a MutationObserver, fall back to polling if needed. if (typeof MutationObserver === "function") { if (this.modalCloseObserver) { return; @@ -363,10 +368,12 @@ export default class LoginModalController { return; } + // Poll for modal close state as a fallback when observers are unavailable. this.modalCloseIntervalId = this.window.setInterval(handleClose, 500); } resetGeneratedKeypair() { + // Clear cached keypair so the next modal session generates fresh keys. this.generatedKeypair = null; } @@ -390,6 +397,7 @@ export default class LoginModalController { } this.modalPrepared = true; + // Ensure close tracking starts after modal preparation updates the element. this.initializeModalCloseTracking(); return this.modalElement instanceof HTMLElement ? this.modalElement : null; } @@ -1688,6 +1696,7 @@ export default class LoginModalController { return null; } + // Generate keys only once per modal session to avoid duplicates. if (!this.generatedKeypair) { const sk = tools.generateSecretKey(); const pk = tools.getPublicKey(sk); @@ -1760,6 +1769,7 @@ export default class LoginModalController { if (loginBtn) { loginBtn.addEventListener("click", () => { + // Block generated accounts if whitelist-only access is enforced. const accessControl = this.services?.authService?.accessControl; if (accessControl && typeof accessControl.canAccess === "function") { let canAccess = true; @@ -1776,6 +1786,7 @@ export default class LoginModalController { if (!canAccess && typeof accessControl.whitelistMode === "function") { const isWhitelistMode = accessControl.whitelistMode(); if (isWhitelistMode) { + // Surface the access error via the login error callback/notification. const accessError = new Error( "Access restricted to admins and moderators users only.", );