Refactor AuthService login logic

Extracted complex logic from `AuthService.login` into private helper methods (`_validateAndNormalizeLoginInputs`, `_checkAccessControl`, `_updateGlobalState`, `_updateSavedProfiles`, `_initiatePostLogin`) to improve readability and maintainability.

Moved `SimpleEventEmitter` to `js/utils/simpleEventEmitter.js` and auth constants/normalization helpers to `js/services/authUtils.js` to reduce file size and coupling.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-10 13:53:10 +00:00
1 parent d3277be3ea
commit f148653c39
3 files changed
+283 -156

No files matched your search

+190 -156
View File
@@ -34,62 +34,15 @@ import {
safeDecodeNpub,
normalizeHexPubkey,
} from "../utils/nostrHelpers.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 FALLBACK_PROFILE = {
name: "Unknown",
picture: "assets/svg/default-profile.svg",
about: "",
website: "",
banner: "",
lud16: "",
lud06: "",
};
const FAST_PROFILE_RELAY_LIMIT = 3;
const FAST_PROFILE_TIMEOUT_MS = 2500;
const BACKGROUND_PROFILE_TIMEOUT_MS = 6000;
import { SimpleEventEmitter } from "../utils/simpleEventEmitter.js";
import {
FALLBACK_PROFILE,
FAST_PROFILE_RELAY_LIMIT,
FAST_PROFILE_TIMEOUT_MS,
BACKGROUND_PROFILE_TIMEOUT_MS,
normalizeProviderId,
normalizeAuthType,
} from "./authUtils.js";
export default class AuthService {
constructor({
@@ -128,7 +81,7 @@ export default class AuthService {
if (typeof getAuthProvider === "function") {
this.resolveAuthProvider = (providerId) => {
const normalizedId = this.normalizeProviderId(providerId);
const normalizedId = normalizeProviderId(providerId);
const provider = getAuthProvider(normalizedId);
if (!provider || typeof provider.login !== "function") {
const error = new Error(`Unknown auth provider: ${normalizedId}`);
@@ -139,7 +92,7 @@ export default class AuthService {
};
} else {
this.resolveAuthProvider = (providerId) => {
const normalizedId = this.normalizeProviderId(providerId);
const normalizedId = normalizeProviderId(providerId);
const registry = this.authProviders || defaultAuthProviders;
const provider =
registry && typeof registry === "object"
@@ -155,45 +108,6 @@ export default class AuthService {
}
}
normalizeProviderId(providerId) {
return typeof providerId === "string" && providerId.trim()
? providerId.trim()
: "nip07";
}
normalizeAuthType(authTypeCandidate, providerId, providerResult) {
const candidates = [];
if (typeof authTypeCandidate === "string") {
candidates.push(authTypeCandidate);
}
if (providerResult && typeof providerResult === "object") {
const resultAuthType = providerResult.authType;
if (typeof resultAuthType === "string") {
candidates.push(resultAuthType);
}
const resultProviderId = providerResult.providerId;
if (typeof resultProviderId === "string") {
candidates.push(resultProviderId);
}
}
if (typeof providerId === "string") {
candidates.push(providerId);
}
for (const candidate of candidates) {
const trimmed = candidate.trim();
if (trimmed) {
return trimmed;
}
}
return "nip07";
}
log(...args) {
try {
this.logger(...args);
@@ -293,7 +207,7 @@ export default class AuthService {
...providerOptions
} = normalizedOptions;
const providerId = this.normalizeProviderId(requestedProviderId);
const providerId = normalizeProviderId(requestedProviderId);
const provider = this.resolveAuthProvider(providerId);
const providerResult = await provider.login({
nostrClient: this.nostrClient,
@@ -315,7 +229,7 @@ export default class AuthService {
providerResult && typeof providerResult === "object"
? providerResult.authType
: null;
const authType = this.normalizeAuthType(
const authType = normalizeAuthType(
authTypeCandidate,
providerId,
providerResult,
@@ -405,11 +319,62 @@ export default class AuthService {
}
async login(pubkey, options = {}) {
const {
normalizedOptions,
providerId,
authType,
signer,
providerResult,
nextPubkey,
normalizedPubkey,
trimmedPubkey,
previousPubkey,
persistActive,
} = this._validateAndNormalizeLoginInputs(pubkey, options);
const { candidateNpub } = this._checkAccessControl(
nextPubkey,
trimmedPubkey,
);
const identityChanged = previousPubkey !== nextPubkey;
this._updateGlobalState(
normalizedPubkey,
nextPubkey,
trimmedPubkey,
candidateNpub,
);
const { savedProfilesMutated, entryAuthType } = this._updateSavedProfiles({
normalizedPubkey,
nextPubkey,
candidateNpub,
authType,
providerId,
providerResult,
persistActive,
});
return this._initiatePostLogin({
activeLoginPubkey: getPubkey(),
previousPubkey,
identityChanged,
savedProfiles: this.cloneSavedProfiles(),
activeProfilePubkey: getActiveProfilePubkey(),
providerId,
entryAuthType,
signer,
providerResult,
});
}
_validateAndNormalizeLoginInputs(pubkey, options) {
const normalizedOptions =
options && typeof options === "object" ? { ...options } : {};
const persistActive =
normalizedOptions.persistActive === false ? false : true;
const providerId = this.normalizeProviderId(normalizedOptions.providerId);
const providerId = normalizeProviderId(normalizedOptions.providerId);
const providerResult = Object.prototype.hasOwnProperty.call(
normalizedOptions,
"providerResult",
@@ -420,28 +385,46 @@ export default class AuthService {
typeof normalizedOptions.authType === "string"
? normalizedOptions.authType
: null;
const authType = this.normalizeAuthType(
const authType = normalizeAuthType(
authTypeCandidate,
providerId,
providerResult,
);
const signer = Object.prototype.hasOwnProperty.call(normalizedOptions, "signer")
const signer = Object.prototype.hasOwnProperty.call(
normalizedOptions,
"signer",
)
? normalizedOptions.signer ?? null
: null;
const previousPubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey();
const normalized = this.normalizeHexPubkey(pubkey);
const trimmed = typeof pubkey === "string" ? pubkey.trim() : "";
const nextPubkey = normalized || trimmed || null;
const normalizedPubkey = this.normalizeHexPubkey(pubkey);
const trimmedPubkey = typeof pubkey === "string" ? pubkey.trim() : "";
const nextPubkey = normalizedPubkey || trimmedPubkey || null;
if (!nextPubkey) {
throw new Error("A valid pubkey is required for login.");
}
const identityChanged = previousPubkey !== nextPubkey;
return {
normalizedOptions,
providerId,
authType,
signer,
providerResult,
nextPubkey,
normalizedPubkey,
trimmedPubkey,
previousPubkey,
persistActive,
};
}
_checkAccessControl(nextPubkey, trimmedPubkey) {
const control = this.accessControl;
const candidateNpub = this.safeEncodeNpub(nextPubkey) || null;
let lockdownActive = false;
if (control && typeof control.isLockdownActive === "function") {
try {
lockdownActive = control.isLockdownActive();
@@ -454,7 +437,10 @@ export default class AuthService {
if (lockdownActive) {
let isAdminCandidate = false;
const adminCheckValue =
candidateNpub || (typeof trimmed === "string" && trimmed ? trimmed : null);
candidateNpub ||
(typeof trimmedPubkey === "string" && trimmedPubkey
? trimmedPubkey
: null);
if (adminCheckValue && typeof control?.isAdminEditor === "function") {
try {
isAdminCandidate = !!control.isAdminEditor(adminCheckValue);
@@ -503,73 +489,101 @@ export default class AuthService {
}
}
if (normalized) {
setPubkey(normalized);
return { candidateNpub };
}
_updateGlobalState(
normalizedPubkey,
nextPubkey,
trimmedPubkey,
candidateNpub,
) {
if (normalizedPubkey) {
setPubkey(normalizedPubkey);
if (this.nostrClient && typeof this.nostrClient === "object") {
this.nostrClient.pubkey = normalized;
this.nostrClient.pubkey = normalizedPubkey;
}
profileCache.setActiveProfile(normalized);
profileCache.setActiveProfile(normalizedPubkey);
} else {
setPubkey(nextPubkey);
if (this.nostrClient && typeof this.nostrClient === "object") {
this.nostrClient.pubkey = trimmed ? trimmed.toLowerCase() : "";
this.nostrClient.pubkey = trimmedPubkey
? trimmedPubkey.toLowerCase()
: "";
}
profileCache.setActiveProfile(trimmed ? trimmed.toLowerCase() : null);
profileCache.setActiveProfile(
trimmedPubkey ? trimmedPubkey.toLowerCase() : null,
);
}
const npub = candidateNpub;
setCurrentUserNpub(npub);
setCurrentUserNpub(candidateNpub);
}
_updateSavedProfiles({
normalizedPubkey,
nextPubkey,
candidateNpub,
authType,
providerId,
providerResult,
persistActive,
}) {
let savedProfilesMutated = false;
const entryAuthType = this.normalizeAuthType(
const entryAuthType = normalizeAuthType(
authType,
providerId,
providerResult,
);
const entryProviderId = this.normalizeProviderId(providerId) || entryAuthType;
const entryProviderId =
normalizeProviderId(providerId) || entryAuthType;
mutateSavedProfiles((profiles) => {
const draft = Array.isArray(profiles) ? profiles.slice() : [];
const existingIndex = draft.findIndex((entry) => {
const normalizedEntry = this.normalizeHexPubkey(entry?.pubkey);
return normalizedEntry && normalizedEntry === normalized;
});
mutateSavedProfiles(
(profiles) => {
const draft = Array.isArray(profiles) ? profiles.slice() : [];
const existingIndex = draft.findIndex((entry) => {
const normalizedEntry = this.normalizeHexPubkey(entry?.pubkey);
return normalizedEntry && normalizedEntry === normalizedPubkey;
});
const cacheEntry = this.getProfileCacheEntry(normalized || nextPubkey);
const cachedProfile = cacheEntry?.profile || {};
const cacheEntry = this.getProfileCacheEntry(
normalizedPubkey || 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: entryAuthType,
providerId: entryProviderId,
};
const nextEntry = {
pubkey: normalizedPubkey || nextPubkey,
npub: candidateNpub || (cachedProfile.npub ?? null),
name: cachedProfile.name || draft[existingIndex]?.name || "",
picture: cachedProfile.picture || draft[existingIndex]?.picture || "",
authType: entryAuthType,
providerId: entryProviderId,
};
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 ||
currentEntry.providerId !== nextEntry.providerId;
if (changed) {
draft[existingIndex] = nextEntry;
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 ||
currentEntry.providerId !== nextEntry.providerId;
if (changed) {
draft[existingIndex] = nextEntry;
savedProfilesMutated = true;
}
} else {
draft.push(nextEntry);
savedProfilesMutated = true;
}
} else {
draft.push(nextEntry);
savedProfilesMutated = true;
}
return draft;
}, { persist: false, persistActive: false });
return draft;
},
{ persist: false, persistActive: false },
);
if (persistActive) {
setActiveProfilePubkey(normalized || nextPubkey, { persist: true });
setActiveProfilePubkey(normalizedPubkey || nextPubkey, { persist: true });
}
if (!persistActive && savedProfilesMutated) {
@@ -582,7 +596,20 @@ export default class AuthService {
this.emitProfileList("login");
}
const activeLoginPubkey = getPubkey();
return { savedProfilesMutated, entryAuthType };
}
_initiatePostLogin({
activeLoginPubkey,
previousPubkey,
identityChanged,
savedProfiles,
activeProfilePubkey,
providerId,
entryAuthType,
signer,
providerResult,
}) {
const normalizedLoginPubkey =
this.normalizeHexPubkey(activeLoginPubkey) || activeLoginPubkey || null;
@@ -591,8 +618,8 @@ export default class AuthService {
npub: getCurrentUserNpub(),
previousPubkey: previousPubkey || null,
identityChanged,
savedProfiles: this.cloneSavedProfiles(),
activeProfilePubkey: getActiveProfilePubkey(),
savedProfiles,
activeProfilePubkey,
providerId,
authType: entryAuthType,
signer,
@@ -607,7 +634,11 @@ export default class AuthService {
postLoginError: null,
};
const { detail: postLoginDetail, completionPromise, relaysReadyPromise } = this.applyPostLoginState({
const {
detail: postLoginDetail,
completionPromise,
relaysReadyPromise,
} = this.applyPostLoginState({
deferBlocks: true,
});
detail.postLogin = postLoginDetail;
@@ -621,7 +652,10 @@ export default class AuthService {
try {
this.emitProfileList("post-login");
} catch (error) {
this.log("[AuthService] Failed to emit profile list after login", error);
this.log(
"[AuthService] Failed to emit profile list after login",
error,
);
}
this.emit("auth:post-login", {
@@ -916,7 +950,7 @@ export default class AuthService {
const normalizedOptions =
options && typeof options === "object" ? { ...options } : {};
const providerId = this.normalizeProviderId(normalizedOptions.providerId);
const providerId = normalizeProviderId(normalizedOptions.providerId);
const normalizedActive = this.normalizeHexPubkey(getActiveProfilePubkey());
if (normalizedActive && normalizedActive === normalizedTarget) {
+53
View File
@@ -0,0 +1,53 @@
export const FALLBACK_PROFILE = {
name: "Unknown",
picture: "assets/svg/default-profile.svg",
about: "",
website: "",
banner: "",
lud16: "",
lud06: "",
};
export const FAST_PROFILE_RELAY_LIMIT = 3;
export const FAST_PROFILE_TIMEOUT_MS = 2500;
export const BACKGROUND_PROFILE_TIMEOUT_MS = 6000;
export function normalizeProviderId(providerId) {
return typeof providerId === "string" && providerId.trim()
? providerId.trim()
: "nip07";
}
export function normalizeAuthType(authTypeCandidate, providerId, providerResult) {
const candidates = [];
if (typeof authTypeCandidate === "string") {
candidates.push(authTypeCandidate);
}
if (providerResult && typeof providerResult === "object") {
const resultAuthType = providerResult.authType;
if (typeof resultAuthType === "string") {
candidates.push(resultAuthType);
}
const resultProviderId = providerResult.providerId;
if (typeof resultProviderId === "string") {
candidates.push(resultProviderId);
}
}
if (typeof providerId === "string") {
candidates.push(providerId);
}
for (const candidate of candidates) {
const trimmed = candidate.trim();
if (trimmed) {
return trimmed;
}
}
return "nip07";
}
+40
View File
@@ -0,0 +1,40 @@
export 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(`Listener for "${eventName}" threw`, error);
}
}
}
}
}