mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 20:02:26 +00:00
Merge pull request #268 from PR0M3TH3AN/codex/build-authservice-for-login-and-profiles
Refactor authentication handling into dedicated service
This commit is contained in:
2 files changed
+822
-386
No files matched your search
@@ -22,6 +22,7 @@ import { relayManager } from "./relayManager.js";
|
||||
import watchHistoryService from "./watchHistoryService.js";
|
||||
import r2Service from "./services/r2Service.js";
|
||||
import PlaybackService from "./services/playbackService.js";
|
||||
import AuthService from "./services/authService.js";
|
||||
import { initQuickR2Upload } from "./r2-quick.js";
|
||||
import { createWatchHistoryRenderer } from "./historyView.js";
|
||||
import { getSidebarLoadingMarkup } from "./sidebarLoading.js";
|
||||
@@ -57,12 +58,6 @@ import {
|
||||
setActiveProfilePubkey as setStoredActiveProfilePubkey,
|
||||
setSavedProfiles,
|
||||
persistSavedProfiles,
|
||||
loadSavedProfilesFromStorage as hydrateSavedProfilesFromStorage,
|
||||
syncSavedProfileFromCache as syncSavedProfileFromCacheState,
|
||||
loadProfileCacheFromStorage as hydrateProfileCacheFromStorage,
|
||||
persistProfileCacheToStorage as persistProfileCacheState,
|
||||
getProfileCacheEntry as getCachedProfileEntry,
|
||||
setProfileCacheEntry as setCachedProfileEntry,
|
||||
getProfileCacheMap,
|
||||
getCachedUrlHealth as readCachedUrlHealth,
|
||||
storeUrlHealth as persistUrlHealth,
|
||||
@@ -393,6 +388,18 @@ class bitvidApp {
|
||||
});
|
||||
this.activePlaybackSession = null;
|
||||
|
||||
this.authService = new AuthService({
|
||||
nostrClient,
|
||||
userBlocks,
|
||||
relayManager,
|
||||
logger: (message, ...args) => this.log(message, ...args),
|
||||
});
|
||||
this.authService.on("auth:login", (detail) => this.handleAuthLogin(detail));
|
||||
this.authService.on("auth:logout", (detail) => this.handleAuthLogout(detail));
|
||||
this.authService.on("profile:updated", (detail) =>
|
||||
this.handleProfileUpdated(detail)
|
||||
);
|
||||
|
||||
// Optional: a "profile" button or avatar (if used)
|
||||
this.profileButton = document.getElementById("profileButton") || null;
|
||||
this.profileAvatar = document.getElementById("profileAvatar") || null;
|
||||
@@ -671,15 +678,15 @@ class bitvidApp {
|
||||
}
|
||||
|
||||
loadSavedProfilesFromStorage() {
|
||||
const { profiles } = hydrateSavedProfilesFromStorage();
|
||||
if (!getActiveProfilePubkey() && Array.isArray(profiles) && profiles.length) {
|
||||
setStoredActiveProfilePubkey(profiles[0].pubkey, { persist: true });
|
||||
}
|
||||
const result = this.authService.loadSavedProfilesFromStorage();
|
||||
this.renderSavedProfiles();
|
||||
return result;
|
||||
}
|
||||
|
||||
syncSavedProfileFromCache(pubkey, { persist = false } = {}) {
|
||||
const updated = syncSavedProfileFromCacheState(pubkey, { persist });
|
||||
const updated = this.authService.syncSavedProfileFromCache(pubkey, {
|
||||
persist,
|
||||
});
|
||||
if (updated) {
|
||||
this.renderSavedProfiles();
|
||||
}
|
||||
@@ -687,31 +694,19 @@ class bitvidApp {
|
||||
}
|
||||
|
||||
loadProfileCacheFromStorage() {
|
||||
hydrateProfileCacheFromStorage();
|
||||
this.authService.loadProfileCacheFromStorage();
|
||||
}
|
||||
|
||||
persistProfileCacheToStorage() {
|
||||
persistProfileCacheState();
|
||||
this.authService.persistProfileCache();
|
||||
}
|
||||
|
||||
getProfileCacheEntry(pubkey) {
|
||||
return getCachedProfileEntry(pubkey);
|
||||
return this.authService.getProfileCacheEntry(pubkey);
|
||||
}
|
||||
|
||||
setProfileCacheEntry(pubkey, profile) {
|
||||
const entry = setCachedProfileEntry(pubkey, profile);
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPubkey = this.normalizeHexPubkey(pubkey);
|
||||
const didUpdateSavedProfile = this.syncSavedProfileFromCache(normalizedPubkey, {
|
||||
persist: true,
|
||||
});
|
||||
if (!didUpdateSavedProfile) {
|
||||
this.renderSavedProfiles();
|
||||
}
|
||||
return entry;
|
||||
return this.authService.setProfileCacheEntry(pubkey, profile);
|
||||
}
|
||||
|
||||
persistActiveProfileSelection(pubkey, { persist = true } = {}) {
|
||||
@@ -754,7 +749,7 @@ class bitvidApp {
|
||||
|
||||
// 3) For each author, fetchAndRenderProfile with forceRefresh = true
|
||||
for (const authorPubkey of uniqueAuthors) {
|
||||
this.fetchAndRenderProfile(authorPubkey, true);
|
||||
this.authService.fetchAndRenderProfile(authorPubkey, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,8 +762,8 @@ class bitvidApp {
|
||||
});
|
||||
}
|
||||
|
||||
this.loadProfileCacheFromStorage();
|
||||
this.loadSavedProfilesFromStorage();
|
||||
this.authService.hydrateFromStorage();
|
||||
this.renderSavedProfiles();
|
||||
|
||||
// 1. Initialize the video modal (components/video-modal.html)
|
||||
await this.initModal();
|
||||
@@ -821,7 +816,7 @@ class bitvidApp {
|
||||
if (savedPubKey) {
|
||||
// Auto-login if a pubkey was saved
|
||||
try {
|
||||
await this.login(savedPubKey, { persistActive: false });
|
||||
await this.authService.login(savedPubKey, { persistActive: false });
|
||||
} catch (error) {
|
||||
console.error("Auto-login failed:", error);
|
||||
}
|
||||
@@ -3364,8 +3359,13 @@ class bitvidApp {
|
||||
}
|
||||
if (this.profileLogoutBtn && !this.profileLogoutBtn.dataset.bound) {
|
||||
this.profileLogoutBtn.dataset.bound = "true";
|
||||
this.profileLogoutBtn.addEventListener("click", () => {
|
||||
this.logout();
|
||||
this.profileLogoutBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await this.authService.logout();
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
this.showError("Failed to logout. Please try again.");
|
||||
}
|
||||
this.hideProfileModal();
|
||||
});
|
||||
}
|
||||
@@ -4251,8 +4251,9 @@ class bitvidApp {
|
||||
setLoadingState(true);
|
||||
|
||||
try {
|
||||
const pubkey = await nostrClient.login({
|
||||
const { pubkey } = await this.authService.requestLogin({
|
||||
allowAccountSelection: true,
|
||||
autoApply: false,
|
||||
});
|
||||
|
||||
const normalizedPubkey = this.normalizeHexPubkey(pubkey);
|
||||
@@ -4275,7 +4276,7 @@ class bitvidApp {
|
||||
let profileMeta = this.getProfileCacheEntry(normalizedPubkey)?.profile;
|
||||
|
||||
if (!profileMeta) {
|
||||
await this.loadOwnProfile(normalizedPubkey);
|
||||
await this.authService.loadOwnProfile(normalizedPubkey);
|
||||
profileMeta = this.getProfileCacheEntry(normalizedPubkey)?.profile;
|
||||
}
|
||||
|
||||
@@ -5030,8 +5031,13 @@ class bitvidApp {
|
||||
setupEventListeners() {
|
||||
// 1) Logout button
|
||||
if (this.logoutButton) {
|
||||
this.logoutButton.addEventListener("click", () => {
|
||||
this.logout();
|
||||
this.logoutButton.addEventListener("click", async () => {
|
||||
try {
|
||||
await this.authService.logout();
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
this.showError("Failed to logout. Please try again.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5101,15 +5107,15 @@ class bitvidApp {
|
||||
"[app.js] loginNIP07 clicked! Attempting extension login..."
|
||||
);
|
||||
try {
|
||||
const pubkey = await nostrClient.login(); // call the extension
|
||||
const { pubkey } = await this.authService.requestLogin();
|
||||
console.log("[NIP-07] login returned pubkey:", pubkey);
|
||||
await this.login(pubkey, { persistActive: true });
|
||||
|
||||
// Hide the login modal
|
||||
const loginModal = document.getElementById("loginModal");
|
||||
if (loginModal) {
|
||||
loginModal.classList.add("hidden");
|
||||
setGlobalModalState("login", false);
|
||||
if (pubkey) {
|
||||
const loginModal = document.getElementById("loginModal");
|
||||
if (loginModal) {
|
||||
loginModal.classList.add("hidden");
|
||||
setGlobalModalState("login", false);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[NIP-07 login error]", err);
|
||||
@@ -5244,87 +5250,11 @@ class bitvidApp {
|
||||
* Attempt to load the user's own profile from Nostr (kind:0).
|
||||
*/
|
||||
async loadOwnProfile(pubkey) {
|
||||
try {
|
||||
const events = await nostrClient.pool.list(nostrClient.relays, [
|
||||
{ kinds: [0], authors: [pubkey], limit: 1 },
|
||||
]);
|
||||
let displayName = "User";
|
||||
let picture = "assets/svg/default-profile.svg";
|
||||
|
||||
if (events.length && events[0].content) {
|
||||
const data = JSON.parse(events[0].content);
|
||||
displayName = data.name || data.display_name || "User";
|
||||
picture = data.picture || "assets/svg/default-profile.svg";
|
||||
}
|
||||
|
||||
this.setProfileCacheEntry(pubkey, {
|
||||
name: displayName,
|
||||
picture,
|
||||
});
|
||||
|
||||
// If you have a top-bar avatar (profileAvatar)
|
||||
if (this.profileAvatar) {
|
||||
this.profileAvatar.src = picture;
|
||||
}
|
||||
// If you want to show name somewhere
|
||||
if (this.profileModalName) {
|
||||
this.profileModalName.textContent = displayName;
|
||||
}
|
||||
if (this.profileModalAvatar) {
|
||||
this.profileModalAvatar.src = picture;
|
||||
}
|
||||
if (this.profileModalNpub) {
|
||||
const encoded = this.safeEncodeNpub(pubkey);
|
||||
this.profileModalNpub.textContent = encoded
|
||||
? truncateMiddle(encoded, 48)
|
||||
: "Not signed in";
|
||||
}
|
||||
if (this.profileChannelLink) {
|
||||
const targetNpub = this.safeEncodeNpub(pubkey);
|
||||
if (targetNpub) {
|
||||
this.profileChannelLink.href = `#view=channel-profile&npub=${targetNpub}`;
|
||||
this.profileChannelLink.dataset.targetNpub = targetNpub;
|
||||
this.profileChannelLink.classList.remove("hidden");
|
||||
} else {
|
||||
this.profileChannelLink.removeAttribute("href");
|
||||
delete this.profileChannelLink.dataset.targetNpub;
|
||||
this.profileChannelLink.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("loadOwnProfile error:", error);
|
||||
}
|
||||
return this.authService.loadOwnProfile(pubkey);
|
||||
}
|
||||
|
||||
async fetchAndRenderProfile(pubkey, forceRefresh = false) {
|
||||
const cacheEntry = this.getProfileCacheEntry(pubkey);
|
||||
if (cacheEntry) {
|
||||
this.updateProfileInDOM(pubkey, cacheEntry.profile);
|
||||
if (!forceRefresh) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Otherwise, fetch from Nostr
|
||||
try {
|
||||
const userEvents = await nostrClient.pool.list(nostrClient.relays, [
|
||||
{ kinds: [0], authors: [pubkey], limit: 1 },
|
||||
]);
|
||||
if (userEvents.length > 0 && userEvents[0].content) {
|
||||
const data = JSON.parse(userEvents[0].content);
|
||||
const profile = {
|
||||
name: data.name || data.display_name || "Unknown",
|
||||
picture: data.picture || "assets/svg/default-profile.svg",
|
||||
};
|
||||
|
||||
// Cache it
|
||||
this.setProfileCacheEntry(pubkey, profile);
|
||||
// Update DOM
|
||||
this.updateProfileInDOM(pubkey, profile);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Profile fetch error:", err);
|
||||
}
|
||||
return this.authService.fetchAndRenderProfile(pubkey, forceRefresh);
|
||||
}
|
||||
|
||||
async batchFetchProfiles(authorSet) {
|
||||
@@ -5628,39 +5558,41 @@ class bitvidApp {
|
||||
const privEl = document.getElementById("uploadIsPrivate");
|
||||
const commentsEl = document.getElementById("uploadEnableComments");
|
||||
|
||||
const title = titleEl?.value.trim() || "";
|
||||
const url = urlEl?.value.trim() || "";
|
||||
const magnet = magnetEl?.value.trim() || "";
|
||||
const ws = wsEl?.value.trim() || "";
|
||||
const xs = xsEl?.value.trim() || "";
|
||||
const thumbnail = thumbEl?.value.trim() || "";
|
||||
const description = descEl?.value.trim() || "";
|
||||
const enableComments = commentsEl ? commentsEl.checked : true;
|
||||
|
||||
const payload = {
|
||||
title,
|
||||
url,
|
||||
magnet,
|
||||
thumbnail,
|
||||
description,
|
||||
ws,
|
||||
xs,
|
||||
enableComments,
|
||||
title: titleEl?.value.trim() || "",
|
||||
url: urlEl?.value.trim() || "",
|
||||
magnet: magnetEl?.value.trim() || "",
|
||||
ws: wsEl?.value.trim() || "",
|
||||
xs: xsEl?.value.trim() || "",
|
||||
thumbnail: thumbEl?.value.trim() || "",
|
||||
description: descEl?.value.trim() || "",
|
||||
enableComments: commentsEl ? commentsEl.checked : true,
|
||||
};
|
||||
|
||||
await this.publishVideoNote(payload, {
|
||||
onSuccess: () => {
|
||||
if (titleEl) titleEl.value = "";
|
||||
if (urlEl) urlEl.value = "";
|
||||
if (magnetEl) magnetEl.value = "";
|
||||
if (wsEl) wsEl.value = "";
|
||||
if (xsEl) xsEl.value = "";
|
||||
if (thumbEl) thumbEl.value = "";
|
||||
if (descEl) descEl.value = "";
|
||||
if (privEl) privEl.checked = false;
|
||||
if (commentsEl) commentsEl.checked = true;
|
||||
},
|
||||
});
|
||||
try {
|
||||
await this.authService.handleUploadSubmit(payload, {
|
||||
publish: (data) =>
|
||||
this.publishVideoNote(data, {
|
||||
onSuccess: () => {
|
||||
if (titleEl) titleEl.value = "";
|
||||
if (urlEl) urlEl.value = "";
|
||||
if (magnetEl) magnetEl.value = "";
|
||||
if (wsEl) wsEl.value = "";
|
||||
if (xsEl) xsEl.value = "";
|
||||
if (thumbEl) thumbEl.value = "";
|
||||
if (descEl) descEl.value = "";
|
||||
if (privEl) privEl.checked = false;
|
||||
if (commentsEl) commentsEl.checked = true;
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
const message =
|
||||
error && typeof error.message === "string" && error.message.trim()
|
||||
? error.message.trim()
|
||||
: "Login required to publish videos.";
|
||||
this.showError(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5669,60 +5601,21 @@ class bitvidApp {
|
||||
* accounts without touching cached avatars.
|
||||
*/
|
||||
removeSavedProfile(pubkey) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey);
|
||||
const target = normalized || (typeof pubkey === "string" ? pubkey.trim() : "");
|
||||
if (!target) {
|
||||
return;
|
||||
const { removed } = this.authService.removeSavedProfile(pubkey);
|
||||
if (removed) {
|
||||
this.renderSavedProfiles();
|
||||
}
|
||||
|
||||
const nextProfiles = this.savedProfiles.filter(
|
||||
(entry) => entry.pubkey !== target
|
||||
);
|
||||
if (nextProfiles.length === this.savedProfiles.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.savedProfiles = nextProfiles;
|
||||
|
||||
if (this.activeProfilePubkey === target) {
|
||||
this.activeProfilePubkey = null;
|
||||
}
|
||||
|
||||
persistSavedProfiles();
|
||||
this.renderSavedProfiles();
|
||||
}
|
||||
|
||||
async switchProfile(pubkey) {
|
||||
const normalizedTarget = this.normalizeHexPubkey(pubkey);
|
||||
if (!normalizedTarget) {
|
||||
this.showError("Unable to switch profiles: invalid account.");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedActive = this.normalizeHexPubkey(this.activeProfilePubkey);
|
||||
if (normalizedActive && normalizedActive === normalizedTarget) {
|
||||
this.hideProfileModal();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await nostrClient.login({
|
||||
allowAccountSelection: true,
|
||||
expectPubkey: normalizedTarget,
|
||||
});
|
||||
const result = await this.authService.switchProfile(pubkey);
|
||||
if (!result?.switched) {
|
||||
this.hideProfileModal();
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error && typeof error.message === "string" && error.message.trim()
|
||||
? error.message.trim()
|
||||
: "Profile switch was cancelled.";
|
||||
this.showError(message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.login(normalizedTarget, { persistActive: true });
|
||||
} catch (error) {
|
||||
console.error("Failed to finalize profile switch:", error);
|
||||
console.error("Failed to switch profiles:", error);
|
||||
const message =
|
||||
error && typeof error.message === "string" && error.message.trim()
|
||||
? error.message.trim()
|
||||
@@ -5732,42 +5625,58 @@ class bitvidApp {
|
||||
}
|
||||
|
||||
this.profileSwitcherSelectionPubkey = null;
|
||||
|
||||
let reordered = false;
|
||||
const currentIndex = this.savedProfiles.findIndex(
|
||||
(entry) => this.normalizeHexPubkey(entry.pubkey) === normalizedTarget
|
||||
);
|
||||
if (currentIndex > 0) {
|
||||
const [moved] = this.savedProfiles.splice(currentIndex, 1);
|
||||
this.savedProfiles.unshift(moved);
|
||||
reordered = true;
|
||||
}
|
||||
|
||||
if (reordered) {
|
||||
persistSavedProfiles({ persistActive: true });
|
||||
}
|
||||
|
||||
this.renderSavedProfiles();
|
||||
this.hideProfileModal();
|
||||
}
|
||||
|
||||
async applyPostLoginState() {
|
||||
await this.refreshAdminPaneState();
|
||||
updateActiveProfileUI(pubkey, profile = {}) {
|
||||
const displayName = profile.name || "User";
|
||||
const picture = profile.picture || "assets/svg/default-profile.svg";
|
||||
|
||||
if (this.profileAvatar) {
|
||||
this.profileAvatar.src = picture;
|
||||
}
|
||||
if (this.profileModalName) {
|
||||
this.profileModalName.textContent = displayName;
|
||||
}
|
||||
if (this.profileModalAvatar) {
|
||||
this.profileModalAvatar.src = picture;
|
||||
}
|
||||
if (this.profileModalNpub) {
|
||||
const encoded = this.safeEncodeNpub(pubkey);
|
||||
this.profileModalNpub.textContent = encoded
|
||||
? truncateMiddle(encoded, 48)
|
||||
: "Not signed in";
|
||||
}
|
||||
|
||||
if (this.profileChannelLink) {
|
||||
const targetNpub = this.safeEncodeNpub(pubkey);
|
||||
if (targetNpub) {
|
||||
this.profileChannelLink.href = `#view=channel-profile&npub=${targetNpub}`;
|
||||
this.profileChannelLink.dataset.targetNpub = targetNpub;
|
||||
this.profileChannelLink.classList.remove("hidden");
|
||||
} else {
|
||||
this.profileChannelLink.removeAttribute("href");
|
||||
delete this.profileChannelLink.dataset.targetNpub;
|
||||
this.profileChannelLink.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleAuthLogin(detail = {}) {
|
||||
if (detail?.identityChanged) {
|
||||
this.resetViewLoggingState();
|
||||
}
|
||||
|
||||
this.renderSavedProfiles();
|
||||
|
||||
try {
|
||||
await userBlocks.loadBlocks(this.pubkey);
|
||||
await this.refreshAdminPaneState();
|
||||
} catch (error) {
|
||||
console.warn("Failed to load personal block list:", error);
|
||||
console.warn("Failed to refresh admin pane after login:", error);
|
||||
}
|
||||
|
||||
this.populateBlockedList();
|
||||
|
||||
try {
|
||||
await relayManager.loadRelayList(this.pubkey);
|
||||
} catch (error) {
|
||||
console.warn("Failed to load relay list:", error);
|
||||
}
|
||||
|
||||
this.populateProfileRelays();
|
||||
|
||||
if (this.loginButton) {
|
||||
@@ -5800,218 +5709,93 @@ class bitvidApp {
|
||||
this.subscriptionsLink.classList.remove("hidden");
|
||||
}
|
||||
|
||||
if (this.pubkey) {
|
||||
this.loadOwnProfile(this.pubkey);
|
||||
const activePubkey = detail?.pubkey || this.pubkey;
|
||||
if (activePubkey && detail?.postLogin?.profile) {
|
||||
this.updateActiveProfileUI(activePubkey, detail.postLogin.profile);
|
||||
}
|
||||
|
||||
await this.loadVideos();
|
||||
|
||||
this.forceRefreshAllProfiles();
|
||||
|
||||
await this.refreshCloudflareBucketPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called upon successful login.
|
||||
*/
|
||||
async login(pubkey, options = {}) {
|
||||
console.log("[app.js] login() called with pubkey =", pubkey);
|
||||
|
||||
let normalizedOptions = options;
|
||||
if (typeof normalizedOptions === "boolean") {
|
||||
normalizedOptions = { persistActive: normalizedOptions };
|
||||
} else if (!normalizedOptions || typeof normalizedOptions !== "object") {
|
||||
normalizedOptions = {};
|
||||
}
|
||||
|
||||
const { persistActive = true } = normalizedOptions;
|
||||
|
||||
const previousActivePubkey =
|
||||
this.normalizeHexPubkey(this.pubkey) ||
|
||||
(typeof this.pubkey === "string" ? this.pubkey.trim() : "");
|
||||
|
||||
const normalizedPubkey = this.normalizeHexPubkey(pubkey);
|
||||
const nextActivePubkey =
|
||||
normalizedPubkey || (typeof pubkey === "string" ? pubkey.trim() : "");
|
||||
|
||||
const identityChanged = previousActivePubkey !== nextActivePubkey;
|
||||
if (identityChanged) {
|
||||
this.resetViewLoggingState();
|
||||
}
|
||||
|
||||
const trimmedPubkey =
|
||||
typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : "";
|
||||
|
||||
if (normalizedPubkey) {
|
||||
this.pubkey = normalizedPubkey;
|
||||
if (nostrClient && typeof nostrClient === "object") {
|
||||
nostrClient.pubkey = normalizedPubkey;
|
||||
}
|
||||
} else {
|
||||
this.pubkey = pubkey;
|
||||
if (nostrClient && typeof nostrClient === "object") {
|
||||
nostrClient.pubkey = trimmedPubkey ? trimmedPubkey.toLowerCase() : "";
|
||||
}
|
||||
}
|
||||
this.currentUserNpub = this.safeEncodeNpub(this.pubkey);
|
||||
|
||||
if (
|
||||
identityChanged &&
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.dispatchEvent === "function"
|
||||
) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("bitvid:auth-changed", {
|
||||
detail: {
|
||||
pubkey: nextActivePubkey || null,
|
||||
previousPubkey: previousActivePubkey || null,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let savedProfilesMutated = false;
|
||||
if (normalizedPubkey) {
|
||||
const cacheEntry = this.profileCache.get(normalizedPubkey);
|
||||
const cachedProfile = cacheEntry?.profile || {};
|
||||
const existingIndex = this.savedProfiles.findIndex(
|
||||
(candidate) => candidate.pubkey === normalizedPubkey
|
||||
);
|
||||
const existingEntry =
|
||||
existingIndex >= 0 ? this.savedProfiles[existingIndex] : null;
|
||||
const npub = this.safeEncodeNpub(normalizedPubkey);
|
||||
const nextEntry = {
|
||||
pubkey: normalizedPubkey,
|
||||
npub: npub || existingEntry?.npub || null,
|
||||
name: cachedProfile.name || existingEntry?.name || "",
|
||||
picture: cachedProfile.picture || existingEntry?.picture || "",
|
||||
authType: "nip07",
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const currentEntry = this.savedProfiles[existingIndex];
|
||||
const changed =
|
||||
currentEntry.npub !== nextEntry.npub ||
|
||||
currentEntry.name !== nextEntry.name ||
|
||||
currentEntry.picture !== nextEntry.picture ||
|
||||
currentEntry.authType !== nextEntry.authType;
|
||||
if (changed) {
|
||||
this.savedProfiles[existingIndex] = nextEntry;
|
||||
savedProfilesMutated = true;
|
||||
}
|
||||
} else {
|
||||
this.savedProfiles.push(nextEntry);
|
||||
savedProfilesMutated = true;
|
||||
}
|
||||
|
||||
this.persistActiveProfileSelection(normalizedPubkey, {
|
||||
persist: persistActive,
|
||||
});
|
||||
|
||||
if (!persistActive && savedProfilesMutated) {
|
||||
persistSavedProfiles({ persistActive: false });
|
||||
}
|
||||
} else {
|
||||
if (persistActive) {
|
||||
console.warn(
|
||||
"[app.js] login() requested storage persistence but pubkey was invalid; skipping saved profile update."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!normalizedPubkey && savedProfilesMutated) {
|
||||
persistSavedProfiles({ persistActive: false });
|
||||
}
|
||||
|
||||
await this.applyPostLoginState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout logic
|
||||
*/
|
||||
async logout() {
|
||||
nostrClient.logout();
|
||||
const previousPubkey =
|
||||
this.normalizeHexPubkey(this.pubkey) ||
|
||||
(typeof this.pubkey === "string" ? this.pubkey.trim() : "");
|
||||
async handleAuthLogout(detail = {}) {
|
||||
this.resetViewLoggingState();
|
||||
this.pubkey = null;
|
||||
this.currentUserNpub = null;
|
||||
|
||||
if (
|
||||
previousPubkey &&
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.dispatchEvent === "function"
|
||||
) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("bitvid:auth-changed", {
|
||||
detail: {
|
||||
pubkey: null,
|
||||
previousPubkey,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
this.renderSavedProfiles();
|
||||
|
||||
this.persistActiveProfileSelection(null, { persist: true });
|
||||
|
||||
userBlocks.reset();
|
||||
this.populateBlockedList();
|
||||
|
||||
relayManager.reset();
|
||||
this.populateProfileRelays();
|
||||
|
||||
// Show the login button again
|
||||
if (this.loginButton) {
|
||||
this.loginButton.classList.remove("hidden");
|
||||
this.loginButton.removeAttribute("hidden");
|
||||
this.loginButton.style.display = "";
|
||||
}
|
||||
|
||||
// Hide logout or userStatus
|
||||
if (this.logoutButton) {
|
||||
this.logoutButton.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (this.userStatus) {
|
||||
this.userStatus.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (this.userPubKey) {
|
||||
this.userPubKey.textContent = "";
|
||||
}
|
||||
|
||||
// Hide upload & profile
|
||||
if (this.uploadButton) {
|
||||
this.uploadButton.classList.add("hidden");
|
||||
this.uploadButton.setAttribute("hidden", "");
|
||||
this.uploadButton.style.display = "none";
|
||||
}
|
||||
|
||||
if (this.profileButton) {
|
||||
this.profileButton.classList.add("hidden");
|
||||
this.profileButton.setAttribute("hidden", "");
|
||||
this.profileButton.style.display = "none";
|
||||
}
|
||||
|
||||
if (this.profileChannelLink) {
|
||||
this.profileChannelLink.classList.add("hidden");
|
||||
this.profileChannelLink.removeAttribute("href");
|
||||
delete this.profileChannelLink.dataset.targetNpub;
|
||||
}
|
||||
|
||||
// Hide the Subscriptions link
|
||||
if (this.subscriptionsLink) {
|
||||
this.subscriptionsLink.classList.add("hidden");
|
||||
}
|
||||
|
||||
await this.refreshAdminPaneState();
|
||||
try {
|
||||
await this.refreshAdminPaneState();
|
||||
} catch (error) {
|
||||
console.warn("Failed to refresh admin pane after logout:", error);
|
||||
}
|
||||
|
||||
this.populateBlockedList();
|
||||
this.populateProfileRelays();
|
||||
|
||||
// Refresh the video list so user sees only public videos again
|
||||
await this.loadVideos();
|
||||
|
||||
// Force a fresh fetch of all profile pictures/names (public ones in this case)
|
||||
this.forceRefreshAllProfiles();
|
||||
|
||||
await this.refreshCloudflareBucketPreview();
|
||||
}
|
||||
|
||||
handleProfileUpdated(detail = {}) {
|
||||
if (Array.isArray(detail?.savedProfiles)) {
|
||||
this.renderSavedProfiles();
|
||||
}
|
||||
|
||||
const normalizedPubkey = detail?.pubkey
|
||||
? this.normalizeHexPubkey(detail.pubkey)
|
||||
: null;
|
||||
const profile = detail?.profile;
|
||||
|
||||
if (normalizedPubkey && profile) {
|
||||
this.updateProfileInDOM(normalizedPubkey, profile);
|
||||
if (this.normalizeHexPubkey(this.pubkey) === normalizedPubkey) {
|
||||
this.updateActiveProfileUI(normalizedPubkey, profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources on unload or modal close.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,652 @@
|
||||
// js/services/authService.js
|
||||
|
||||
import {
|
||||
setPubkey,
|
||||
setCurrentUserNpub,
|
||||
getPubkey,
|
||||
getCurrentUserNpub,
|
||||
} from "../state/appState.js";
|
||||
import {
|
||||
getSavedProfiles,
|
||||
getActiveProfilePubkey,
|
||||
setActiveProfilePubkey,
|
||||
mutateSavedProfiles,
|
||||
persistSavedProfiles,
|
||||
loadSavedProfilesFromStorage as hydrateSavedProfilesFromStorage,
|
||||
syncSavedProfileFromCache as syncSavedProfileFromCacheState,
|
||||
loadProfileCacheFromStorage as hydrateProfileCacheFromStorage,
|
||||
persistProfileCacheToStorage as persistProfileCacheState,
|
||||
getProfileCacheEntry as getCachedProfileEntry,
|
||||
setProfileCacheEntry as setCachedProfileEntry,
|
||||
} from "../state/cache.js";
|
||||
|
||||
class SimpleEventEmitter {
|
||||
constructor(logger = null) {
|
||||
this.logger = typeof logger === "function" ? logger : null;
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
if (typeof handler !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, new Set());
|
||||
}
|
||||
const handlers = this.listeners.get(eventName);
|
||||
handlers.add(handler);
|
||||
return () => {
|
||||
handlers.delete(handler);
|
||||
if (!handlers.size) {
|
||||
this.listeners.delete(eventName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emit(eventName, detail) {
|
||||
const handlers = this.listeners.get(eventName);
|
||||
if (!handlers || !handlers.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const handler of Array.from(handlers)) {
|
||||
try {
|
||||
handler(detail);
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
this.logger(`AuthService listener for "${eventName}" threw`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
const FALLBACK_PROFILE = {
|
||||
name: "Unknown",
|
||||
picture: "assets/svg/default-profile.svg",
|
||||
};
|
||||
|
||||
export default class AuthService {
|
||||
constructor({ nostrClient, userBlocks, relayManager, logger } = {}) {
|
||||
this.nostrClient = nostrClient || null;
|
||||
this.userBlocks = userBlocks || null;
|
||||
this.relayManager = relayManager || null;
|
||||
|
||||
if (typeof logger === "function") {
|
||||
this.logger = logger;
|
||||
} else if (logger && typeof logger.log === "function") {
|
||||
this.logger = (...args) => logger.log(...args);
|
||||
} else {
|
||||
this.logger = () => {};
|
||||
}
|
||||
|
||||
this.emitter = new SimpleEventEmitter((message, error) => {
|
||||
try {
|
||||
this.logger(message, error);
|
||||
} catch (logError) {
|
||||
console.warn("[AuthService] logger threw", logError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log(...args) {
|
||||
try {
|
||||
this.logger(...args);
|
||||
} catch (error) {
|
||||
console.warn("[AuthService] logger threw", error);
|
||||
}
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
return this.emitter.on(eventName, handler);
|
||||
}
|
||||
|
||||
emit(eventName, detail) {
|
||||
this.emitter.emit(eventName, detail);
|
||||
}
|
||||
|
||||
hydrateFromStorage() {
|
||||
hydrateProfileCacheFromStorage();
|
||||
const { profiles } = hydrateSavedProfilesFromStorage();
|
||||
if (!getActiveProfilePubkey() && Array.isArray(profiles) && profiles.length) {
|
||||
const first = profiles[0]?.pubkey || null;
|
||||
if (first) {
|
||||
setActiveProfilePubkey(first, { persist: true });
|
||||
}
|
||||
}
|
||||
this.emitProfileList("hydrate");
|
||||
}
|
||||
|
||||
persistProfileCache() {
|
||||
persistProfileCacheState();
|
||||
}
|
||||
|
||||
getProfileCacheEntry(pubkey) {
|
||||
return getCachedProfileEntry(pubkey);
|
||||
}
|
||||
|
||||
setProfileCacheEntry(pubkey, profile, { persist = true, reason = "manual" } = {}) {
|
||||
const entry = setCachedProfileEntry(pubkey, profile, { persist });
|
||||
if (entry) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey);
|
||||
if (normalized) {
|
||||
const updated = syncSavedProfileFromCacheState(normalized, {
|
||||
persist,
|
||||
});
|
||||
if (updated) {
|
||||
this.emitProfileList("cache-sync");
|
||||
}
|
||||
this.emit("profile:updated", {
|
||||
pubkey: normalized,
|
||||
profile: entry.profile || FALLBACK_PROFILE,
|
||||
reason,
|
||||
savedProfiles: this.cloneSavedProfiles(),
|
||||
activeProfilePubkey: getActiveProfilePubkey(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
cloneSavedProfiles() {
|
||||
const saved = getSavedProfiles();
|
||||
return Array.isArray(saved) ? saved.map((entry) => ({ ...entry })) : [];
|
||||
}
|
||||
|
||||
emitProfileList(reason = "update") {
|
||||
this.emit("profile:updated", {
|
||||
reason,
|
||||
savedProfiles: this.cloneSavedProfiles(),
|
||||
activeProfilePubkey: getActiveProfilePubkey(),
|
||||
});
|
||||
}
|
||||
|
||||
safeEncodeNpub(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
try {
|
||||
return window?.NostrTools?.nip19?.npubEncode(trimmed) || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
safeDecodeNpub(npub) {
|
||||
if (typeof npub !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = npub.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = window?.NostrTools?.nip19?.decode(trimmed);
|
||||
if (decoded?.type === "npub" && typeof decoded.data === "string") {
|
||||
return decoded.data;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
normalizeHexPubkey(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (HEX64_REGEX.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
const decoded = this.safeDecodeNpub(trimmed);
|
||||
if (decoded && HEX64_REGEX.test(decoded)) {
|
||||
return decoded.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async requestLogin(options = {}) {
|
||||
if (!this.nostrClient || typeof this.nostrClient.login !== "function") {
|
||||
throw new Error("Nostr login is not available.");
|
||||
}
|
||||
|
||||
const result = await this.nostrClient.login(options);
|
||||
const pubkey =
|
||||
typeof result === "string"
|
||||
? result
|
||||
: result && typeof result === "object"
|
||||
? result.pubkey || result.publicKey || ""
|
||||
: "";
|
||||
|
||||
const trimmed = typeof pubkey === "string" ? pubkey.trim() : "";
|
||||
if (!trimmed) {
|
||||
return { pubkey: null };
|
||||
}
|
||||
|
||||
if (options?.autoApply === false) {
|
||||
return { pubkey: trimmed };
|
||||
}
|
||||
|
||||
await this.login(trimmed, {
|
||||
persistActive: options?.persistActive !== false,
|
||||
});
|
||||
|
||||
return { pubkey: trimmed };
|
||||
}
|
||||
|
||||
async handleUploadSubmit(payload, { publish } = {}) {
|
||||
if (!this.getActivePubkey()) {
|
||||
await this.requestLogin({ allowAccountSelection: true });
|
||||
if (!this.getActivePubkey()) {
|
||||
throw new Error("Login required to publish videos.");
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof publish !== "function") {
|
||||
throw new Error("A publish callback must be provided to handleUploadSubmit.");
|
||||
}
|
||||
|
||||
return publish(payload);
|
||||
}
|
||||
|
||||
getActivePubkey() {
|
||||
return getPubkey();
|
||||
}
|
||||
|
||||
async login(pubkey, options = {}) {
|
||||
const normalizedOptions =
|
||||
options && typeof options === "object" ? options : { persistActive: true };
|
||||
const persistActive =
|
||||
normalizedOptions.persistActive === false ? false : true;
|
||||
|
||||
const previousPubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey();
|
||||
const normalized = this.normalizeHexPubkey(pubkey);
|
||||
const trimmed = typeof pubkey === "string" ? pubkey.trim() : "";
|
||||
const nextPubkey = normalized || trimmed || null;
|
||||
if (!nextPubkey) {
|
||||
throw new Error("A valid pubkey is required for login.");
|
||||
}
|
||||
|
||||
const identityChanged = previousPubkey !== nextPubkey;
|
||||
|
||||
if (normalized) {
|
||||
setPubkey(normalized);
|
||||
if (this.nostrClient && typeof this.nostrClient === "object") {
|
||||
this.nostrClient.pubkey = normalized;
|
||||
}
|
||||
} else {
|
||||
setPubkey(nextPubkey);
|
||||
if (this.nostrClient && typeof this.nostrClient === "object") {
|
||||
this.nostrClient.pubkey = trimmed ? trimmed.toLowerCase() : "";
|
||||
}
|
||||
}
|
||||
|
||||
const npub = this.safeEncodeNpub(nextPubkey);
|
||||
setCurrentUserNpub(npub);
|
||||
|
||||
let savedProfilesMutated = false;
|
||||
mutateSavedProfiles((profiles) => {
|
||||
const draft = Array.isArray(profiles) ? profiles.slice() : [];
|
||||
const existingIndex = draft.findIndex((entry) => {
|
||||
const normalizedEntry = this.normalizeHexPubkey(entry?.pubkey);
|
||||
return normalizedEntry && normalizedEntry === normalized;
|
||||
});
|
||||
|
||||
const cacheEntry = this.getProfileCacheEntry(normalized || nextPubkey);
|
||||
const cachedProfile = cacheEntry?.profile || {};
|
||||
|
||||
const nextEntry = {
|
||||
pubkey: normalized || nextPubkey,
|
||||
npub: npub || (cachedProfile.npub ?? null),
|
||||
name: cachedProfile.name || draft[existingIndex]?.name || "",
|
||||
picture: cachedProfile.picture || draft[existingIndex]?.picture || "",
|
||||
authType: "nip07",
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const currentEntry = draft[existingIndex] || {};
|
||||
const changed =
|
||||
currentEntry.npub !== nextEntry.npub ||
|
||||
currentEntry.name !== nextEntry.name ||
|
||||
currentEntry.picture !== nextEntry.picture ||
|
||||
currentEntry.authType !== nextEntry.authType;
|
||||
if (changed) {
|
||||
draft[existingIndex] = nextEntry;
|
||||
savedProfilesMutated = true;
|
||||
}
|
||||
} else {
|
||||
draft.push(nextEntry);
|
||||
savedProfilesMutated = true;
|
||||
}
|
||||
|
||||
return draft;
|
||||
}, { persist: false, persistActive: false });
|
||||
|
||||
if (persistActive) {
|
||||
setActiveProfilePubkey(normalized || nextPubkey, { persist: true });
|
||||
}
|
||||
|
||||
if (!persistActive && savedProfilesMutated) {
|
||||
persistSavedProfiles({ persistActive: false });
|
||||
} else if (persistActive || savedProfilesMutated) {
|
||||
persistSavedProfiles({ persistActive: true });
|
||||
}
|
||||
|
||||
if (savedProfilesMutated) {
|
||||
this.emitProfileList("login");
|
||||
}
|
||||
|
||||
const postLogin = await this.applyPostLoginState();
|
||||
|
||||
const detail = {
|
||||
pubkey: getPubkey(),
|
||||
npub: getCurrentUserNpub(),
|
||||
previousPubkey: previousPubkey || null,
|
||||
identityChanged,
|
||||
savedProfiles: this.cloneSavedProfiles(),
|
||||
activeProfilePubkey: getActiveProfilePubkey(),
|
||||
postLogin,
|
||||
};
|
||||
|
||||
this.emit("auth:login", detail);
|
||||
return detail;
|
||||
}
|
||||
|
||||
async applyPostLoginState() {
|
||||
const activePubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey();
|
||||
if (!activePubkey) {
|
||||
return { pubkey: null };
|
||||
}
|
||||
|
||||
let blocksLoaded = false;
|
||||
if (this.userBlocks && typeof this.userBlocks.loadBlocks === "function") {
|
||||
try {
|
||||
await this.userBlocks.loadBlocks(activePubkey);
|
||||
blocksLoaded = true;
|
||||
} catch (error) {
|
||||
this.log("[AuthService] Failed to load block list", error);
|
||||
}
|
||||
}
|
||||
|
||||
let relaysLoaded = false;
|
||||
if (this.relayManager && typeof this.relayManager.loadRelayList === "function") {
|
||||
try {
|
||||
await this.relayManager.loadRelayList(activePubkey);
|
||||
relaysLoaded = true;
|
||||
} catch (error) {
|
||||
this.log("[AuthService] Failed to load relay list", error);
|
||||
}
|
||||
}
|
||||
|
||||
let profile = null;
|
||||
try {
|
||||
profile = await this.loadOwnProfile(activePubkey);
|
||||
} catch (error) {
|
||||
this.log("[AuthService] Failed to load own profile", error);
|
||||
}
|
||||
|
||||
return {
|
||||
pubkey: activePubkey,
|
||||
blocksLoaded,
|
||||
relaysLoaded,
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
async logout() {
|
||||
if (this.nostrClient && typeof this.nostrClient.logout === "function") {
|
||||
try {
|
||||
this.nostrClient.logout();
|
||||
} catch (error) {
|
||||
this.log("[AuthService] nostrClient.logout threw", error);
|
||||
}
|
||||
}
|
||||
|
||||
const previousPubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey();
|
||||
|
||||
setPubkey(null);
|
||||
setCurrentUserNpub(null);
|
||||
setActiveProfilePubkey(null, { persist: true });
|
||||
|
||||
if (this.userBlocks && typeof this.userBlocks.reset === "function") {
|
||||
try {
|
||||
this.userBlocks.reset();
|
||||
} catch (error) {
|
||||
this.log("[AuthService] userBlocks.reset threw", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.relayManager && typeof this.relayManager.reset === "function") {
|
||||
try {
|
||||
this.relayManager.reset();
|
||||
} catch (error) {
|
||||
this.log("[AuthService] relayManager.reset threw", error);
|
||||
}
|
||||
}
|
||||
|
||||
persistSavedProfiles({ persistActive: true });
|
||||
|
||||
const detail = {
|
||||
previousPubkey: previousPubkey || null,
|
||||
savedProfiles: this.cloneSavedProfiles(),
|
||||
activeProfilePubkey: getActiveProfilePubkey(),
|
||||
};
|
||||
|
||||
this.emit("auth:logout", detail);
|
||||
return detail;
|
||||
}
|
||||
|
||||
removeSavedProfile(pubkey) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey) ||
|
||||
(typeof pubkey === "string" ? pubkey.trim() : "");
|
||||
if (!normalized) {
|
||||
return { removed: false };
|
||||
}
|
||||
|
||||
const { changed } = mutateSavedProfiles((profiles) =>
|
||||
profiles.filter((entry) => entry?.pubkey !== normalized),
|
||||
{
|
||||
persist: true,
|
||||
persistActive: true,
|
||||
});
|
||||
|
||||
if (changed && getActiveProfilePubkey() === normalized) {
|
||||
setActiveProfilePubkey(null, { persist: true });
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
this.emitProfileList("remove-saved-profile");
|
||||
}
|
||||
|
||||
return { removed: changed };
|
||||
}
|
||||
|
||||
async switchProfile(pubkey) {
|
||||
const normalizedTarget = this.normalizeHexPubkey(pubkey);
|
||||
if (!normalizedTarget) {
|
||||
throw new Error("Unable to switch profiles: invalid account.");
|
||||
}
|
||||
|
||||
const normalizedActive = this.normalizeHexPubkey(getActiveProfilePubkey());
|
||||
if (normalizedActive && normalizedActive === normalizedTarget) {
|
||||
return { switched: false, reason: "already-active" };
|
||||
}
|
||||
|
||||
try {
|
||||
await this.requestLogin({
|
||||
allowAccountSelection: true,
|
||||
expectPubkey: normalizedTarget,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const detail = await this.login(normalizedTarget, { persistActive: true });
|
||||
|
||||
mutateSavedProfiles((profiles) => {
|
||||
const draft = profiles.slice();
|
||||
const index = draft.findIndex(
|
||||
(entry) => this.normalizeHexPubkey(entry?.pubkey) === normalizedTarget
|
||||
);
|
||||
if (index > 0) {
|
||||
const [moved] = draft.splice(index, 1);
|
||||
draft.unshift(moved);
|
||||
}
|
||||
return draft;
|
||||
}, { persist: true, persistActive: true });
|
||||
|
||||
this.emitProfileList("switch-profile");
|
||||
|
||||
return { switched: true, detail };
|
||||
}
|
||||
|
||||
async loadOwnProfile(pubkey) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey) ||
|
||||
(typeof pubkey === "string" ? pubkey.trim() : "");
|
||||
if (!normalized) {
|
||||
return FALLBACK_PROFILE;
|
||||
}
|
||||
|
||||
if (!this.nostrClient?.pool || !Array.isArray(this.nostrClient?.relays)) {
|
||||
return FALLBACK_PROFILE;
|
||||
}
|
||||
|
||||
const events = await this.nostrClient.pool.list(this.nostrClient.relays, [
|
||||
{ kinds: [0], authors: [normalized], limit: 1 },
|
||||
]);
|
||||
|
||||
let profile = FALLBACK_PROFILE;
|
||||
if (Array.isArray(events) && events.length && events[0]?.content) {
|
||||
try {
|
||||
const data = JSON.parse(events[0].content);
|
||||
profile = {
|
||||
name: data.name || data.display_name || FALLBACK_PROFILE.name,
|
||||
picture: data.picture || FALLBACK_PROFILE.picture,
|
||||
};
|
||||
} catch (error) {
|
||||
this.log("[AuthService] Failed to parse profile metadata", error);
|
||||
}
|
||||
}
|
||||
|
||||
this.setProfileCacheEntry(normalized, profile, {
|
||||
persist: true,
|
||||
reason: "load-own-profile",
|
||||
});
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
async fetchProfile(pubkey, { forceRefresh = false } = {}) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey) ||
|
||||
(typeof pubkey === "string" ? pubkey.trim() : "");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheEntry = this.getProfileCacheEntry(normalized);
|
||||
if (cacheEntry && !forceRefresh) {
|
||||
this.emit("profile:updated", {
|
||||
pubkey: normalized,
|
||||
profile: cacheEntry.profile || FALLBACK_PROFILE,
|
||||
reason: "cache-hit",
|
||||
savedProfiles: this.cloneSavedProfiles(),
|
||||
activeProfilePubkey: getActiveProfilePubkey(),
|
||||
});
|
||||
return cacheEntry.profile;
|
||||
}
|
||||
|
||||
if (!this.nostrClient?.pool || !Array.isArray(this.nostrClient?.relays)) {
|
||||
return cacheEntry?.profile || FALLBACK_PROFILE;
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
this.nostrClient.relays.map((relayUrl) =>
|
||||
this.nostrClient.pool.list([relayUrl], [
|
||||
{ kinds: [0], authors: [normalized], limit: 1 },
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
const events = results.flat();
|
||||
let newest = null;
|
||||
for (const event of events) {
|
||||
if (!event || event.pubkey !== normalized || !event.content) {
|
||||
continue;
|
||||
}
|
||||
if (!newest || event.created_at > newest.created_at) {
|
||||
newest = event;
|
||||
}
|
||||
}
|
||||
|
||||
if (newest?.content) {
|
||||
const data = JSON.parse(newest.content);
|
||||
const profile = {
|
||||
name: data.name || data.display_name || FALLBACK_PROFILE.name,
|
||||
picture: data.picture || FALLBACK_PROFILE.picture,
|
||||
};
|
||||
this.setProfileCacheEntry(normalized, profile, {
|
||||
persist: true,
|
||||
reason: forceRefresh ? "force-refresh" : "fetch-profile",
|
||||
});
|
||||
return profile;
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("[AuthService] Failed to fetch profile", error);
|
||||
}
|
||||
|
||||
return cacheEntry?.profile || FALLBACK_PROFILE;
|
||||
}
|
||||
|
||||
async fetchAndRenderProfile(pubkey, forceRefresh = false) {
|
||||
return this.fetchProfile(pubkey, { forceRefresh });
|
||||
}
|
||||
|
||||
syncSavedProfileFromCache(pubkey, { persist = false } = {}) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey) ||
|
||||
(typeof pubkey === "string" ? pubkey.trim() : "");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const updated = syncSavedProfileFromCacheState(normalized, { persist });
|
||||
if (updated) {
|
||||
this.emitProfileList("cache-sync");
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
loadProfileCacheFromStorage() {
|
||||
hydrateProfileCacheFromStorage();
|
||||
}
|
||||
|
||||
loadSavedProfilesFromStorage() {
|
||||
const result = hydrateSavedProfilesFromStorage();
|
||||
this.emitProfileList("hydrate");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user