Add lockdown modal and defer startup until admin login

This commit is contained in:
thePR0M3TH3AN committed 2025-10-20 15:57:28 -04:00
1 parent 16dd391617
commit ba5c01613a
2 files changed
+371

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
<div
id="lockdownModal"
class="bv-modal hidden items-center justify-center"
role="dialog"
aria-modal="true"
aria-labelledby="lockdownModalTitle"
aria-describedby="lockdownModalDescription"
data-ds="new"
>
<div class="bv-modal-backdrop"></div>
<div class="modal-sheet max-h-modal-shell overflow-hidden" tabindex="-1">
<div class="modal-body space-y-8 text-center">
<div class="flex justify-center">
<span class="inline-flex h-16 items-center text-text bv-logo" role="img" aria-label="bitvid logo">
<svg class="h-full w-auto" viewBox="0 0 318.32 108.98" aria-hidden="true" focusable="false">
<g class="bv-logo__accent">
<rect x="7.1" y="7.8" width="5.84" height="5.48" />
<rect x="7.1" y="18.78" width="24.02" height="5.48" />
<rect x="7.1" y="29.77" width="42.85" height="5.48" />
<rect x="7.1" y="40.76" width="61.68" height="5.48" />
<rect x="7.1" y="51.75" width="80.88" height="5.48" />
<rect x="7.1" y="62.74" width="61.68" height="5.48" />
<rect x="7.1" y="73.73" width="42.85" height="5.48" />
<rect x="7.1" y="84.72" width="24.02" height="5.48" />
<rect x="7.1" y="95.71" width="5.84" height="5.48" />
</g>
<g class="bv-logo__wordmark">
<path d="M121.28 25.08v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V25.08h5.83Z" />
<path d="M168.53 25.08v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M191.36 25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V25.08h5.83Z" />
<path d="M212.88 42.24l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M264.05 25.08v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83v-41.67h5.83Z" />
<path d="M311.21 25.08v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V25.08h5.83Z" />
</g>
</svg>
</span>
</div>
<div id="lockdownModalDescription" class="space-y-4">
<h2 id="lockdownModalTitle" class="text-2xl font-semibold tracking-tight text-text">
bitvid is in lockdown mode
</h2>
<p class="text-base leading-relaxed text-text-frosted">
Only approved administrators and moderators can access the app while lockdown is active. Sign in with an authorized Nostr profile to continue.
</p>
<p class="text-sm text-text-muted">
You will be prompted by your NIP-07 browser extension. Make sure it is installed, unlocked, and ready to approve the request.
</p>
</div>
<div class="space-y-2">
<p
id="lockdownStatusMessage"
class="text-sm text-text"
data-lockdown-status
aria-live="polite"
>
Administrator access is required to proceed.
</p>
<p
id="lockdownErrorMessage"
class="hidden text-sm text-critical"
data-lockdown-error
role="alert"
></p>
</div>
<button
id="lockdownLoginButton"
class="btn w-full"
data-variant="danger"
data-lockdown-login
type="button"
>
Login with Nostr
</button>
</div>
</div>
</div>
+291
View File
@@ -26,7 +26,13 @@ import {
GITHUB_URL,
BETA_URL,
DNS_URL,
isLockdownMode,
} from "./config.js";
import AuthService from "./services/authService.js";
import { accessControl } from "./accessControl.js";
import { nostrClient } from "./nostr.js";
import { userBlocks } from "./userBlocks.js";
import { relayManager } from "./relayManager.js";
validateInstanceConfig();
@@ -36,6 +42,15 @@ initThemeController();
let application = null;
let applicationReadyPromise = Promise.resolve();
const LOCKDOWN_MODAL_ID = "lockdownModal";
const LOCKDOWN_LOGIN_BUTTON_ID = "lockdownLoginButton";
const LOCKDOWN_STATUS_ID = "lockdownStatusMessage";
const LOCKDOWN_ERROR_ID = "lockdownErrorMessage";
let lockdownAuthService = null;
let lockdownAccessControlPromise = null;
let lockdownResumePromise = null;
const bindOptionalExternalLink = ({ selector, url, label }) => {
const element = document.querySelector(selector);
const sanitizedUrl = typeof url === "string" ? url.trim() : "";
@@ -59,6 +74,275 @@ const bindOptionalExternalLink = ({ selector, url, label }) => {
element.rel = "noopener noreferrer";
};
function ensureLockdownAccessControlReady() {
if (lockdownAccessControlPromise) {
return lockdownAccessControlPromise;
}
lockdownAccessControlPromise = accessControl
.refresh()
.then(
() => true,
(error) => {
devLogger.error(
"[Lockdown] Failed to refresh admin lists before login attempt:",
error,
);
return false;
},
);
return lockdownAccessControlPromise;
}
function getLockdownAuthService() {
if (lockdownAuthService) {
return lockdownAuthService;
}
const service = new AuthService({
nostrClient,
userBlocks,
relayManager,
logger: devLogger,
accessControl,
});
try {
service.hydrateFromStorage();
} catch (error) {
devLogger.warn("[Lockdown] Failed to hydrate auth state from storage:", error);
}
lockdownAuthService = service;
return service;
}
function setLockdownStatusMessage(message) {
const statusNode = document.getElementById(LOCKDOWN_STATUS_ID);
if (!(statusNode instanceof HTMLElement)) {
return;
}
const text = typeof message === "string" ? message.trim() : "";
if (text) {
statusNode.textContent = text;
statusNode.classList.remove("hidden");
} else {
statusNode.textContent = "";
statusNode.classList.add("hidden");
}
}
function setLockdownErrorMessage(message) {
const errorNode = document.getElementById(LOCKDOWN_ERROR_ID);
if (!(errorNode instanceof HTMLElement)) {
return;
}
const text = typeof message === "string" ? message.trim() : "";
if (text) {
errorNode.textContent = text;
errorNode.classList.remove("hidden");
} else {
errorNode.textContent = "";
errorNode.classList.add("hidden");
}
}
function resetLockdownError() {
setLockdownErrorMessage("");
}
function setLockdownLoginBusy(isBusy) {
const button = document.getElementById(LOCKDOWN_LOGIN_BUTTON_ID);
if (!(button instanceof HTMLButtonElement)) {
return;
}
const busy = Boolean(isBusy);
button.disabled = busy;
if (busy) {
button.setAttribute("aria-busy", "true");
} else {
button.removeAttribute("aria-busy");
}
}
async function tryAutoResumeLockdownSession() {
const authService = getLockdownAuthService();
if (!authService || typeof authService.getActivePubkey !== "function") {
return false;
}
const savedPubkey = authService.getActivePubkey();
if (!savedPubkey) {
return false;
}
resetLockdownError();
setLockdownLoginBusy(true);
setLockdownStatusMessage("Restoring your previous session…");
try {
await ensureLockdownAccessControlReady();
await authService.login(savedPubkey, { persistActive: false });
setLockdownStatusMessage("Administrator session restored. Loading bitvid…");
await resumeLockdownStartup();
return true;
} catch (error) {
devLogger.warn("[Lockdown] Stored session failed to resume:", error);
if (error && typeof error === "object") {
if (error.code === "site-lockdown") {
setLockdownErrorMessage(
"Stored credentials are no longer permitted during lockdown. Please sign in with an authorized administrator account.",
);
} else if (typeof error.message === "string" && error.message.trim()) {
setLockdownErrorMessage(error.message.trim());
}
}
return false;
} finally {
if (!lockdownResumePromise) {
setLockdownLoginBusy(false);
setLockdownStatusMessage("Administrator access is required to proceed.");
}
}
}
function resumeLockdownStartup() {
if (lockdownResumePromise) {
return lockdownResumePromise;
}
const modal = document.getElementById(LOCKDOWN_MODAL_ID);
const loginButton = document.getElementById(LOCKDOWN_LOGIN_BUTTON_ID);
if (loginButton instanceof HTMLButtonElement) {
loginButton.removeEventListener("click", handleLockdownLoginClick);
}
if (modal) {
closeStaticModal(modal);
}
lockdownResumePromise = Promise.resolve()
.then(() => initializeInterface())
.catch((error) => {
lockdownResumePromise = null;
userLogger.error("Failed to start bitvid after lockdown login:", error);
if (modal) {
openStaticModal(modal);
}
setLockdownLoginBusy(false);
setLockdownStatusMessage("Administrator access is required to proceed.");
setLockdownErrorMessage(
"We couldn't start the application. Please try again.",
);
throw error;
});
return lockdownResumePromise;
}
async function handleLockdownLoginClick(event) {
event.preventDefault();
resetLockdownError();
setLockdownStatusMessage("Requesting authorization from your Nostr extension…");
setLockdownLoginBusy(true);
let shouldReleaseButton = true;
try {
await ensureLockdownAccessControlReady();
const authService = getLockdownAuthService();
const result = await authService.requestLogin({ allowAccountSelection: true });
const pubkey =
result && typeof result === "object" && typeof result.pubkey === "string"
? result.pubkey
: null;
if (!pubkey) {
setLockdownStatusMessage(
"No account was selected. Choose an authorized administrator profile to continue.",
);
return;
}
setLockdownStatusMessage("Login successful. Loading bitvid…");
shouldReleaseButton = false;
await resumeLockdownStartup();
return;
} catch (error) {
userLogger.error("Lockdown login failed:", error);
let message = "Failed to login. Please try again.";
if (error && typeof error === "object") {
if (error.code === "site-lockdown") {
message =
"Only administrators and moderators may sign in during lockdown. Try another account.";
} else if (error.code === "extension-permission-denied") {
message =
"The NIP-07 extension denied the permission request required to finish logging in.";
} else if (typeof error.message === "string" && error.message.trim()) {
message = error.message.trim();
}
}
setLockdownErrorMessage(message);
setLockdownStatusMessage("Administrator access is required to proceed.");
} finally {
if (shouldReleaseButton) {
setLockdownLoginBusy(false);
}
}
}
async function prepareLockdownInterface() {
ensureLockdownAccessControlReady();
try {
await loadModal("components/lockdown-modal.html");
} catch (error) {
userLogger.error("Failed to load lockdown modal markup:", error);
throw error;
}
const modal =
prepareStaticModal({ id: LOCKDOWN_MODAL_ID }) ||
document.getElementById(LOCKDOWN_MODAL_ID);
if (!(modal instanceof HTMLElement)) {
throw new Error("Lockdown modal markup is missing after load.");
}
const loginButton = modal.querySelector("[data-lockdown-login]");
if (loginButton instanceof HTMLButtonElement) {
loginButton.addEventListener("click", handleLockdownLoginClick);
} else {
devLogger.warn(
"[Lockdown] Login button not found inside lockdown modal. Users cannot sign in.",
);
}
openStaticModal(modal);
resetLockdownError();
setLockdownStatusMessage("Administrator access is required to proceed.");
setLockdownLoginBusy(false);
const autoResumed = await tryAutoResumeLockdownSession();
if (autoResumed) {
return;
}
if (loginButton instanceof HTMLButtonElement && typeof window !== "undefined") {
window.requestAnimationFrame(() => {
try {
loginButton.focus();
} catch (error) {
devLogger.warn("[Lockdown] Failed to focus lockdown login button:", error);
}
});
}
}
function startApplication() {
if (application) {
return applicationReadyPromise;
@@ -713,6 +997,13 @@ async function initializeInterface() {
}
function onDomReady() {
if (isLockdownMode) {
prepareLockdownInterface().catch((error) => {
userLogger.error("Failed to initialize lockdown interface:", error);
});
return;
}
initializeInterface().catch((error) => {
userLogger.error("Unhandled error during bitvid initialization:", error);
});