mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-31 01:42:27 +00:00
Add admin governance roles and UI
This commit is contained in:
1 parent
2eb00740fd
commit
875cee4428
6 files changed
+1086
-227
No files matched your search
+296
-190
@@ -1,228 +1,334 @@
|
||||
// js/accessControl.js
|
||||
|
||||
import { isDevMode, isWhitelistEnabled } from "./config.js";
|
||||
import { initialWhitelist, initialBlacklist } from "./lists.js";
|
||||
import {
|
||||
ADMIN_EDITORS_NPUBS,
|
||||
ADMIN_SUPER_NPUB,
|
||||
getWhitelistMode,
|
||||
setWhitelistMode as persistWhitelistMode,
|
||||
ADMIN_WHITELIST_MODE_STORAGE_KEY,
|
||||
} from "./config.js";
|
||||
import {
|
||||
ADMIN_INITIAL_BLACKLIST,
|
||||
ADMIN_INITIAL_WHITELIST,
|
||||
} from "./lists.js";
|
||||
|
||||
class AccessControl {
|
||||
constructor() {
|
||||
// Debug logging for initialization
|
||||
console.log("DEBUG: AccessControl constructor called");
|
||||
const ADMIN_EDITORS_KEY = "bitvid_admin_editors";
|
||||
const ADMIN_WHITELIST_KEY = "bitvid_admin_whitelist";
|
||||
const ADMIN_BLACKLIST_KEY = "bitvid_admin_blacklist";
|
||||
|
||||
const { data: storedWhitelist, status: whitelistStatus } = this.loadWhitelist();
|
||||
const { data: storedBlacklist, status: blacklistStatus } = this.loadBlacklist();
|
||||
const LEGACY_WHITELIST_KEY = "bitvid_whitelist";
|
||||
const LEGACY_BLACKLIST_KEY = "bitvid_blacklist";
|
||||
|
||||
if (storedWhitelist !== null) {
|
||||
this.whitelist = new Set(storedWhitelist);
|
||||
} else {
|
||||
this.whitelist = new Set(initialWhitelist);
|
||||
this.saveWhitelist();
|
||||
if (whitelistStatus && whitelistStatus !== "missing") {
|
||||
console.warn(
|
||||
`Whitelist storage ${whitelistStatus}. Falling back to initial whitelist.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (storedBlacklist !== null) {
|
||||
this.blacklist = new Set(storedBlacklist);
|
||||
} else {
|
||||
this.blacklist = new Set(initialBlacklist.filter((x) => x)); // Filter out empty strings
|
||||
this.saveBlacklist();
|
||||
if (blacklistStatus && blacklistStatus !== "missing") {
|
||||
console.warn(
|
||||
`Blacklist storage ${blacklistStatus}. Falling back to initial blacklist.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Debug the sets
|
||||
console.log("DEBUG: Whitelist after initialization:", [...this.whitelist]);
|
||||
console.log("DEBUG: Blacklist after initialization:", [...this.blacklist]);
|
||||
}
|
||||
|
||||
// Rest of the class remains the same...
|
||||
loadWhitelist() {
|
||||
try {
|
||||
const stored = localStorage.getItem("bitvid_whitelist");
|
||||
if (!stored) {
|
||||
if (isDevMode) console.log("No stored whitelist found in localStorage.");
|
||||
return { data: null, status: "missing" };
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(stored);
|
||||
const sanitized = this.sanitizeList(parsed, "whitelist");
|
||||
return {
|
||||
data: sanitized,
|
||||
status: sanitized === null ? "invalid" : "ok",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error loading whitelist, using defaults:", error);
|
||||
return { data: null, status: "error" };
|
||||
}
|
||||
}
|
||||
|
||||
loadBlacklist() {
|
||||
try {
|
||||
const stored = localStorage.getItem("bitvid_blacklist");
|
||||
if (!stored) {
|
||||
if (isDevMode) console.log("No stored blacklist found in localStorage.");
|
||||
return { data: null, status: "missing" };
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(stored);
|
||||
const sanitized = this.sanitizeList(parsed, "blacklist");
|
||||
return {
|
||||
data: sanitized,
|
||||
status: sanitized === null ? "invalid" : "ok",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error loading blacklist, using defaults:", error);
|
||||
return { data: null, status: "error" };
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeList(list, listName) {
|
||||
if (!Array.isArray(list)) {
|
||||
console.warn(
|
||||
`Stored ${listName} is not an array. Received:`,
|
||||
list
|
||||
);
|
||||
function loadJSONList(key) {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw === null) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : null;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse list for ${key}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const sanitized = [];
|
||||
let invalidEntries = 0;
|
||||
function saveJSONList(key, values) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(values));
|
||||
} catch (error) {
|
||||
console.warn(`Failed to persist list for ${key}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
list.forEach((value) => {
|
||||
if (typeof value !== "string") {
|
||||
invalidEntries += 1;
|
||||
return;
|
||||
function normalizeNpub(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function dedupeNpubs(values) {
|
||||
const normalized = Array.isArray(values) ? values.map(normalizeNpub) : [];
|
||||
return Array.from(
|
||||
normalized.reduce((set, npub) => {
|
||||
if (npub) {
|
||||
set.add(npub);
|
||||
}
|
||||
return set;
|
||||
}, new Set())
|
||||
);
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (trimmed) {
|
||||
sanitized.push(trimmed);
|
||||
} else {
|
||||
invalidEntries += 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (invalidEntries > 0) {
|
||||
console.warn(
|
||||
`Stored ${listName} contained ${invalidEntries} invalid entr${
|
||||
invalidEntries === 1 ? "y" : "ies"
|
||||
}. Sanitized list:`,
|
||||
sanitized
|
||||
);
|
||||
}
|
||||
function migrateLegacyList(targetKey, legacyKey, fallback) {
|
||||
const stored = loadJSONList(targetKey);
|
||||
if (stored !== null) {
|
||||
return dedupeNpubs(stored);
|
||||
}
|
||||
|
||||
const legacy = legacyKey ? loadJSONList(legacyKey) : null;
|
||||
if (legacy !== null) {
|
||||
const sanitized = dedupeNpubs(legacy);
|
||||
saveJSONList(targetKey, sanitized);
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
saveWhitelist() {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"bitvid_whitelist",
|
||||
JSON.stringify([...this.whitelist])
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error saving whitelist:", error);
|
||||
}
|
||||
const sanitizedFallback = dedupeNpubs(fallback);
|
||||
if (sanitizedFallback.length) {
|
||||
saveJSONList(targetKey, sanitizedFallback);
|
||||
}
|
||||
return sanitizedFallback;
|
||||
}
|
||||
|
||||
function isValidNpub(npub) {
|
||||
if (typeof npub !== "string") {
|
||||
return false;
|
||||
}
|
||||
const trimmed = npub.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
saveBlacklist() {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"bitvid_blacklist",
|
||||
JSON.stringify([...this.blacklist])
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error saving blacklist:", error);
|
||||
}
|
||||
try {
|
||||
const decoded = window?.NostrTools?.nip19?.decode(trimmed);
|
||||
return decoded?.type === "npub";
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class AccessControl {
|
||||
constructor() {
|
||||
this.editors = new Set();
|
||||
this.whitelist = new Set();
|
||||
this.blacklist = new Set();
|
||||
this.whitelistEnabled = getWhitelistMode();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
addToWhitelist(npub) {
|
||||
if (!this.isValidNpub(npub)) {
|
||||
throw new Error("Invalid npub format");
|
||||
}
|
||||
this.whitelist.add(npub);
|
||||
this.saveWhitelist();
|
||||
if (isDevMode) console.log(`Added ${npub} to whitelist`);
|
||||
refresh() {
|
||||
this.editors = new Set(
|
||||
dedupeNpubs([
|
||||
...ADMIN_EDITORS_NPUBS,
|
||||
...migrateLegacyList(ADMIN_EDITORS_KEY, null, []),
|
||||
])
|
||||
);
|
||||
|
||||
this.whitelist = new Set(
|
||||
migrateLegacyList(ADMIN_WHITELIST_KEY, LEGACY_WHITELIST_KEY, [
|
||||
...ADMIN_INITIAL_WHITELIST,
|
||||
])
|
||||
);
|
||||
|
||||
this.blacklist = new Set(
|
||||
migrateLegacyList(ADMIN_BLACKLIST_KEY, LEGACY_BLACKLIST_KEY, [
|
||||
...ADMIN_INITIAL_BLACKLIST,
|
||||
])
|
||||
);
|
||||
|
||||
this.whitelistEnabled = getWhitelistMode();
|
||||
}
|
||||
|
||||
removeFromWhitelist(npub) {
|
||||
this.whitelist.delete(npub);
|
||||
this.saveWhitelist();
|
||||
if (isDevMode) console.log(`Removed ${npub} from whitelist`);
|
||||
whitelistMode() {
|
||||
return this.whitelistEnabled;
|
||||
}
|
||||
|
||||
addToBlacklist(npub) {
|
||||
if (!this.isValidNpub(npub)) {
|
||||
throw new Error("Invalid npub format");
|
||||
}
|
||||
this.blacklist.add(npub);
|
||||
this.saveBlacklist();
|
||||
if (isDevMode) console.log(`Added ${npub} to blacklist`);
|
||||
isSuperAdmin(npub) {
|
||||
const normalized = normalizeNpub(npub);
|
||||
return normalized ? normalized === ADMIN_SUPER_NPUB : false;
|
||||
}
|
||||
|
||||
removeFromBlacklist(npub) {
|
||||
this.blacklist.delete(npub);
|
||||
this.saveBlacklist();
|
||||
if (isDevMode) console.log(`Removed ${npub} from blacklist`);
|
||||
}
|
||||
|
||||
isWhitelisted(npub) {
|
||||
const result = this.whitelist.has(npub);
|
||||
if (isDevMode)
|
||||
console.log(
|
||||
`Checking if ${npub} is whitelisted:`,
|
||||
result,
|
||||
"Current whitelist:",
|
||||
[...this.whitelist]
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
isBlacklisted(npub) {
|
||||
return this.blacklist.has(npub);
|
||||
}
|
||||
|
||||
canAccess(npub) {
|
||||
if (this.isBlacklisted(npub)) {
|
||||
isAdminEditor(npub) {
|
||||
const normalized = normalizeNpub(npub);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const canAccess = !isWhitelistEnabled || this.isWhitelisted(npub);
|
||||
if (isDevMode) console.log(`Checking access for ${npub}:`, canAccess);
|
||||
return canAccess;
|
||||
}
|
||||
|
||||
filterVideos(videos) {
|
||||
return videos.filter((video) => {
|
||||
try {
|
||||
const npub = window.NostrTools.nip19.npubEncode(video.pubkey);
|
||||
return !this.isBlacklisted(npub);
|
||||
} catch (error) {
|
||||
console.error("Error filtering video:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isValidNpub(npub) {
|
||||
try {
|
||||
return npub.startsWith("npub1") && npub.length === 63;
|
||||
} catch (error) {
|
||||
return false;
|
||||
if (this.isSuperAdmin(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return this.editors.has(normalized);
|
||||
}
|
||||
|
||||
canEditAdminLists(npub) {
|
||||
return this.isAdminEditor(npub);
|
||||
}
|
||||
|
||||
getWhitelist() {
|
||||
return [...this.whitelist];
|
||||
return Array.from(this.whitelist);
|
||||
}
|
||||
|
||||
getBlacklist() {
|
||||
return [...this.blacklist];
|
||||
return Array.from(this.blacklist);
|
||||
}
|
||||
|
||||
getEditors() {
|
||||
return Array.from(this.editors);
|
||||
}
|
||||
|
||||
addModerator(requestorNpub, moderatorNpub) {
|
||||
if (!this.isSuperAdmin(requestorNpub)) {
|
||||
return { ok: false, error: "forbidden" };
|
||||
}
|
||||
if (!isValidNpub(moderatorNpub)) {
|
||||
return { ok: false, error: "invalid npub" };
|
||||
}
|
||||
|
||||
const normalized = normalizeNpub(moderatorNpub);
|
||||
if (!normalized || normalized === ADMIN_SUPER_NPUB) {
|
||||
return { ok: false, error: "immutable" };
|
||||
}
|
||||
|
||||
this.editors.add(normalized);
|
||||
saveJSONList(ADMIN_EDITORS_KEY, this.getEditors());
|
||||
this.refresh();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
removeModerator(requestorNpub, moderatorNpub) {
|
||||
if (!this.isSuperAdmin(requestorNpub)) {
|
||||
return { ok: false, error: "forbidden" };
|
||||
}
|
||||
|
||||
const normalized = normalizeNpub(moderatorNpub);
|
||||
if (!normalized || normalized === ADMIN_SUPER_NPUB) {
|
||||
return { ok: false, error: "immutable" };
|
||||
}
|
||||
|
||||
this.editors.delete(normalized);
|
||||
saveJSONList(ADMIN_EDITORS_KEY, this.getEditors());
|
||||
this.refresh();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
addToWhitelist(actorNpub, targetNpub) {
|
||||
if (!this.canEditAdminLists(actorNpub)) {
|
||||
return { ok: false, error: "forbidden" };
|
||||
}
|
||||
if (!isValidNpub(targetNpub)) {
|
||||
return { ok: false, error: "invalid npub" };
|
||||
}
|
||||
|
||||
const normalized = normalizeNpub(targetNpub);
|
||||
if (!normalized) {
|
||||
return { ok: false, error: "invalid npub" };
|
||||
}
|
||||
|
||||
this.blacklist.delete(normalized);
|
||||
this.whitelist.add(normalized);
|
||||
saveJSONList(ADMIN_WHITELIST_KEY, this.getWhitelist());
|
||||
saveJSONList(ADMIN_BLACKLIST_KEY, this.getBlacklist());
|
||||
this.refresh();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
removeFromWhitelist(actorNpub, targetNpub) {
|
||||
if (!this.canEditAdminLists(actorNpub)) {
|
||||
return { ok: false, error: "forbidden" };
|
||||
}
|
||||
|
||||
const normalized = normalizeNpub(targetNpub);
|
||||
if (!normalized) {
|
||||
return { ok: false, error: "invalid npub" };
|
||||
}
|
||||
|
||||
this.whitelist.delete(normalized);
|
||||
saveJSONList(ADMIN_WHITELIST_KEY, this.getWhitelist());
|
||||
this.refresh();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
addToBlacklist(actorNpub, targetNpub) {
|
||||
if (!this.canEditAdminLists(actorNpub)) {
|
||||
return { ok: false, error: "forbidden" };
|
||||
}
|
||||
if (!isValidNpub(targetNpub)) {
|
||||
return { ok: false, error: "invalid npub" };
|
||||
}
|
||||
|
||||
const normalized = normalizeNpub(targetNpub);
|
||||
if (!normalized) {
|
||||
return { ok: false, error: "invalid npub" };
|
||||
}
|
||||
|
||||
if (this.isSuperAdmin(normalized) || this.isAdminEditor(normalized)) {
|
||||
return { ok: false, error: "immutable" };
|
||||
}
|
||||
|
||||
this.whitelist.delete(normalized);
|
||||
this.blacklist.add(normalized);
|
||||
saveJSONList(ADMIN_BLACKLIST_KEY, this.getBlacklist());
|
||||
saveJSONList(ADMIN_WHITELIST_KEY, this.getWhitelist());
|
||||
this.refresh();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
removeFromBlacklist(actorNpub, targetNpub) {
|
||||
if (!this.canEditAdminLists(actorNpub)) {
|
||||
return { ok: false, error: "forbidden" };
|
||||
}
|
||||
|
||||
const normalized = normalizeNpub(targetNpub);
|
||||
if (!normalized) {
|
||||
return { ok: false, error: "invalid npub" };
|
||||
}
|
||||
|
||||
this.blacklist.delete(normalized);
|
||||
saveJSONList(ADMIN_BLACKLIST_KEY, this.getBlacklist());
|
||||
this.refresh();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
setWhitelistMode(actorNpub, enabled) {
|
||||
if (!this.isSuperAdmin(actorNpub)) {
|
||||
return { ok: false, error: "forbidden" };
|
||||
}
|
||||
persistWhitelistMode(!!enabled);
|
||||
this.whitelistEnabled = !!enabled;
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
isBlacklisted(npub) {
|
||||
const normalized = normalizeNpub(npub);
|
||||
return normalized ? this.blacklist.has(normalized) : false;
|
||||
}
|
||||
|
||||
canAccess(candidate) {
|
||||
let npub = "";
|
||||
|
||||
if (typeof candidate === "string") {
|
||||
npub = candidate;
|
||||
} else if (candidate && typeof candidate === "object") {
|
||||
if (typeof candidate.npub === "string") {
|
||||
npub = candidate.npub;
|
||||
} else if (typeof candidate.pubkey === "string") {
|
||||
try {
|
||||
npub = window.NostrTools.nip19.npubEncode(candidate.pubkey);
|
||||
} catch (error) {
|
||||
npub = candidate.pubkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = normalizeNpub(npub);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.blacklist.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.whitelistEnabled && !this.whitelist.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const accessControl = new AccessControl();
|
||||
export {
|
||||
ADMIN_EDITORS_KEY,
|
||||
ADMIN_WHITELIST_KEY,
|
||||
ADMIN_BLACKLIST_KEY,
|
||||
ADMIN_WHITELIST_MODE_STORAGE_KEY,
|
||||
normalizeNpub,
|
||||
isValidNpub,
|
||||
};
|
||||
Reference in new issue
Block a user