From f68a82f7eed8b8075d249e45c087f7ce6193b911 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:38:25 -0400 Subject: [PATCH 1/4] Fix comment composer login check for nip07 --- js/app.js | 6 +- tests/app/is-user-logged-in.test.mjs | 91 ++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 tests/app/is-user-logged-in.test.mjs diff --git a/js/app.js b/js/app.js index 48da3666..a6a5d0f8 100644 --- a/js/app.js +++ b/js/app.js @@ -3494,10 +3494,8 @@ class Application { return false; } - const sessionActorPubkey = this.normalizeHexPubkey( - nostrClient?.sessionActor?.pubkey, - ); - if (sessionActorPubkey && sessionActorPubkey === normalizedPubkey) { + const clientPubkey = this.normalizeHexPubkey(nostrClient?.pubkey); + if (clientPubkey && clientPubkey !== normalizedPubkey) { return false; } diff --git a/tests/app/is-user-logged-in.test.mjs b/tests/app/is-user-logged-in.test.mjs new file mode 100644 index 00000000..2ad3c09b --- /dev/null +++ b/tests/app/is-user-logged-in.test.mjs @@ -0,0 +1,91 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { Application } from "../../js/app.js"; +import { nostrClient } from "../../js/nostrClientFacade.js"; + +const HEX64_REGEX = /^[0-9a-f]{64}$/i; + +function createTestApp(pubkey) { + const app = { + _pubkey: pubkey, + normalizeHexPubkey(value) { + if (typeof value !== "string") { + return null; + } + + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + if (HEX64_REGEX.test(trimmed)) { + return trimmed.toLowerCase(); + } + + return null; + }, + isUserLoggedIn: Application.prototype.isUserLoggedIn, + }; + + Object.defineProperty(app, "pubkey", { + get() { + return this._pubkey; + }, + set(value) { + this._pubkey = value; + }, + }); + + return app; +} + +test("isUserLoggedIn returns false when no user pubkey is set", () => { + const previousPubkey = nostrClient.pubkey; + const previousSessionActor = nostrClient.sessionActor; + + try { + nostrClient.pubkey = null; + nostrClient.sessionActor = null; + + const app = createTestApp(null); + assert.equal(app.isUserLoggedIn(), false); + } finally { + nostrClient.pubkey = previousPubkey; + nostrClient.sessionActor = previousSessionActor; + } +}); + +test("isUserLoggedIn treats extension logins as authenticated", () => { + const previousPubkey = nostrClient.pubkey; + const previousSessionActor = nostrClient.sessionActor; + + try { + const pubkey = "893a31fa5a6481295bb76ea2dd8d5ec70640e6e6ad398e6547d771e8256af11f"; + nostrClient.pubkey = pubkey; + nostrClient.sessionActor = { pubkey }; + + const app = createTestApp(pubkey); + assert.equal(app.isUserLoggedIn(), true); + } finally { + nostrClient.pubkey = previousPubkey; + nostrClient.sessionActor = previousSessionActor; + } +}); + +test("isUserLoggedIn guards against mismatched nostrClient state", () => { + const previousPubkey = nostrClient.pubkey; + const previousSessionActor = nostrClient.sessionActor; + + try { + const pubkey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + nostrClient.pubkey = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + nostrClient.sessionActor = { pubkey }; + + const app = createTestApp(pubkey); + assert.equal(app.isUserLoggedIn(), false); + } finally { + nostrClient.pubkey = previousPubkey; + nostrClient.sessionActor = previousSessionActor; + } +}); From f6a3e8b2aeb5bf7fb090524325a82d29122b10bb Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:56:08 -0400 Subject: [PATCH 2/4] Ensure nostr session actor matches login pubkey --- js/app.js | 7 +++++++ tests/app/is-user-logged-in.test.mjs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/js/app.js b/js/app.js index a6a5d0f8..322db86a 100644 --- a/js/app.js +++ b/js/app.js @@ -3499,6 +3499,13 @@ class Application { return false; } + const sessionActorPubkey = this.normalizeHexPubkey( + nostrClient?.sessionActor?.pubkey, + ); + if (sessionActorPubkey && sessionActorPubkey !== normalizedPubkey) { + return false; + } + return true; } diff --git a/tests/app/is-user-logged-in.test.mjs b/tests/app/is-user-logged-in.test.mjs index 2ad3c09b..82ee98f6 100644 --- a/tests/app/is-user-logged-in.test.mjs +++ b/tests/app/is-user-logged-in.test.mjs @@ -89,3 +89,22 @@ test("isUserLoggedIn guards against mismatched nostrClient state", () => { nostrClient.sessionActor = previousSessionActor; } }); + +test("isUserLoggedIn rejects mismatched session actor pubkeys", () => { + const previousPubkey = nostrClient.pubkey; + const previousSessionActor = nostrClient.sessionActor; + + try { + const pubkey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + nostrClient.pubkey = pubkey; + nostrClient.sessionActor = { + pubkey: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + }; + + const app = createTestApp(pubkey); + assert.equal(app.isUserLoggedIn(), false); + } finally { + nostrClient.pubkey = previousPubkey; + nostrClient.sessionActor = previousSessionActor; + } +}); From 7985a481a435300aff3060e72349c87d427fcbce Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:08:13 -0400 Subject: [PATCH 3/4] Allow anonymous session actors when checking login state --- js/app.js | 22 +++++++++++++++---- tests/app/is-user-logged-in.test.mjs | 32 +++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/js/app.js b/js/app.js index 322db86a..3d4f5086 100644 --- a/js/app.js +++ b/js/app.js @@ -3499,11 +3499,25 @@ class Application { return false; } - const sessionActorPubkey = this.normalizeHexPubkey( - nostrClient?.sessionActor?.pubkey, - ); + const sessionActor = nostrClient?.sessionActor || null; + const sessionActorPubkey = this.normalizeHexPubkey(sessionActor?.pubkey); if (sessionActorPubkey && sessionActorPubkey !== normalizedPubkey) { - return false; + const rawPrivateKey = + typeof sessionActor?.privateKey === "string" + ? sessionActor.privateKey.trim() + : ""; + const hasEmbeddedPrivateKey = + rawPrivateKey.length > 0 && HEX64_REGEX.test(rawPrivateKey); + + const declaredSource = + typeof sessionActor?.source === "string" + ? sessionActor.source.trim() + : ""; + const isPersisted = sessionActor?.persisted === true; + + if (!hasEmbeddedPrivateKey || declaredSource || isPersisted) { + return false; + } } return true; diff --git a/tests/app/is-user-logged-in.test.mjs b/tests/app/is-user-logged-in.test.mjs index 82ee98f6..84d503d6 100644 --- a/tests/app/is-user-logged-in.test.mjs +++ b/tests/app/is-user-logged-in.test.mjs @@ -78,8 +78,8 @@ test("isUserLoggedIn guards against mismatched nostrClient state", () => { const previousSessionActor = nostrClient.sessionActor; try { - const pubkey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - nostrClient.pubkey = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + const pubkey = "a".repeat(64); + nostrClient.pubkey = "b".repeat(64); nostrClient.sessionActor = { pubkey }; const app = createTestApp(pubkey); @@ -90,15 +90,37 @@ test("isUserLoggedIn guards against mismatched nostrClient state", () => { } }); -test("isUserLoggedIn rejects mismatched session actor pubkeys", () => { +test("isUserLoggedIn ignores anonymous session actor mismatches", () => { const previousPubkey = nostrClient.pubkey; const previousSessionActor = nostrClient.sessionActor; try { - const pubkey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const pubkey = "c".repeat(64); nostrClient.pubkey = pubkey; nostrClient.sessionActor = { - pubkey: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + pubkey: "d".repeat(64), + privateKey: "e".repeat(64), + createdAt: Date.now(), + }; + + const app = createTestApp(pubkey); + assert.equal(app.isUserLoggedIn(), true); + } finally { + nostrClient.pubkey = previousPubkey; + nostrClient.sessionActor = previousSessionActor; + } +}); + +test("isUserLoggedIn rejects mismatched managed session actors", () => { + const previousPubkey = nostrClient.pubkey; + const previousSessionActor = nostrClient.sessionActor; + + try { + const pubkey = "f".repeat(64); + nostrClient.pubkey = pubkey; + nostrClient.sessionActor = { + pubkey: "0".repeat(64), + source: "nsec", }; const app = createTestApp(pubkey); From f3f5d59a5d497d53ac183815b501266714afb887 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:23:49 -0400 Subject: [PATCH 4/4] Fix sidebar upload button hydration --- js/app.js | 41 +++ js/ui/appChromeController.js | 247 ++++++++++++------ tests/app/hydrate-sidebar-navigation.test.mjs | 96 +++++++ tests/ui/app-chrome-controller.test.mjs | 58 ++++ 4 files changed, 363 insertions(+), 79 deletions(-) create mode 100644 tests/app/hydrate-sidebar-navigation.test.mjs create mode 100644 tests/ui/app-chrome-controller.test.mjs diff --git a/js/app.js b/js/app.js index 3d4f5086..e747d190 100644 --- a/js/app.js +++ b/js/app.js @@ -3607,6 +3607,34 @@ class Application { } } + refreshChromeElements() { + const doc = typeof document !== "undefined" ? document : null; + const findElement = (id) => { + if (!doc || typeof doc.getElementById !== "function") { + return null; + } + const element = doc.getElementById(id); + if (!element || typeof element.addEventListener !== "function") { + return null; + } + return element; + }; + + this.loginButton = findElement("loginButton"); + this.logoutButton = findElement("logoutButton"); + this.uploadButton = findElement("uploadButton"); + this.profileButton = findElement("profileButton"); + this.closeLoginModalBtn = findElement("closeLoginModal"); + + return { + loginButton: this.loginButton, + logoutButton: this.logoutButton, + uploadButton: this.uploadButton, + profileButton: this.profileButton, + closeLoginModalButton: this.closeLoginModalBtn, + }; + } + resolveSubscriptionsLink() { if (this.subscriptionsLink instanceof HTMLElement) { return this.subscriptionsLink; @@ -3623,7 +3651,20 @@ class Application { } hydrateSidebarNavigation() { + const chromeElements = this.refreshChromeElements(); this.resolveSubscriptionsLink(); + + if (this.appChromeController) { + if (typeof this.appChromeController.setElements === "function") { + this.appChromeController.setElements(chromeElements); + } else { + if (this.appChromeController.elements) { + Object.assign(this.appChromeController.elements, chromeElements); + } + this.appChromeController.initialize(); + } + } + this.syncAuthUiState(); } diff --git a/js/ui/appChromeController.js b/js/ui/appChromeController.js index f8cb7467..1960bd20 100644 --- a/js/ui/appChromeController.js +++ b/js/ui/appChromeController.js @@ -16,11 +16,13 @@ export default class AppChromeController { logger, } = {}) { this.elements = { - logoutButton: elements.logoutButton || null, - profileButton: elements.profileButton || null, - uploadButton: elements.uploadButton || null, - loginButton: elements.loginButton || null, - closeLoginModalButton: elements.closeLoginModalButton || null, + logoutButton: this.normalizeElement(elements.logoutButton), + profileButton: this.normalizeElement(elements.profileButton), + uploadButton: this.normalizeElement(elements.uploadButton), + loginButton: this.normalizeElement(elements.loginButton), + closeLoginModalButton: this.normalizeElement( + elements.closeLoginModalButton, + ), }; this.callbacks = { @@ -59,42 +61,17 @@ export default class AppChromeController { this.logger = logger || defaultLogger; this.isInitialized = false; + this.hasBoundGlobalEvents = false; - this.handleBeforeUnload = this.handleBeforeUnload.bind(this); - this.handleVisibilityChange = this.handleVisibilityChange.bind(this); - this.handlePopstate = this.handlePopstate.bind(this); - this.handleDocumentClick = this.handleDocumentClick.bind(this); - } + this.boundElements = { + logoutButton: null, + profileButton: null, + uploadButton: null, + loginButton: null, + closeLoginModalButton: null, + }; - initialize() { - if (this.isInitialized) { - return; - } - - this.bindLogoutButton(); - this.bindProfileButton(); - this.bindUploadButton(); - this.bindLoginButton(); - this.bindCloseLoginButton(); - this.bindGlobalEvents(); - - this.isInitialized = true; - } - - getTriggerFromEvent(event) { - if (!event) { - return null; - } - return event.currentTarget || event.target || null; - } - - bindLogoutButton() { - const { logoutButton } = this.elements; - if (!logoutButton || typeof logoutButton.addEventListener !== "function") { - return; - } - - logoutButton.addEventListener("click", async () => { + this.handleLogoutClick = async () => { try { if (typeof this.callbacks.requestLogout === "function") { await this.callbacks.requestLogout(); @@ -107,16 +84,9 @@ export default class AppChromeController { this.callbacks.showError(DEFAULT_LOGOUT_ERROR_MESSAGE); } } - }); - } + }; - bindProfileButton() { - const { profileButton } = this.elements; - if (!profileButton || typeof profileButton.addEventListener !== "function") { - return; - } - - profileButton.addEventListener("click", () => { + this.handleProfileClick = () => { if (typeof this.callbacks.showProfileModal !== "function") { return; } @@ -128,19 +98,13 @@ export default class AppChromeController { this.logger.error("Failed to open profile modal:", error); } }); - }); - } + }; - bindUploadButton() { - const { uploadButton } = this.elements; - if (!uploadButton || typeof uploadButton.addEventListener !== "function") { - return; - } - - uploadButton.addEventListener("click", (event) => { + this.handleUploadClick = (event) => { if (typeof this.callbacks.openUploadModal !== "function") { return; } + const triggerElement = this.getTriggerFromEvent(event); try { this.callbacks.openUploadModal({ triggerElement }); @@ -149,16 +113,9 @@ export default class AppChromeController { this.logger.error("Failed to open upload modal:", error); } } - }); - } + }; - bindLoginButton() { - const { loginButton } = this.elements; - if (!loginButton || typeof loginButton.addEventListener !== "function") { - return; - } - - loginButton.addEventListener("click", (event) => { + this.handleLoginClick = (event) => { if (this.logger?.log) { this.logger.log("Login button clicked!"); } @@ -178,19 +135,9 @@ export default class AppChromeController { this.callbacks.onLoginModalOpened(); } } - }); - } + }; - bindCloseLoginButton() { - const { closeLoginModalButton } = this.elements; - if ( - !closeLoginModalButton || - typeof closeLoginModalButton.addEventListener !== "function" - ) { - return; - } - - closeLoginModalButton.addEventListener("click", () => { + this.handleCloseLoginClick = () => { if (this.logger?.log) { this.logger.log("[app.js] closeLoginModal button clicked!"); } @@ -201,7 +148,149 @@ export default class AppChromeController { this.callbacks.onLoginModalClosed(); } } - }); + }; + + this.handleBeforeUnload = this.handleBeforeUnload.bind(this); + this.handleVisibilityChange = this.handleVisibilityChange.bind(this); + this.handlePopstate = this.handlePopstate.bind(this); + this.handleDocumentClick = this.handleDocumentClick.bind(this); + } + + initialize() { + this.bindLogoutButton(); + this.bindProfileButton(); + this.bindUploadButton(); + this.bindLoginButton(); + this.bindCloseLoginButton(); + + if (!this.hasBoundGlobalEvents) { + this.bindGlobalEvents(); + this.hasBoundGlobalEvents = true; + } + + this.isInitialized = true; + } + + getTriggerFromEvent(event) { + if (!event) { + return null; + } + return event.currentTarget || event.target || null; + } + + bindLogoutButton() { + const button = this.normalizeElement(this.elements.logoutButton); + if (this.boundElements.logoutButton === button) { + return; + } + + this.detachBoundElement("logoutButton", this.handleLogoutClick); + + if (button) { + button.addEventListener("click", this.handleLogoutClick); + this.boundElements.logoutButton = button; + } + } + + bindProfileButton() { + const button = this.normalizeElement(this.elements.profileButton); + if (this.boundElements.profileButton === button) { + return; + } + + this.detachBoundElement("profileButton", this.handleProfileClick); + + if (button) { + button.addEventListener("click", this.handleProfileClick); + this.boundElements.profileButton = button; + } + } + + bindUploadButton() { + const button = this.normalizeElement(this.elements.uploadButton); + if (this.boundElements.uploadButton === button) { + return; + } + + this.detachBoundElement("uploadButton", this.handleUploadClick); + + if (button) { + button.addEventListener("click", this.handleUploadClick); + this.boundElements.uploadButton = button; + } + } + + bindLoginButton() { + const button = this.normalizeElement(this.elements.loginButton); + if (this.boundElements.loginButton === button) { + return; + } + + this.detachBoundElement("loginButton", this.handleLoginClick); + + if (button) { + button.addEventListener("click", this.handleLoginClick); + this.boundElements.loginButton = button; + } + } + + bindCloseLoginButton() { + const button = this.normalizeElement(this.elements.closeLoginModalButton); + if (this.boundElements.closeLoginModalButton === button) { + return; + } + + this.detachBoundElement("closeLoginModalButton", this.handleCloseLoginClick); + + if (button) { + button.addEventListener("click", this.handleCloseLoginClick); + this.boundElements.closeLoginModalButton = button; + } + } + + setElements(nextElements = {}) { + if (!nextElements || typeof nextElements !== "object") { + return; + } + + const keys = [ + "logoutButton", + "profileButton", + "uploadButton", + "loginButton", + "closeLoginModalButton", + ]; + + let shouldRebind = false; + + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(nextElements, key)) { + const normalized = this.normalizeElement(nextElements[key]); + if (this.elements[key] !== normalized) { + this.elements[key] = normalized; + shouldRebind = true; + } + } + } + + if (shouldRebind) { + this.initialize(); + } + } + + detachBoundElement(key, handler) { + const previous = this.boundElements[key]; + if (previous && typeof previous.removeEventListener === "function") { + previous.removeEventListener("click", handler); + } + this.boundElements[key] = null; + } + + normalizeElement(element) { + if (!element || typeof element.addEventListener !== "function") { + return null; + } + return element; } bindGlobalEvents() { diff --git a/tests/app/hydrate-sidebar-navigation.test.mjs b/tests/app/hydrate-sidebar-navigation.test.mjs new file mode 100644 index 00000000..139e7e51 --- /dev/null +++ b/tests/app/hydrate-sidebar-navigation.test.mjs @@ -0,0 +1,96 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { JSDOM } from "jsdom"; + +import { Application } from "../../js/app.js"; + +const VIEW_HTML = ` + +
+