From bae9b29f49ff5c2c70a08f8c6c41424ce6450afb Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:38:29 -0400 Subject: [PATCH] Enforce lockdown restrictions on login --- js/accessControl.js | 9 +++++++ js/app.js | 52 ++++++++++++++++++++++++++++--------- js/services/authService.js | 46 ++++++++++++++++++++++++++++++-- tests/auth-service.test.mjs | 47 +++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 14 deletions(-) diff --git a/js/accessControl.js b/js/accessControl.js index ce883de2..4addf648 100644 --- a/js/accessControl.js +++ b/js/accessControl.js @@ -6,6 +6,7 @@ import { getWhitelistMode, setWhitelistMode as persistWhitelistMode, ADMIN_WHITELIST_MODE_STORAGE_KEY, + isLockdownMode, } from "./config.js"; import { loadAdminState, persistAdminState } from "./adminListStore.js"; import { userLogger } from "./utils/logger.js"; @@ -390,6 +391,10 @@ class AccessControl { return true; } + if (this.isLockdownActive()) { + return false; + } + if (this.whitelist.has(normalized)) { return true; } @@ -404,6 +409,10 @@ class AccessControl { return true; } + + isLockdownActive() { + return Boolean(isLockdownMode); + } } export const accessControl = new AccessControl(); diff --git a/js/app.js b/js/app.js index f2dbeb9b..f4fd10e3 100644 --- a/js/app.js +++ b/js/app.js @@ -350,6 +350,7 @@ class Application { userBlocks, relayManager, logger: devLogger, + accessControl, }); this.authEventUnsubscribes.push( this.authService.on("auth:login", (detail) => { @@ -1345,6 +1346,13 @@ class Application { await this.authService.login(savedPubKey, { persistActive: false }); } catch (error) { devLogger.error("Auto-login failed:", error); + if (error && error.code === "site-lockdown") { + const message = this.describeLoginError( + error, + "Auto-login failed. Please sign in again once lockdown ends.", + ); + this.showStatus(message, { autoHideMs: 12000 }); + } } } @@ -1991,15 +1999,35 @@ class Application { this.showSuccess("Profile added. Select it when you're ready to switch."); } catch (error) { devLogger.error("Failed to add profile via NIP-07:", error); - const message = - error && typeof error.message === "string" && error.message.trim() - ? error.message.trim() - : "Couldn't add that profile. Please try again."; + const message = this.describeLoginError( + error, + "Couldn't add that profile. Please try again.", + ); this.showError(message); } finally { setLoadingState(false); } } + + describeLoginError(error, fallbackMessage = "Failed to login. Please try again.") { + const code = + error && typeof error.code === "string" && error.code.trim() + ? error.code.trim() + : null; + if (code === "site-lockdown") { + return "This site is temporarily locked down. Only administrators may sign in right now."; + } + + if (error && typeof error.message === "string") { + const trimmed = error.message.trim(); + if (trimmed) { + return trimmed; + } + } + + return fallbackMessage; + } + canCurrentUserManageBlacklist() { const actorNpub = this.getCurrentUserNpub(); if (!actorNpub) { @@ -2307,10 +2335,10 @@ class Application { } } catch (err) { devLogger.error("[NIP-07 login error]", err); - const message = - err && typeof err.message === "string" && err.message.trim() - ? err.message.trim() - : "Failed to login with NIP-07. Please try again."; + const message = this.describeLoginError( + err, + "Failed to login with NIP-07. Please try again.", + ); this.showError(message); } finally { setLoadingState(false); @@ -2873,10 +2901,10 @@ class Application { if (this.uploadModal?.cancelCustomSubmitCooldown) { this.uploadModal.cancelCustomSubmitCooldown(); } - const message = - error && typeof error.message === "string" && error.message.trim() - ? error.message.trim() - : "Login required to publish videos."; + const message = this.describeLoginError( + error, + "Login required to publish videos.", + ); this.showError(message); } } diff --git a/js/services/authService.js b/js/services/authService.js index 22dc8ffb..2a440d8d 100644 --- a/js/services/authService.js +++ b/js/services/authService.js @@ -79,10 +79,17 @@ const FAST_PROFILE_TIMEOUT_MS = 2500; const BACKGROUND_PROFILE_TIMEOUT_MS = 6000; export default class AuthService { - constructor({ nostrClient, userBlocks, relayManager, logger } = {}) { + constructor({ + nostrClient, + userBlocks, + relayManager, + logger, + accessControl, + } = {}) { this.nostrClient = nostrClient || null; this.userBlocks = userBlocks || null; this.relayManager = relayManager || null; + this.accessControl = accessControl || null; if (typeof logger === "function") { this.logger = logger; @@ -318,6 +325,41 @@ export default class AuthService { const identityChanged = previousPubkey !== nextPubkey; + const control = this.accessControl; + const candidateNpub = this.safeEncodeNpub(nextPubkey) || null; + let lockdownActive = false; + if (control && typeof control.isLockdownActive === "function") { + try { + lockdownActive = control.isLockdownActive(); + } catch (error) { + this.log("[AuthService] accessControl.isLockdownActive threw", error); + lockdownActive = Boolean(lockdownActive); + } + } + + if (lockdownActive) { + let isAdminCandidate = false; + const adminCheckValue = + candidateNpub || (typeof trimmed === "string" && trimmed ? trimmed : null); + if (adminCheckValue && typeof control?.isAdminEditor === "function") { + try { + isAdminCandidate = !!control.isAdminEditor(adminCheckValue); + } catch (error) { + this.log("[AuthService] accessControl.isAdminEditor threw", error); + isAdminCandidate = false; + } + } + + if (!isAdminCandidate) { + const lockdownError = new Error( + "This site is temporarily locked down. Only administrators may sign in right now.", + ); + lockdownError.code = "site-lockdown"; + lockdownError.npub = candidateNpub || adminCheckValue; + throw lockdownError; + } + } + if (normalized) { setPubkey(normalized); if (this.nostrClient && typeof this.nostrClient === "object") { @@ -330,7 +372,7 @@ export default class AuthService { } } - const npub = this.safeEncodeNpub(nextPubkey); + const npub = candidateNpub; setCurrentUserNpub(npub); let savedProfilesMutated = false; diff --git a/tests/auth-service.test.mjs b/tests/auth-service.test.mjs index f11a2596..b747971c 100644 --- a/tests/auth-service.test.mjs +++ b/tests/auth-service.test.mjs @@ -172,6 +172,53 @@ await (async () => { const detail = await service.login(SAMPLE_PUBKEY); + await detail.postLoginPromise; + assert.equal(detail.postLogin, postLogin); assert.deepEqual(detail.postLogin, postLogin); })(); + +await (async () => { + resetState(); + + const adminCheckCalls = []; + const service = new AuthService({ + accessControl: { + isLockdownActive: () => true, + isAdminEditor: (npub) => { + adminCheckCalls.push(npub); + return false; + }, + }, + }); + + let error; + try { + await service.login(SAMPLE_PUBKEY); + } catch (err) { + error = err; + } + + assert(error instanceof Error); + assert.equal(error.code, "site-lockdown"); + assert.match(error.message, /locked down/i); + assert.equal(adminCheckCalls.length, 1); + assert.equal(typeof adminCheckCalls[0], "string"); + assert(adminCheckCalls[0].length > 0); +})(); + +await (async () => { + resetState(); + + const service = new AuthService({ + accessControl: { + isLockdownActive: () => true, + isAdminEditor: () => true, + }, + }); + + const detail = await service.login(SAMPLE_PUBKEY); + + assert.ok(detail); + assert.equal(detail.pubkey, SAMPLE_PUBKEY); +})();