mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add lockdown modal and defer startup until admin login
This commit is contained in:
1 parent
16dd391617
commit
ba5c01613a
2 files changed
+371
No files matched your search
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user