Files
bitvid/js/accessControl.js
T

335 lines
8.0 KiB
JavaScript

// js/accessControl.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";
const ADMIN_EDITORS_KEY = "bitvid_admin_editors";
const ADMIN_WHITELIST_KEY = "bitvid_admin_whitelist";
const ADMIN_BLACKLIST_KEY = "bitvid_admin_blacklist";
const LEGACY_WHITELIST_KEY = "bitvid_whitelist";
const LEGACY_BLACKLIST_KEY = "bitvid_blacklist";
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;
}
}
function saveJSONList(key, values) {
try {
localStorage.setItem(key, JSON.stringify(values));
} catch (error) {
console.warn(`Failed to persist list for ${key}:`, error);
}
}
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())
);
}
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;
}
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;
}
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();
}
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();
}
whitelistMode() {
return this.whitelistEnabled;
}
isSuperAdmin(npub) {
const normalized = normalizeNpub(npub);
return normalized ? normalized === ADMIN_SUPER_NPUB : false;
}
isAdminEditor(npub) {
const normalized = normalizeNpub(npub);
if (!normalized) {
return false;
}
if (this.isSuperAdmin(normalized)) {
return true;
}
return this.editors.has(normalized);
}
canEditAdminLists(npub) {
return this.isAdminEditor(npub);
}
getWhitelist() {
return Array.from(this.whitelist);
}
getBlacklist() {
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,
};