From 79e65e3860b1cb3bd5d974d79b4e0d80c5ac2ad0 Mon Sep 17 00:00:00 2001 From: n <n> Date: Sat, 12 Apr 2025 20:04:12 +0100 Subject: [PATCH] fixes --- client/billboard.html | 5 + client/src/auth-manager.ts | 383 +++++++------ client/src/billboard.ts | 299 ++++++---- client/src/client-event-handler.ts | 4 +- client/src/client.ts | 535 ++++++------------ client/src/components/ClientEventsTable.ts | 39 +- client/src/components/EventDetail.ts | 13 +- client/src/components/EventList.ts | 67 +-- client/src/components/HttpMessagesTable.ts | 11 +- client/src/components/HttpRequestExecutor.ts | 4 +- client/src/components/Nostr21121Creator.ts | 9 +- client/src/components/ResponseViewer.ts | 19 +- client/src/components/ServerUI.ts | 20 +- client/src/converter.ts | 2 +- client/src/http-response-viewer.ts | 14 +- client/src/http-response-viewer.updated.ts | 15 +- client/src/navbar-diagnostics.ts | 148 ----- client/src/profile.ts | 80 +-- client/src/receiver.ts | 4 +- client/src/server-ui.ts | 6 +- client/src/services/AuthenticationService.ts | 497 ++++++---------- client/src/services/ClientEventStore.ts | 1 + client/src/services/EventDetailsRenderer.ts | 27 +- .../services/EventDetailsRenderer.updated.ts | 14 +- client/src/services/EventListRenderer.ts | 3 +- .../src/services/EventListRenderer.updated.ts | 21 +- .../services/EventManager.initialization.ts | 6 +- client/src/services/EventManager.test.ts | 3 +- client/src/services/EventManager.ts | 2 +- client/src/services/HttpClient.ts | 2 +- client/src/services/HttpService.ts | 1 + client/src/services/Nostr21121EventHandler.ts | 14 +- .../services/Nostr21121IntegrationHelper.ts | 17 +- .../src/services/Nostr21121ResponseHandler.ts | 5 +- client/src/services/Nostr21121Service.ts | 5 +- client/src/services/Nostr31120Service.ts | 5 +- .../src/services/NostrEventService.updated.ts | 8 +- client/src/services/NostrService.ts | 7 +- client/src/services/SecureStorageService.ts | 2 +- client/src/services/ToastNotifier.ts | 4 +- client/src/services/UiService.ts | 28 +- client/src/services/WebSocketManager.ts | 29 +- client/src/types/window.d.ts | 10 + client/styles.css | 65 ++- 44 files changed, 1066 insertions(+), 1387 deletions(-) delete mode 100644 client/src/navbar-diagnostics.ts diff --git a/client/billboard.html b/client/billboard.html index 81e45e3..6786ecd 100644 --- a/client/billboard.html +++ b/client/billboard.html @@ -38,6 +38,11 @@ <div id="billboardRelayStatus" class="relay-status">Not connected</div> </div> <div class="billboard-actions"> + <div class="login-container"> + <div id="login-status">Not logged in.</div> + <button id="login-button" class="auth-button">Login with Nostr</button> + <button id="logout-button" class="auth-button" style="display: none;">Logout</button> + </div> <button id="createBillboardBtn" class="primary-button">+ Add New Billboard</button> </div> </div> diff --git a/client/src/auth-manager.ts b/client/src/auth-manager.ts index 6f7303f..4c45377 100644 --- a/client/src/auth-manager.ts +++ b/client/src/auth-manager.ts @@ -1,9 +1,8 @@ /** * auth-manager.ts * Centralized authentication manager for the entire application. - * Prevents network requests until the user is properly authenticated. - * - * Updated to use the new AuthenticationService for more robust authentication. + * Refactored to remove localStorage and session dependencies. + * Uses direct Nostr extension checks to determine auth state. */ import { AuthenticationService } from './services/AuthenticationService'; @@ -11,12 +10,6 @@ import { AuthenticationService } from './services/AuthenticationService'; // Create singleton instance of AuthenticationService const authService = new AuthenticationService(); -// For backward compatibility -let userAuthenticated = false; - -// Event name for authentication state changes -const AUTH_STATE_CHANGED_EVENT = 'auth-state-changed'; - // Direct listeners registry for auth state changes type AuthStateListener = (authenticated: boolean, pubkey?: string) => void; const authStateListeners: AuthStateListener[] = []; @@ -37,52 +30,76 @@ function notifyAuthStateListeners(authenticated: boolean, pubkey?: string): void } } +/** + * Update the login status in UI elements + */ +function updateLoginStatus(pubkey: string | null): void { + const statusElement = document.getElementById('login-status'); + const loginButton = document.getElementById('login-button'); + const logoutButton = document.getElementById('logout-button'); + + if (statusElement && loginButton && logoutButton) { + if (pubkey) { + statusElement.textContent = `Logged in as: ${pubkey.substring(0, 8)}...`; + loginButton.style.display = 'none'; + logoutButton.style.display = 'inline-block'; + } else { + statusElement.textContent = 'Not logged in.'; + loginButton.style.display = 'inline-block'; + logoutButton.style.display = 'none'; + } + } +} + // Queue for operations that should run after authentication const postAuthQueue: Array<() => void> = []; /** * Check if the user is currently authenticated + * Makes a direct check to the extension without using localStorage + * + * @returns Promise<boolean> indicating if user is authenticated */ -export function isAuthenticated(): boolean { - // Use the AuthenticationService - const result = authService.isAuthenticated(); - - // Update the backward-compatible flag - userAuthenticated = result; - - return result; +export async function checkAuthentication(): Promise<boolean> { + return authService.checkAuthentication(); } /** - * Set the authentication state + * Synchronous check if user is authenticated (uses cached state) + * This doesn't make a new request to the extension + * + * @returns boolean indicating if user appears to be authenticated */ -export function setAuthenticated(authenticated: boolean, pubkey?: string): void { - const previousState = userAuthenticated; - userAuthenticated = authenticated; - - if (authenticated && pubkey) { - // For now, just store the pubkey in the AuthenticationService without full authentication - // This maintains backward compatibility while allowing us to use the newer service - const currentSession = authService.getCurrentSession(); +export function isAuthenticated(): boolean { + return authService.isAuthenticated(); +} + +/** + * Login with NostrLogin + * @returns Promise resolving to user's pubkey or null if login failed + */ +export async function loginWithNostrLogin(): Promise<string | null> { + try { + const result = await authService.authenticate(); - // If there's no active session, we need to create one - if (!currentSession) { - // This will trigger a full authentication flow when the user next performs an action - // requiring signature verification - localStorage.setItem('userPublicKey', pubkey); + if (result.success && result.pubkey) { + // Update UI + updateLoginStatus(result.pubkey); + + // Notify listeners + notifyAuthStateListeners(true, result.pubkey); + + // Execute queued operations + executePostAuthQueue(); + + return result.pubkey; } - } else if (!authenticated) { - // Log out through the AuthenticationService - authService.logout(); + + return null; + } catch (error) { + console.error("Error using Nostr login:", error); + return null; } - - // Execute queued operations if becoming authenticated - if (!previousState && authenticated) { - executePostAuthQueue(); - } - - // Notify listeners of state change - notifyAuthStateListeners(authenticated, pubkey); } /** @@ -90,7 +107,7 @@ export function setAuthenticated(authenticated: boolean, pubkey?: string): void * This ensures the operation only runs after successful authentication */ export function addPostAuthOperation(operation: () => void): void { - if (isAuthenticated()) { + if (authService.isAuthenticated()) { // If already authenticated, run immediately operation(); } else { @@ -121,26 +138,15 @@ function executePostAuthQueue(): void { /** * Listen for authentication state changes - * Supports both direct callback registration and CustomEvent listeners */ export function onAuthStateChanged(callback: AuthStateListener): () => void { // Add to direct listeners authStateListeners.push(callback); - // Set up CustomEvent listener for backward compatibility - const handler = (event: Event) => { - const authEvent = event as CustomEvent; - callback( - authEvent.detail.authenticated, - authEvent.detail.pubkey - ); - }; + // Also register with the auth service + const unsubscribe = authService.onAuthStateChanged(callback); - if (typeof window !== 'undefined') { - window.addEventListener(AUTH_STATE_CHANGED_EVENT, handler); - } - - // Return a function to remove both listeners + // Return a function to remove the listener return () => { // Remove from direct listeners const index = authStateListeners.indexOf(callback); @@ -148,10 +154,8 @@ export function onAuthStateChanged(callback: AuthStateListener): () => void { authStateListeners.splice(index, 1); } - // Remove CustomEvent listener - if (typeof window !== 'undefined') { - window.removeEventListener(AUTH_STATE_CHANGED_EVENT, handler); - } + // Also unsubscribe from auth service + unsubscribe(); }; } @@ -162,126 +166,185 @@ export function clearAuthState(): void { // Use the AuthenticationService to log out authService.logout(); - // Update the backward-compatible flag - userAuthenticated = false; + // Update UI + updateLoginStatus(null); - // Clear temporary auth state in sessionStorage - [ - 'nostrLoginInitialized', - 'nostrAuthInProgress', - 'nostrLoginState', - 'nostrAuthPending', - 'nostrLoginStarted' - ].forEach(key => { - sessionStorage.removeItem(key); - }); + // Notify listeners + notifyAuthStateListeners(false); } -/** - * Update authentication state from localStorage - * This should be called on page load - */ -export function updateAuthStateFromStorage(): void { - // Let the AuthenticationService handle loading from storage - // It will check its own storage first, then fall back to the old localStorage key - - // If we're not authenticated in the new service but have a key in the old format, - // use that to restore state - if (!authService.isAuthenticated()) { - const savedPubkey = localStorage.getItem('userPublicKey'); - if (savedPubkey) { - userAuthenticated = true; - } - } else { - userAuthenticated = true; - } -} - -// Initialize authentication state from localStorage -// This ensures the auth state is set correctly when the module is loaded -updateAuthStateFromStorage(); - -// Initialize authentication when the page loads to ensure consistency -if (typeof window !== 'undefined') { - window.addEventListener('DOMContentLoaded', () => { - updateAuthStateFromStorage(); - }); -} - -// Listen for page unload to cleanup authentication state -if (typeof window !== 'undefined') { - window.addEventListener('beforeunload', () => { - // Don't clear authentication state, just session-specific flags - sessionStorage.removeItem('nostrLoginInitialized'); - - // Clear any temporary auth state in sessionStorage - [ - 'nostrAuthInProgress', - 'nostrLoginState', - 'nostrAuthPending', - 'nostrLoginStarted' - ].forEach(key => { - sessionStorage.removeItem(key); - }); - }); -} - -// Listen for auth state changes from the AuthenticationService -authService.onAuthStateChanged((authenticated, pubkey) => { - userAuthenticated = authenticated; - - // Dispatch the legacy event for backward compatibility - if (typeof window !== 'undefined') { - window.dispatchEvent( - new CustomEvent(AUTH_STATE_CHANGED_EVENT, { - detail: { authenticated, pubkey } - }) - ); - } - - // If becoming authenticated, execute queued operations - if (authenticated) { - executePostAuthQueue(); - } -}); - /** * Get the current user's public key + * This attempts to fetch it from the extension if not already cached + * + * @returns Promise resolving to the user's pubkey or null + */ +export async function getCurrentUserPubkeyAsync(): Promise<string | null> { + return authService.getPubkey(); +} + +/** + * Get the current user's public key (synchronous version) + * This only returns the cached pubkey and doesn't make a new request + * + * @returns The cached pubkey or null */ export function getCurrentUserPubkey(): string | null { return authService.getCurrentUserPubkey(); } /** - * Create an authenticated HTTP request using NIP-98 + * Initialize login elements in the UI + */ +export function initializeLoginUI(): void { + if (typeof window === 'undefined' || !window.document) {return;} + + const loginButton = document.getElementById('login-button'); + const logoutButton = document.getElementById('logout-button'); + + if (loginButton) { + loginButton.addEventListener('click', async () => { + const pubkey = await loginWithNostrLogin(); + if (pubkey) { + console.log(`Login successful: ${pubkey.substring(0, 8)}...`); + } + }); + } + + if (logoutButton) { + logoutButton.addEventListener('click', () => { + clearAuthState(); + }); + } + + // Update UI based on current state + authService.getPubkey().then(pubkey => { + updateLoginStatus(pubkey); + }); +} + +// Initialize login UI when DOM is loaded +if (typeof window !== 'undefined' && window.document) { + window.addEventListener('DOMContentLoaded', () => { + initializeLoginUI(); + + // Check authentication status on page load + checkAuthentication().then(isAuth => { + if (isAuth) { + getCurrentUserPubkeyAsync().then(pubkey => { + if (pubkey) { + updateLoginStatus(pubkey); + } + }); + } else { + updateLoginStatus(null); + } + }); + }); + + // Check authentication when returning to the page + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + console.log('[AUTH-MANAGER] Page visibility changed to visible, checking auth state'); + checkAuthentication().then(isAuth => { + if (isAuth) { + getCurrentUserPubkeyAsync().then(pubkey => { + if (pubkey) { + updateLoginStatus(pubkey); + } + }); + } else { + updateLoginStatus(null); + } + }); + } + }); +} + +/** + * Update authentication state + * This is for backwards compatibility with existing code + * In the new implementation, we check with the extension directly instead of relying on storage + */ +export function updateAuthStateFromStorage(): void { + console.log('[AUTH-MANAGER] Checking authentication with extension'); + + // Check with the extension directly + checkAuthentication().then(isAuth => { + if (isAuth) { + getCurrentUserPubkeyAsync().then(pubkey => { + if (pubkey) { + console.log(`[AUTH-MANAGER] Found active pubkey: ${pubkey.substring(0, 8)}...`); + updateLoginStatus(pubkey); + + // Ensure the global window property is set + if (window) { + window.nostrPubkey = pubkey; + } + + // Notify listeners + notifyAuthStateListeners(true, pubkey); + } + }); + } else { + console.log('[AUTH-MANAGER] No active pubkey found'); + updateLoginStatus(null); + } + }); +} + +/** + * Set the authentication state + * This is for backwards compatibility with existing code * + * @param authenticated Whether the user is authenticated + * @param pubkey The user's public key (if authenticated) + */ +export function setAuthenticated(authenticated: boolean, pubkey?: string): void { + if (authenticated && pubkey) { + // Update in-memory state in AuthenticationService + authService['cachedPubkey'] = pubkey; + + // Update UI + updateLoginStatus(pubkey); + + // Ensure the global window property is set + if (window) { + window.nostrPubkey = pubkey; + } + + // Execute queued operations + executePostAuthQueue(); + + // Notify listeners + notifyAuthStateListeners(true, pubkey); + } else if (!authenticated) { + // Log out through the AuthenticationService + authService.logout(); + + // Update UI + updateLoginStatus(null); + + // Notify listeners + notifyAuthStateListeners(false); + } +} + +/** + * Create an authenticated HTTP request using NIP-98 (stub) + * @deprecated This method is maintained only for backwards compatibility * @param method HTTP method * @param url Request URL * @param payload Optional request payload - * @returns Headers object with Authorization header, or null if not authenticated + * @returns Always returns null */ export async function createAuthHeaders( method: string, url: string, payload?: string ): Promise<Headers | null> { - if (!isAuthenticated()) { - return null; - } - - // Use the AuthenticationService to create a NIP-98 auth event - const authEvent = await authService.createAuthEvent( - method as any, - url, - payload - ); - - if (!authEvent) { - return null; - } - - // Create headers from the auth event - return authService.createAuthHeaders(authEvent); + console.warn('auth-manager.createAuthHeaders is deprecated and will always return null'); + return null; } /** diff --git a/client/src/billboard.ts b/client/src/billboard.ts index 454df17..9f39e61 100644 --- a/client/src/billboard.ts +++ b/client/src/billboard.ts @@ -3,10 +3,14 @@ import * as nostrTools from 'nostr-tools'; +import * as authManager from './auth-manager'; import { defaultServerConfig } from './config'; import { NostrService } from './services/NostrService'; import { toggleTheme } from './theme-utils'; -import * as authManager from './auth-manager'; + +// Import nostr-login using CommonJS pattern to ensure it's available + +const NostrLogin = typeof require !== 'undefined' ? require('nostr-login') : null; // Module-level variables let nostrService: NostrService; @@ -23,6 +27,40 @@ document.addEventListener('DOMContentLoaded', () => { // Make sure authentication state is fresh from localStorage authManager.updateAuthStateFromStorage(); + // Initialize the login UI elements + authManager.initializeLoginUI(); + + // Add event listener to login button + const loginButton = document.getElementById('login-button'); + if (loginButton) { + loginButton.addEventListener('click', async () => { + console.log('Login button clicked'); + + try { + // Show login in progress + const statusElement = document.getElementById('login-status'); + if (statusElement) { + statusElement.textContent = 'Logging in...'; + } + + // Try to authenticate with the improved auth-manager + console.log('Attempting to login with AuthManager...'); + const pubkey = await authManager.loginWithNostrLogin(); + + if (pubkey) { + console.log(`Login successful with pubkey: ${pubkey}`); + updateUIBasedOnAuth(); + } else { + console.log('Login attempt failed, showing error'); + alert('Login failed. Please make sure you have a Nostr extension installed (like Alby or nos2x).'); + } + } catch (error) { + console.error('Authentication error:', error); + alert(`Authentication error: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }); + } + // Initialize services // Create NostrService with a status update callback nostrService = new NostrService((message: string, className: string) => { @@ -36,7 +74,6 @@ document.addEventListener('DOMContentLoaded', () => { // Auto-connect to the default relay after a brief delay setTimeout(autoConnectToDefaultRelay, 500); - setTimeout(autoConnectToDefaultRelay, 500); }); /** @@ -49,6 +86,13 @@ function updateUIBasedOnAuth(): void { // First refresh auth state from storage to ensure it's current authManager.updateAuthStateFromStorage(); + // Try to restore authentication state from localStorage if available + const savedPubkey = localStorage.getItem('userPublicKey'); + if (savedPubkey && !authManager.isAuthenticated()) { + console.log("UpdateUI: Found pubkey in localStorage but not authenticated, attempting to restore auth state"); + authManager.setAuthenticated(true, savedPubkey); + } + // Enable or disable the create button based on auth state if (authManager.isAuthenticated()) { createBillboardBtn.removeAttribute('disabled'); @@ -94,45 +138,53 @@ function setupUIElements(): void { authManager.updateAuthStateFromStorage(); // Check if user is authenticated + console.log("Billboard: Checking auth state..."); + + // Try to restore authentication state from localStorage if available + const savedPubkey = localStorage.getItem('userPublicKey'); + if (savedPubkey && !authManager.isAuthenticated()) { + console.log("Billboard: Found pubkey in localStorage but not authenticated, attempting to restore auth state"); + authManager.setAuthenticated(true, savedPubkey); + } + + // Check authentication again after potential restoration if (!authManager.isAuthenticated()) { - if (window.nostr) { - // If nostr extension is available, try to authenticate using our improved service - try { - const result = await authManager.authService.authenticate(); - if (result.success && result.session) { - console.log(`Successfully authenticated with pubkey: ${result.session.pubkey.substring(0, 8)}...`); - handleCreateBillboard(); - return; - } - } catch (error) { - console.error("Error during authentication:", error); - } + try { + console.log("Attempting authentication with NostrLogin..."); - // Fall back to basic authentication if needed - try { - const pubkey = await window.nostr.getPublicKey(); - if (pubkey) { - // Set authentication and proceed - authManager.setAuthenticated(true, pubkey); - console.log(`Direct login successful, pubkey: ${pubkey.substring(0, 8)}...`); - handleCreateBillboard(); - return; + // Use the NostrLogin integration + const pubkey = await authManager.loginWithNostrLogin(); + + if (pubkey) { + console.log(`Successfully authenticated with pubkey: ${pubkey.substring(0, 8)}...`); + updateUIBasedOnAuth(); + handleCreateBillboard(); + return; + } else { + // If NostrLogin failed, try to authenticate using extensions directly + if (window.nostr) { + try { + const pubkey = await window.nostr.getPublicKey(); + if (pubkey) { + // Set authentication and proceed + authManager.setAuthenticated(true, pubkey); + console.log(`Direct login successful, pubkey: ${pubkey.substring(0, 8)}...`); + updateUIBasedOnAuth(); + handleCreateBillboard(); + return; + } + } catch (error) { + console.error("Error getting pubkey from extension:", error); + } } - } catch (error) { - console.error("Error getting pubkey from extension:", error); + + console.warn('Authentication failed when trying to create billboard'); + return; } + } catch (error) { + console.error("Error during authentication:", error); + return; } - - // If we get here, authentication failed - show a more helpful message - const promptResult = confirm( - 'You need to be logged in to create a billboard.\nWould you like to go to the Profile page to log in now?' - ); - - if (promptResult) { - // Redirect to profile page - window.location.href = 'profile.html'; - } - return; } handleCreateBillboard(); @@ -207,20 +259,26 @@ function updateRelayStatus(message: string, className: string): void { async function handleConnectRelay(): Promise<void> { // Check if user is authenticated if (!authManager.isAuthenticated()) { - updateRelayStatus('Authentication required to connect', 'error'); - - // Display an authentication prompt - const billboardContent = document.getElementById('billboardContent'); - if (billboardContent) { - billboardContent.innerHTML = ` - <div class="auth-required-message"> - <h3>Authentication Required</h3> - <p>You need to be logged in to connect to relays.</p> - <p>Please visit the <a href="profile.html">Profile page</a> to log in with your Nostr extension.</p> - </div> - `; + try { + // Use AuthManager to authenticate + updateRelayStatus('Connecting to Nostr extension...', 'connecting'); + + // Try to authenticate using our improved auth-manager + const pubkey = await authManager.loginWithNostrLogin(); + + if (pubkey) { + // Authentication successful + updateUIBasedOnAuth(); + updateRelayStatus('Authentication successful. Connecting...', 'connecting'); + } else { + updateRelayStatus('Authentication failed. Please try again or install a Nostr extension like Alby or nos2x.', 'error'); + return; + } + } catch (error) { + console.error("Error during authentication:", error); + updateRelayStatus('Authentication error. Please check your Nostr extension.', 'error'); + return; } - return; } const relayUrlInput = document.getElementById('billboardRelayUrl') as HTMLInputElement; @@ -431,11 +489,28 @@ async function loadEventsFromCache(userPubkeyHex: string | null, showAllEvents: * Process a server advertisement event (kind 31120) * Open the modal for creating a new billboard */ -function handleCreateBillboard(): void { - // Check if user is logged in +async function handleCreateBillboard(): Promise<void> { + // First check if user is logged in + authManager.updateAuthStateFromStorage(); + if (!authManager.isAuthenticated()) { - alert('You need to be logged in to create a billboard. Please visit the Profile page to log in.'); - return; + // If user isn't logged in, try to authenticate with NostrLogin + try { + console.log("Authentication attempt from handleCreateBillboard"); + + const pubkey = await authManager.loginWithNostrLogin(); + + if (pubkey) { + // Update authentication state + updateUIBasedOnAuth(); + } else { + console.warn('Authentication failed when trying to create billboard'); + return; + } + } catch (error) { + console.error("Error during authentication:", error); + return; + } } // Reset form @@ -536,8 +611,23 @@ async function handleSaveBillboard(e: Event): Promise<void> { try { // Check if user is logged in using auth manager if (!authManager.isAuthenticated()) { - alert('You need to be logged in to publish a billboard. Please visit the Profile page to log in.'); - return; + // Try to authenticate with NostrLogin + try { + console.log("Authentication attempt from handleSaveBillboard"); + + const pubkey = await authManager.loginWithNostrLogin(); + + if (pubkey) { + // Update UI + updateUIBasedOnAuth(); + } else { + console.warn('Authentication failed when trying to publish billboard'); + return; + } + } catch (error) { + console.error("Error during authentication:", error); + return; + } } // Get user's pubkey @@ -813,14 +903,30 @@ function processServerEvent(event: nostrTools.Event): void { // Add event listener for the "Edit" button const editBtn = eventCard.querySelector('.billboard-edit-btn'); if (editBtn) { - editBtn.addEventListener('click', (e) => { + editBtn.addEventListener('click', async (e) => { const target = e.target as HTMLElement; const eventId = target.getAttribute('data-id'); if (eventId) { // Check if user is logged in const loggedInPubkey = nostrService.getLoggedInPubkey(); if (!loggedInPubkey) { - alert('You need to be logged in to edit a billboard. Please visit the Profile page to log in.'); + // Try to authenticate with NostrLogin + try { + console.log("Authentication attempt from edit button"); + + const pubkey = await authManager.loginWithNostrLogin(); + + if (pubkey) { + console.log(`Successfully authenticated with pubkey: ${pubkey.substring(0, 8)}...`); + updateUIBasedOnAuth(); + handleEditBillboard(event); + return; + } else { + console.warn('Authentication failed when trying to edit billboard'); + } + } catch (error) { + console.error("Error during authentication:", error); + } return; } @@ -865,7 +971,16 @@ async function autoConnectToDefaultRelay(): Promise<void> { // Update auth status from storage authManager.updateAuthStateFromStorage(); - // Try to authenticate directly with extension if possible + // Check if we have a saved pubkey in localStorage but aren't authenticated + const savedPubkey = localStorage.getItem('userPublicKey'); + if (savedPubkey && !authManager.isAuthenticated()) { + console.log(`AutoConnect: Found saved pubkey in localStorage: ${savedPubkey.substring(0, 8)}...`); + console.log("AutoConnect: Setting authenticated state based on localStorage"); + authManager.setAuthenticated(true, savedPubkey); + updateUIBasedOnAuth(); + } + + // If still not authenticated and nostr is available, try direct authentication if (!authManager.isAuthenticated() && window.nostr) { try { console.log('Attempting direct authentication with Nostr extension...'); @@ -938,70 +1053,10 @@ async function autoConnectToDefaultRelay(): Promise<void> { connectButton.click(); } } else { - console.log('User is not authenticated, showing login prompt...'); - // Show a login prompt with direct login option - const billboardContent = document.getElementById('billboardContent'); + console.log('User is not authenticated, skipping auto-connect...'); - // Only show the login prompt if no events were loaded from cache - if (billboardContent && billboardContent.querySelector('.empty-state')) { - billboardContent.innerHTML = ` - <div class="auth-required-message"> - <h3>Authentication Required</h3> - <p>You need to be logged in to view and manage billboards.</p> - <p> - <button id="directLoginBtn" class="primary-button">Login with Nostr Extension</button> - or visit the <a href="profile.html">Profile page</a> to login. - </p> - </div> - `; - - // Add event listener to the direct login button - const directLoginBtn = document.getElementById('directLoginBtn'); - if (directLoginBtn) { - directLoginBtn.addEventListener('click', async () => { - if (window.nostr) { - try { - directLoginBtn.textContent = 'Authenticating...'; - directLoginBtn.setAttribute('disabled', 'true'); - - // Try to authenticate using our improved service - const result = await authManager.authService.authenticate(); - if (result.success && result.session) { - console.log(`Direct login successful using authService, pubkey: ${result.session.pubkey.substring(0, 8)}...`); - - // Update UI and reload - updateUIBasedOnAuth(); - window.location.reload(); - return; - } - - // Fall back to basic pubkey retrieval - const pubkey = await window.nostr.getPublicKey(); - if (pubkey) { - authManager.setAuthenticated(true, pubkey); - localStorage.setItem('userPublicKey', pubkey); - console.log(`Direct login successful, pubkey: ${pubkey.substring(0, 8)}...`); - - // Update UI and connect - updateUIBasedOnAuth(); - - // Reload the page to properly initialize - window.location.reload(); - } - } catch (error) { - console.error('Error during direct login:', error); - directLoginBtn.textContent = 'Login Failed - Try Again'; - directLoginBtn.removeAttribute('disabled'); - } - } else { - alert('No Nostr extension detected. Please install NIP-07 compatible extension like nos2x or Alby.'); - } - }); - } - } - - // Update the relay status - updateRelayStatus('Authentication required', 'warning'); + // Just update the relay status without showing the auth required message + updateRelayStatus('Authentication required to connect', 'warning'); } } } @@ -1012,7 +1067,7 @@ async function autoConnectToDefaultRelay(): Promise<void> { * @returns Server pubkey or null if not found */ function getServerPubkeyFromEvent(event: nostrTools.Event): string | null { - if (event.kind !== 31120) return null; + if (event.kind !== 31120) {return null;} // Find the d tag which contains the server pubkey const dTag = event.tags.find(tag => tag[0] === 'd'); diff --git a/client/src/client-event-handler.ts b/client/src/client-event-handler.ts index 792035f..6911b76 100644 --- a/client/src/client-event-handler.ts +++ b/client/src/client-event-handler.ts @@ -9,10 +9,10 @@ * 4. Listening for incoming 21121 responses and associating them with requests */ -import { ClientEventStore, ClientEventStatus } from './services/ClientEventStore'; import { ClientEventsTable } from './components/ClientEventsTable'; import type { NostrEvent } from './relay'; -import { NostrRelayService } from './services/NostrRelayService'; +import { ClientEventStore, ClientEventStatus } from './services/ClientEventStore'; +import type { NostrRelayService } from './services/NostrRelayService'; // Create a singleton instance of the client event store const clientEventStore = new ClientEventStore(); diff --git a/client/src/client.ts b/client/src/client.ts index 0e754fa..5a88b2f 100644 --- a/client/src/client.ts +++ b/client/src/client.ts @@ -3,7 +3,6 @@ // IMPORTANT: Immediately import all critical modules and execute them // This ensures they are included in the bundle and executed immediately -import './navbar-diagnostics'; // Import diagnostics first import './navbar'; import './navbar-init'; @@ -37,16 +36,18 @@ import './navbar-init'; import * as nostrTools from 'nostr-tools'; // Import type definitions -import type { NostrEvent } from './converter'; -// Import functions from internal modules -import { displayConvertedEvent } from './converter'; -// Import client events tracking system +import * as authManager from './auth-manager'; import { initClientEventHandler, trackOutgoingEvent, handleIncomingResponse, reconnectRelayService } from './client-event-handler'; +import type { NostrEvent } from './converter'; +// Import functions from internal modules +import { displayConvertedEvent } from './converter'; +// Import client events tracking system +import { initializeNavbar } from './navbar'; import { publishToRelay, convertNpubToHex, verifyEvent } from './relay'; // Import profile functions (not using direct imports since we'll load modules based on page) // This ensures all page modules are included in the bundle @@ -54,11 +55,9 @@ import './profile'; import './receiver'; // Import receiver module for relay connections and subscriptions import './billboard'; // Import billboard module for server registration display import { HttpFormatter } from './services/HttpFormatter'; // Import formatter for HTTP content -import { NostrService } from './services/NostrService'; import { Nostr31120Service } from './services/Nostr31120Service'; // Import our new dedicated service +import { NostrService } from './services/NostrService'; import { getUserPubkey } from './services/NostrUtils'; -import { initializeNavbar } from './navbar'; -import * as authManager from './auth-manager'; // Immediately initialize the navbar to ensure it's visible on page load try { @@ -68,6 +67,7 @@ try { console.error('[CLIENT] Error initializing navbar:', e); } +import { toggleTheme } from './theme-utils'; import { sanitizeText, setDefaultHttpRequest, @@ -78,7 +78,6 @@ import { } from './utils'; // Import toggleTheme from theme-utils instead of utils -import { toggleTheme } from './theme-utils'; // On page load, always fetch the latest pubkey from window.nostr getUserPubkey().then(pubkey => { @@ -157,7 +156,7 @@ async function handleRelaySearch(): Promise<void> { if (!authManager.isAuthenticated()) { console.log('Cannot search relay: User not authenticated'); // Display authentication prompt instead of performing search - await retryAuthentication(); + await loginWithNostr(); // Check if authentication was successful if (!authManager.isAuthenticated()) { @@ -591,94 +590,45 @@ function loadSavedServer(): void { } /** - * Initialize nostr-login - * Prevents multiple initializations by checking if it's already been initialized + * Initialize direct interaction with Nostr extension + * Simplified to only check extension availability and current auth state */ function initNostrLogin(): void { - // Check if we've already initialized NostrLogin in this session - const nostrLoginInitialized = window.sessionStorage.getItem('nostrLoginInitialized'); - if (nostrLoginInitialized === 'true') { - console.log("NostrLogin already initialized in this session, skipping"); - return; + try { + // Simply check if we're already authenticated with the extension + if (window.nostr) { + console.log("Checking Nostr extension for authentication state..."); + + window.nostr.getPublicKey().then(pubkey => { + if (pubkey) { + console.log(`Found pubkey from extension: ${pubkey.slice(0, 8)}...`); + // Update UI to show pubkey + updateClientPubkeyDisplay(pubkey); + + // Update auth state in the service + authManager.setAuthenticated(true, pubkey); + } + }).catch(err => { + console.warn("Not currently connected to Nostr extension:", err); + }); + } else { + console.warn("Nostr extension not available"); } - - try { - // Initialize NostrLogin without requiring UI elements - if (NostrLogin && NostrLogin.init) { - // Create a temporary container if needed - const tempContainer = document.createElement('div'); - tempContainer.style.display = 'none'; - document.body.appendChild(tempContainer); - - console.log("Initializing NostrLogin..."); - - NostrLogin.init({ - element: tempContainer, - onConnect: (pubkey: string): void => { - console.log(`Connected to Nostr with pubkey: ${pubkey.slice(0, 8)}...`); - // Store pubkey in localStorage for other parts of the app - localStorage.setItem('userPublicKey', pubkey); - // Update UI to show pubkey - updateClientPubkeyDisplay(pubkey); - }, - onDisconnect: (): void => { - console.log('Disconnected from Nostr'); - localStorage.removeItem('userPublicKey'); - }, - // Add error handler to catch "Already started" errors - onError: (error: Error): void => { - console.error("NostrLogin error:", error); - - if (error.message === 'Already started') { - console.log("Detected 'Already started' error. Resetting state..."); - // Clear auth state manually - window.sessionStorage.removeItem('nostrLoginInitialized'); - ['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => { - window.sessionStorage.removeItem(key); - }); - } - } - }); - - // Mark as initialized to prevent duplicate initialization - window.sessionStorage.setItem('nostrLoginInitialized', 'true'); - - // Check if we can get an existing pubkey (already connected) - if (window.nostr) { - window.nostr.getPublicKey().then(pubkey => { - console.log(`Already connected with pubkey: ${pubkey.slice(0, 8)}...`); - localStorage.setItem('userPublicKey', pubkey); - updateClientPubkeyDisplay(pubkey); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`Error initializing Nostr check: ${errorMessage}`); + } +} + /** * Reset the Nostr login state in case of errors - * This can be used to recover from "Already started" errors + * This is simplified to use our stateless approach */ function resetNostrLoginState(): void { console.log("Resetting Nostr login state..."); - // Clear our initialization flags - window.sessionStorage.removeItem('nostrLoginInitialized'); - - // Clear any auth state that might be stuck - ['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => { - window.sessionStorage.removeItem(key); - }); - - // Clear any relevant localStorage items - // (but keep the user's pubkey if they were logged in) - ['nostrLoginTemporaryState'].forEach(key => { - localStorage.removeItem(key); - }); - - // If the NostrLogin library provides a reset method, use it - if (NostrLogin && typeof NostrLogin.reset === 'function') { - try { - NostrLogin.reset(); - console.log("NostrLogin.reset() called successfully"); - } catch (error) { - console.warn("Error calling NostrLogin.reset():", error); - } - } + // Reset the auth state in our service + authManager.clearAuthState(); console.log("Nostr login state has been reset. You can now try again."); } @@ -688,74 +638,37 @@ function resetNostrLoginState(): void { * @returns Promise resolving to whether the user is authenticated */ async function ensureAuthenticated(): Promise<boolean> { - // Check if we already have a pubkey in localStorage - const savedPubkey = localStorage.getItem('userPublicKey'); - if (savedPubkey) { - console.log(`Found saved pubkey: ${savedPubkey.substring(0, 8)}...`); + // Check directly with the Auth service, which checks the extension + const isAuth = await authManager.checkAuthentication(); + + if (isAuth) { + // Already authenticated + const pubkey = await authManager.getCurrentUserPubkeyAsync(); + if (pubkey) { + // Update UI with the pubkey + updateClientPubkeyDisplay(pubkey); + } return true; } - // If we don't have a pubkey, check if nostr is available + // Not authenticated, check if extension is available if (!window.nostr) { console.warn("Nostr extension not available"); return false; } - // Check if auth is already in progress - if (window.sessionStorage.getItem('nostrAuthInProgress') === 'true') { - console.log("Auth already in progress, resetting state first"); - resetNostrLoginState(); - // Wait a moment for the reset to take effect - await new Promise(resolve => setTimeout(resolve, 300)); - } - - // Set flag that we're starting auth - window.sessionStorage.setItem('nostrAuthInProgress', 'true'); - try { // Try to get the user's public key console.log("Starting Nostr authentication..."); - const pubkey = await window.nostr.getPublicKey(); + const result = await authManager.loginWithNostrLogin(); - if (pubkey) { - console.log(`Authentication successful, pubkey: ${pubkey.substring(0, 8)}...`); - // Store the pubkey for future use - localStorage.setItem('userPublicKey', pubkey); - // Update UI - updateClientPubkeyDisplay(pubkey); - return true; - } - - return false; + return result !== null; } catch (error) { console.error("Error during authentication:", error); - - // Handle "Already started" error specifically - if (error instanceof Error && error.message === 'Already started') { - console.log("Detected 'Already started' error. Resetting login state..."); - resetNostrLoginState(); - } - return false; - } finally { - // Clear the in-progress flag regardless of outcome - window.sessionStorage.removeItem('nostrAuthInProgress'); } } - }).catch(err => { - console.warn("Not connected to Nostr extension:", err); - }); - } - } else { - console.warn("NostrLogin initialization unavailable"); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`Error initializing Nostr login: ${errorMessage}`); - } -} - /** * Handle the publish button click @@ -1127,244 +1040,100 @@ function handleToggleKeyFormat(): void { } }); } - /** - * Initialize authentication and ensure the user is signed in - * This should be called at startup to check authentication status + * Simple function to authenticate using direct Nostr extension check */ -/** - * Fixed authentication flow that properly waits for completion - * This is a simpler approach that focuses on the specific issue - */ -async function safeAuthenticate(): Promise<string | null> { - console.log("Starting safe authentication..."); - - // Check if auth is already in progress to prevent duplicate attempts - if (window.authInProgress) { - console.log("Authentication already in progress, waiting..."); - return new Promise((resolve) => { - // Set up a small interval to check when the auth is complete - const checkInterval = setInterval(() => { - if (!window.authInProgress) { - clearInterval(checkInterval); - // After auth is complete, check if we have a pubkey - const pubkey = localStorage.getItem('userPublicKey'); - resolve(pubkey); - } - }, 100); - - // Set a timeout in case the auth gets stuck - setTimeout(() => { - clearInterval(checkInterval); - console.log("Authentication wait timeout, proceeding..."); - const pubkey = localStorage.getItem('userPublicKey'); - resolve(pubkey); - }, 5000); - }); - } - - // Set the global auth flag to prevent duplicate attempts - window.authInProgress = true; - +async function login(): Promise<string | null> { try { - // Clear any stale auth state - window.sessionStorage.removeItem('nostrLoginInitialized'); - ['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => { - window.sessionStorage.removeItem(key); - }); + // Use the auth service with direct extension check + const result = await authManager.authService.authenticate(); - // Initialize NostrLogin - initNostrLogin(); - - // Wait a moment for initialization to complete - await new Promise(resolve => setTimeout(resolve, 300)); - - // Check if we're already authenticated via AuthenticationService - if (authManager.authService.isAuthenticated()) { - console.log("Already authenticated via AuthenticationService"); - window.authInProgress = false; - const pubkey = authManager.getCurrentUserPubkey(); - if (pubkey) { - updateClientPubkeyDisplay(pubkey); - return pubkey; - } - } - - // If we already have a pubkey in localStorage, use that - const savedPubkey = localStorage.getItem('userPublicKey'); - if (savedPubkey) { - console.log(`Found saved pubkey: ${savedPubkey.substring(0, 8)}...`); - updateClientPubkeyDisplay(savedPubkey); - // Set authentication state in the central auth manager - authManager.setAuthenticated(true, savedPubkey); - window.authInProgress = false; - return savedPubkey; - } - } catch (error) { - console.error("Error in authentication setup:", error); - window.authInProgress = false; - return null; - } - - // Set a flag that we're starting auth - window.sessionStorage.setItem('nostrAuthInProgress', 'true'); - - try { - // If the extension is available, try to get the pubkey - if (window.nostr) { - console.log("Requesting public key from extension..."); + if (result.success && result.pubkey) { + const pubkey = result.pubkey; + console.log(`Authenticated with public key: ${pubkey.substring(0, 8)}...`); - // First try using our enhanced AuthenticationService - try { - const result = await authManager.authService.authenticate(); - if (result.success && result.session) { - const pubkey = result.session.pubkey; - console.log(`Authentication successful via AuthenticationService, pubkey: ${pubkey.substring(0, 8)}...`); - localStorage.setItem('userPublicKey', pubkey); - updateClientPubkeyDisplay(pubkey); - window.authInProgress = false; - return pubkey; - } - } catch (authServiceError) { - console.warn("AuthenticationService authentication failed, falling back to extension:", authServiceError); - } + // Update UI to show pubkey + updateClientPubkeyDisplay(pubkey); - // Fall back to direct extension request - const pubkey = await window.nostr.getPublicKey(); - if (pubkey) { - console.log(`Authentication successful via extension, pubkey: ${pubkey.substring(0, 8)}...`); - localStorage.setItem('userPublicKey', pubkey); - updateClientPubkeyDisplay(pubkey); - // Set authentication state in the central auth manager - authManager.setAuthenticated(true, pubkey); - window.authInProgress = false; - return pubkey; - } - } else { - console.warn("Nostr extension not available"); + return pubkey; } - window.authInProgress = false; return null; } catch (error) { - console.error("Error during authentication:", error); - window.authInProgress = false; + console.error('Login failed:', error); return null; - } finally { - // Clear the in-progress flag - window.sessionStorage.removeItem('nostrAuthInProgress'); - window.authInProgress = false; } } -// Set up auth state based on stored credentials but don't auto-authenticate -// This prevents automatic network requests on page load -(function() { - try { - const savedPubkey = localStorage.getItem('userPublicKey'); - if (savedPubkey) { - console.log(`Found saved pubkey: ${savedPubkey.substring(0, 8)}...`); - updateClientPubkeyDisplay(savedPubkey); - - // Update the auth state but don't make network requests yet - authManager.updateAuthStateFromStorage(); - } - } catch (error) { - console.error("Error checking stored authentication:", error); - } -})(); - -// Add a reset mechanism for auth state when navigating away from the page -// This helps ensure we don't get "Already started" errors on page reload -window.addEventListener('beforeunload', () => { - // Clear all auth-related state to ensure a clean start on the next page load - window.sessionStorage.removeItem('nostrLoginInitialized'); - - // Clear any temporary auth state in sessionStorage - // This is a preventative measure to avoid auth state conflicts - ['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => { - window.sessionStorage.removeItem(key); - }); -}); - -// Function to manually retry authentication if the user encounters issues -async function retryAuthentication(): Promise<void> { - console.log("Manual authentication retry requested"); - - // Check if auth is already in progress - if (window.authInProgress) { - console.log("Authentication already in progress, please wait..."); - - // If auth is in progress, just update UI - const statusElement = document.getElementById('authStatus'); - if (statusElement) { - statusElement.textContent = 'Authentication already in progress...'; - statusElement.className = 'status-message loading'; - } - - // Set a timeout to check if auth completed - setTimeout(() => { - if (authManager.isAuthenticated()) { - if (statusElement) { - statusElement.textContent = 'Authentication successful!'; - statusElement.className = 'status-message success'; - } - } - }, 5000); - - return; - } +/** + * Function to login with nostr + * Uses direct extension check without localStorage or sessions + */ +async function loginWithNostr(): Promise<void> { + console.log("Nostr login requested"); // Clear UI status const statusElement = document.getElementById('authStatus'); if (statusElement) { - statusElement.textContent = 'Retrying authentication...'; + statusElement.textContent = 'Logging in...'; statusElement.className = 'status-message loading'; } - // Force reset auth state to clear any stuck processes - window.authInProgress = false; - window.sessionStorage.removeItem('nostrLoginInitialized'); - ['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => { - window.sessionStorage.removeItem(key); - }); - - // Wait a moment for state to clear - await new Promise(resolve => setTimeout(resolve, 300)); - - // Try authentication again with our enhanced service first try { + // Use our direct extension check approach const result = await authManager.authService.authenticate(); - if (result.success && result.session) { - console.log(`Authentication successful via AuthenticationService: ${result.session.pubkey.substring(0, 8)}...`); - - // Update UI status - if (statusElement) { + + // Update UI based on result + if (statusElement) { + if (result.success && result.pubkey) { statusElement.textContent = 'Authentication successful!'; statusElement.className = 'status-message success'; + } else { + statusElement.textContent = result.error || 'Authentication failed. Please check your Nostr extension.'; + statusElement.className = 'status-message error'; } - - return; } } catch (error) { - console.warn("Could not authenticate with AuthenticationService, falling back:", error); - } - - // Fall back to standard authentication - const pubkey = await safeAuthenticate(); - - // Update status - if (statusElement) { - if (pubkey) { - statusElement.textContent = 'Authentication successful!'; - statusElement.className = 'status-message success'; - } else { + console.error("Error during authentication:", error); + + // Update UI status on error + if (statusElement) { statusElement.textContent = 'Authentication failed. Please check your Nostr extension.'; statusElement.className = 'status-message error'; } } } +// Export login function for use in other modules +export { login, loginWithNostr }; + +// Set up auth state by checking with extension but don't auto-authenticate +// This prevents automatic network requests on page load +(function() { + try { + // Check if we're authenticated with the extension + authManager.getCurrentUserPubkeyAsync().then(pubkey => { + if (pubkey) { + console.log(`Found active pubkey: ${pubkey.substring(0, 8)}...`); + updateClientPubkeyDisplay(pubkey); + } + }); + } catch (error) { + console.error("Error checking authentication status:", error); + } +})(); + +// Add a reset mechanism for auth state when navigating away from the page +// This helps ensure we don't get "Already started" errors on page reload +// No need to clear session storage on page unload anymore +// Since we're not using sessions for auth state +window.addEventListener('beforeunload', () => { + // Just clear any global state that might be problematic on reload + // but we no longer need to clear session storage + console.log('Page unloading, auth state will be re-checked on next load'); +}); + + // Initialize HTTP response viewer with raw/formatted view support and 21121 integration window.addEventListener('DOMContentLoaded', () => { try { @@ -1563,7 +1332,7 @@ function handleToggleClientKeyFormat(): void { try { if (currentFormat === 'npub') { // Switch to hex format - let hexValue = clientPubkeyElement.dataset.hex; + const hexValue = clientPubkeyElement.dataset.hex; if (hexValue) { clientPubkeyElement.textContent = hexValue; @@ -1573,7 +1342,7 @@ function handleToggleClientKeyFormat(): void { } } else { // Switch to npub format - let npubValue = clientPubkeyElement.dataset.npub; + const npubValue = clientPubkeyElement.dataset.npub; if (npubValue) { clientPubkeyElement.textContent = npubValue; @@ -1615,7 +1384,7 @@ document.addEventListener('DOMContentLoaded', function(): void { } }); - // Add auth status indicator and retry button to the UI + // Add sign in button or auth status based on authentication state const addAuthUI = (): void => { // Find a good place for the auth elements (navbar or header) const navbarRight = document.querySelector('.nav-right, header'); @@ -1628,37 +1397,59 @@ document.addEventListener('DOMContentLoaded', function(): void { authContainer.style.marginLeft = 'auto'; authContainer.style.marginRight = '10px'; - // Create status indicator - const authStatus = document.createElement('div'); - authStatus.id = 'authStatus'; - authStatus.className = 'status-message'; - authStatus.style.fontSize = '0.8em'; - authStatus.style.marginRight = '10px'; - // Check current auth status const pubkey = localStorage.getItem('userPublicKey'); + if (pubkey) { - authStatus.textContent = 'Authenticated'; + // User is authenticated, show status + const authStatus = document.createElement('div'); + authStatus.id = 'authStatus'; authStatus.className = 'status-message success'; + authStatus.style.fontSize = '0.8em'; + authStatus.style.marginRight = '10px'; + authStatus.textContent = 'Authenticated'; + + // Add sign out button + const signOutButton = document.createElement('button'); + signOutButton.textContent = 'Sign Out'; + signOutButton.className = 'auth-btn'; + signOutButton.style.fontSize = '0.8em'; + signOutButton.style.padding = '3px 8px'; + signOutButton.addEventListener('click', () => { + authManager.clearAuthState(); + localStorage.removeItem('userPublicKey'); + window.location.reload(); // Reload to update UI + }); + + // Add elements to container + authContainer.appendChild(authStatus); + authContainer.appendChild(signOutButton); } else { - authStatus.textContent = 'Not authenticated'; - authStatus.className = 'status-message warning'; + // User is not authenticated, show sign in button + const signInButton = document.createElement('button'); + signInButton.textContent = 'Sign in with Nostr'; + signInButton.className = 'auth-btn sign-in-btn'; + signInButton.style.fontSize = '0.8em'; + signInButton.style.padding = '3px 8px'; + signInButton.style.backgroundColor = 'var(--button-primary, #4285f4)'; + signInButton.style.color = 'white'; + signInButton.style.border = 'none'; + signInButton.style.borderRadius = '4px'; + signInButton.style.cursor = 'pointer'; + + signInButton.addEventListener('click', async () => { + await loginWithNostr(); + + // Reload the page after authentication attempt to update UI + if (authManager.isAuthenticated()) { + window.location.reload(); + } + }); + + // Add sign in button to container + authContainer.appendChild(signInButton); } - // Create retry button - const retryButton = document.createElement('button'); - retryButton.textContent = 'Retry Auth'; - retryButton.className = 'auth-retry-btn'; - retryButton.style.fontSize = '0.8em'; - retryButton.style.padding = '3px 8px'; - retryButton.addEventListener('click', async () => { - await retryAuthentication(); - }); - - // Add elements to container - authContainer.appendChild(authStatus); - authContainer.appendChild(retryButton); - // Add container to navbar navbarRight.appendChild(authContainer); } @@ -1700,7 +1491,7 @@ document.addEventListener('DOMContentLoaded', function(): void { } // Attempt authentication - await retryAuthentication(); + await loginWithNostr(); // If authentication succeeded, proceed with search if (authManager.isAuthenticated()) { @@ -1740,7 +1531,7 @@ document.addEventListener('DOMContentLoaded', function(): void { } // Attempt authentication - await retryAuthentication(); + await loginWithNostr(); } if (authManager.isAuthenticated()) { diff --git a/client/src/components/ClientEventsTable.ts b/client/src/components/ClientEventsTable.ts index f953aa6..c009fa1 100644 --- a/client/src/components/ClientEventsTable.ts +++ b/client/src/components/ClientEventsTable.ts @@ -5,12 +5,15 @@ * in a table format with a detailed view modal. */ -import { EventChangeType } from '../services/EventManager'; -import type { ClientEventStore, ClientStoredEvent } from '../services/ClientEventStore'; -import { ClientEventStatus } from '../services/ClientEventStore'; -import { HttpFormatter } from '../services/HttpFormatter'; import { nip19 } from 'nostr-tools'; + import { reconnectRelayService } from '../client-event-handler'; +import type { ClientEventStore } from '../services/ClientEventStore'; +import { ClientEventStatus } from '../services/ClientEventStore'; +import { EventChangeType } from '../services/EventManager'; +import { HttpFormatter } from '../services/HttpFormatter'; + + export class ClientEventsTable { private container: HTMLElement | null = null; @@ -270,7 +273,7 @@ export class ClientEventsTable { try { const npub = nip19.npubEncode(targetServerTag[1]); targetServer = `${npub.substring(0, 8)}...${npub.substring(npub.length - 4)}`; - } catch (e) { + } catch { targetServer = targetServerTag[1].substring(0, 8) + '...'; } } @@ -279,7 +282,7 @@ export class ClientEventsTable { const shortEventId = eventId.substring(0, 8) + '...'; // Determine status indicator - let statusHtml = this.getStatusHtml(storedEvent.status); + const statusHtml = this.getStatusHtml(storedEvent.status); // Set row HTML row.innerHTML = ` @@ -371,15 +374,19 @@ export class ClientEventsTable { private loadEventData(eventId: string): void { const storedEvent = this.eventStore.getEvent(eventId); - if (!storedEvent || !this.modal) return; + if (!storedEvent || !this.modal) { + return; + } // Get tab content containers const jsonTab = document.getElementById('tab-21120-json-content'); const httpRequestTab = document.getElementById('tab-21120-http-content'); const httpResponseTab = document.getElementById('tab-21121-http-content'); const jsonResponseTab = document.getElementById('tab-21121-json-content'); + if (!jsonTab || !httpRequestTab || !httpResponseTab || !jsonResponseTab) { + return; + } - if (!jsonTab || !httpRequestTab || !httpResponseTab || !jsonResponseTab) return; // Populate 21120 JSON tab jsonTab.innerHTML = `<pre>${JSON.stringify(storedEvent.event, null, 2)}</pre>`; @@ -387,7 +394,7 @@ export class ClientEventsTable { // Populate 21120 HTTP request tab try { httpRequestTab.innerHTML = HttpFormatter.formatHttpContent(storedEvent.event.content, true, true); - } catch (e) { + } catch { httpRequestTab.innerHTML = `<pre>${storedEvent.event.content}</pre>`; } @@ -407,7 +414,7 @@ export class ClientEventsTable { // Populate 21121 HTTP response tab try { httpResponseTab.innerHTML = HttpFormatter.formatHttpContent(responseEvent.content, false, true); - } catch (e) { + } catch { httpResponseTab.innerHTML = `<pre>${responseEvent.content}</pre>`; } } else { @@ -517,7 +524,9 @@ export class ClientEventsTable { } private showModal(eventId: string): void { - if (!this.modal) return; + if (!this.modal) { + return; + } this.currentEventId = eventId; @@ -535,7 +544,9 @@ export class ClientEventsTable { } private hideModal(): void { - if (!this.modal) return; + if (!this.modal) { + return; + } this.modal.style.display = 'none'; this.currentEventId = null; @@ -543,7 +554,9 @@ export class ClientEventsTable { private switchTab(event: Event): void { const clickedTab = event.currentTarget as HTMLElement; - if (!clickedTab || !clickedTab.dataset.tabId) return; + if (!clickedTab || !clickedTab.dataset.tabId) { + return; + } // Hide all tab contents const tabContents = document.querySelectorAll('.modal-tab-content'); diff --git a/client/src/components/EventDetail.ts b/client/src/components/EventDetail.ts index 91ae4c1..bfa32a3 100644 --- a/client/src/components/EventDetail.ts +++ b/client/src/components/EventDetail.ts @@ -4,7 +4,8 @@ */ import { NostrEvent } from '../relay'; -import { EventManager, EventChangeType } from '../services/EventManager'; +import type { EventManager} from '../services/EventManager'; +import { EventChangeType } from '../services/EventManager'; import { HttpFormatter } from '../services/HttpFormatter'; /** @@ -92,7 +93,7 @@ export class EventDetail { * Show empty state when no event is selected */ private showEmptyState(): void { - if (!this.container) return; + if (!this.container) {return;} this.container.innerHTML = ` <div class="empty-state"> @@ -105,7 +106,7 @@ export class EventDetail { * Render the details of the currently selected event */ private renderEventDetail(): void { - if (!this.container) return; + if (!this.container) {return;} // Get the selected event from the EventManager const managedEvent = this.eventManager.getSelectedEvent(); @@ -121,7 +122,7 @@ export class EventDetail { const isResponse = event.kind === 21121; // Determine the content to display - let httpContent = managedEvent.decrypted ? + const httpContent = managedEvent.decrypted ? managedEvent.decryptedContent || event.content : event.content; @@ -223,7 +224,7 @@ export class EventDetail { * Set up tab buttons for switching between raw and formatted views */ private setupTabButtons(): void { - if (!this.container) return; + if (!this.container) {return;} const tabButtons = this.container.querySelectorAll('.tab-btn'); tabButtons.forEach(button => { @@ -251,7 +252,7 @@ export class EventDetail { * Set up event listeners for related events and action buttons */ private setupEventListeners(): void { - if (!this.container) return; + if (!this.container) {return;} // Related event links const relatedLinks = this.container.querySelectorAll('.related-event-link'); diff --git a/client/src/components/EventList.ts b/client/src/components/EventList.ts index 9238be3..b048681 100644 --- a/client/src/components/EventList.ts +++ b/client/src/components/EventList.ts @@ -3,8 +3,9 @@ * Modular UI component for rendering and managing a list of Nostr events */ -import { NostrEvent } from '../relay'; -import { EventManager, EventChangeType } from '../services/EventManager'; +import type { NostrEvent } from '../relay'; +import type { EventManager} from '../services/EventManager'; +import { EventChangeType } from '../services/EventManager'; /** * Options for initializing the EventList component @@ -109,7 +110,7 @@ export class EventList { * Create the UI structure for the event list and controls */ private createUIStructure(): void { - if (!this.container) return; + if (!this.container) {return;} // Create the header with enhanced search and filter controls const header = document.createElement('div'); @@ -343,7 +344,7 @@ export class EventList { private saveCurrentFilter(): void { // Create a dialog to name the filter const filterName = prompt('Enter a name for this filter:', 'Filter ' + (this.savedFilters.length + 1)); - if (!filterName) return; + if (!filterName) {return;} // Save current filter configuration const filterConfig = { @@ -370,7 +371,7 @@ export class EventList { */ private updateSavedFiltersList(): void { const savedFiltersList = document.getElementById('savedFiltersList'); - if (!savedFiltersList) return; + if (!savedFiltersList) {return;} if (this.savedFilters.length === 0) { savedFiltersList.innerHTML = '<div class="empty-saved-filters">No saved filters</div>'; @@ -409,7 +410,7 @@ export class EventList { * Apply a saved filter */ private applyFilter(index: number): void { - if (index < 0 || index >= this.savedFilters.length) return; + if (index < 0 || index >= this.savedFilters.length) {return;} const filter = this.savedFilters[index].config; @@ -450,7 +451,7 @@ export class EventList { * Delete a saved filter */ private deleteFilter(index: number): void { - if (index < 0 || index >= this.savedFilters.length) return; + if (index < 0 || index >= this.savedFilters.length) {return;} // Remove the filter this.savedFilters.splice(index, 1); @@ -478,7 +479,7 @@ export class EventList { */ private setupVirtualScrolling(): void { const eventsListWrapper = this.eventsListContainer?.parentElement; - if (!eventsListWrapper) return; + if (!eventsListWrapper) {return;} // Create intersection observer to detect visible events this.scrollObserver = new IntersectionObserver( @@ -516,7 +517,7 @@ export class EventList { * Render all existing events from the EventManager */ private renderExistingEvents(): void { - if (!this.eventsListContainer) return; + if (!this.eventsListContainer) {return;} // Clear existing content and tracked events this.eventsListContainer.innerHTML = ''; @@ -537,7 +538,7 @@ export class EventList { this.filteredEventIds.sort((a, b) => { const eventA = this.eventManager.getEvent(a); const eventB = this.eventManager.getEvent(b); - if (!eventA || !eventB) return 0; + if (!eventA || !eventB) {return 0;} return eventB.receivedAt - eventA.receivedAt; }); @@ -555,7 +556,7 @@ export class EventList { * Update which events are visible in the virtual scroll view */ private updateVisibleEvents(): void { - if (!this.eventsListContainer || !this.eventsListContainer.parentElement) return; + if (!this.eventsListContainer || !this.eventsListContainer.parentElement) {return;} const scrollContainer = this.eventsListContainer.parentElement; const scrollTop = scrollContainer.scrollTop; @@ -616,14 +617,14 @@ export class EventList { // Apply filters to get filtered IDs this.filteredEventIds = this.allEventIds.filter(eventId => { const managedEvent = this.eventManager.getEvent(eventId); - if (!managedEvent) return false; + if (!managedEvent) {return false;} const event = managedEvent.event; // Filter by server if needed if (!this.showAllEvents) { const isToServer = this.checkIfToServer(event); - if (!isToServer) return false; + if (!isToServer) {return false;} } // Filter by event type @@ -680,7 +681,7 @@ export class EventList { this.filteredEventIds.sort((a, b) => { const eventA = this.eventManager.getEvent(a); const eventB = this.eventManager.getEvent(b); - if (!eventA || !eventB) return 0; + if (!eventA || !eventB) {return 0;} return eventB.receivedAt - eventA.receivedAt; }); @@ -701,7 +702,7 @@ export class EventList { // Legacy code for backward compatibility const eventsList = document.getElementById('eventsList'); - if (!eventsList) return; + if (!eventsList) {return;} const items = eventsList.querySelectorAll('.event-item'); let visibleCount = 0; @@ -739,7 +740,7 @@ export class EventList { * Show empty state when there are no events */ private showEmptyState(): void { - if (!this.eventsListContainer) return; + if (!this.eventsListContainer) {return;} // Clear virtual scrolling data this.filteredEventIds = []; @@ -762,11 +763,11 @@ export class EventList { private checkIfToServer(event: NostrEvent): boolean { // Get server pubkey from EventManager const serverPubkey = this.eventManager.getServerPubkey(); - if (!serverPubkey) return false; + if (!serverPubkey) {return false;} // Check for p tag to identify recipient const pTag = event.tags.find(tag => tag[0] === 'p'); - if (!pTag || pTag.length <= 1) return false; + if (!pTag || pTag.length <= 1) {return false;} // Check if the p tag matches our server pubkey return (pTag[1] === serverPubkey); @@ -778,7 +779,7 @@ export class EventList { private getRecipientDisplay(event: NostrEvent): string { // Find recipient if any const pTag = event.tags.find(tag => tag[0] === 'p'); - if (!pTag || pTag.length <= 1) return ''; + if (!pTag || pTag.length <= 1) {return '';} return `<div class="recipient">To: ${pTag[1].substring(0, 8)}...</div>`; } @@ -788,11 +789,11 @@ export class EventList { */ private renderEventItem(eventId: string): HTMLElement | null { const eventsList = document.getElementById('eventsList'); - if (!eventsList) return null; + if (!eventsList) {return null;} // Get the event from EventManager const managedEvent = this.eventManager.getEvent(eventId); - if (!managedEvent) return null; + if (!managedEvent) {return null;} const event = managedEvent.event; @@ -861,10 +862,10 @@ export class EventList { const statusCode = statusMatch[1]; let statusClass = ''; - if (statusCode.startsWith('2')) statusClass = 'status-success'; - else if (statusCode.startsWith('3')) statusClass = 'status-redirect'; - else if (statusCode.startsWith('4')) statusClass = 'status-client-error'; - else if (statusCode.startsWith('5')) statusClass = 'status-server-error'; + if (statusCode.startsWith('2')) {statusClass = 'status-success';} + else if (statusCode.startsWith('3')) {statusClass = 'status-redirect';} + else if (statusCode.startsWith('4')) {statusClass = 'status-client-error';} + else if (statusCode.startsWith('5')) {statusClass = 'status-server-error';} httpContext = `<div class="http-preview ${statusClass}">Status: ${statusCode}</div>`; } @@ -1094,7 +1095,7 @@ export class EventList { * Highlight the selected event in the UI */ private highlightSelectedEvent(eventId: string): void { - if (!this.eventsListContainer || !this.eventsListContainer.parentElement) return; + if (!this.eventsListContainer || !this.eventsListContainer.parentElement) {return;} // 1. Remove selected class from all visible items this.visibleEvents.forEach((element) => { @@ -1172,7 +1173,7 @@ export class EventList { private displayResponsesForRequest(requestId: string): void { // Find the responses list container const responsesListContainer = document.getElementById('responsesList'); - if (!responsesListContainer) return; + if (!responsesListContainer) {return;} // Get related response events from EventManager const responses = this.eventManager.getResponsesForRequest(requestId); @@ -1210,10 +1211,10 @@ export class EventList { const statusCode = statusMatch[1]; let statusClass = ''; - if (statusCode.startsWith('2')) statusClass = 'status-success'; - else if (statusCode.startsWith('3')) statusClass = 'status-redirect'; - else if (statusCode.startsWith('4')) statusClass = 'status-client-error'; - else if (statusCode.startsWith('5')) statusClass = 'status-server-error'; + if (statusCode.startsWith('2')) {statusClass = 'status-success';} + else if (statusCode.startsWith('3')) {statusClass = 'status-redirect';} + else if (statusCode.startsWith('4')) {statusClass = 'status-client-error';} + else if (statusCode.startsWith('5')) {statusClass = 'status-server-error';} statusInfo = `<span class="status-code ${statusClass}">Status: ${statusCode}</span>`; } @@ -1253,14 +1254,14 @@ export class EventList { */ private displayResponseJson(event: NostrEvent): void { const jsonContainer = document.getElementById('response21121Json'); - if (!jsonContainer) return; + if (!jsonContainer) {return;} // Make container visible jsonContainer.style.display = 'block'; // Get the pre element const pre = jsonContainer.querySelector('pre.json-content'); - if (!pre) return; + if (!pre) {return;} // Format JSON with indentation pre.textContent = JSON.stringify(event, null, 2); diff --git a/client/src/components/HttpMessagesTable.ts b/client/src/components/HttpMessagesTable.ts index a4503e2..83e0b2c 100644 --- a/client/src/components/HttpMessagesTable.ts +++ b/client/src/components/HttpMessagesTable.ts @@ -5,9 +5,10 @@ * Includes a modal dialog to show detailed information when a row is clicked */ +import { nip19 } from 'nostr-tools'; + import { EventChangeType, EventKind } from '../services/EventManager'; import type { EventManager } from '../services/EventManager'; -import { nip19 } from 'nostr-tools'; import { HttpFormatter } from '../services/HttpFormatter'; export class HttpMessagesTable { @@ -302,7 +303,7 @@ export class HttpMessagesTable { * @param eventId The ID of the event to show details for */ private showModal(eventId: string): void { - if (!this.modal) return; + if (!this.modal) {return;} this.currentEventId = eventId; @@ -323,7 +324,7 @@ export class HttpMessagesTable { * Hide the modal dialog */ private hideModal(): void { - if (!this.modal) return; + if (!this.modal) {return;} this.modal.style.display = 'none'; this.currentEventId = null; @@ -335,7 +336,7 @@ export class HttpMessagesTable { */ private switchTab(event: Event): void { const clickedTab = event.currentTarget as HTMLElement; - if (!clickedTab || !clickedTab.dataset.tabId) return; + if (!clickedTab || !clickedTab.dataset.tabId) {return;} // Hide all tab contents const tabContents = document.querySelectorAll('.modal-tab-content'); @@ -565,7 +566,7 @@ export class HttpMessagesTable { * This can be called when a new 21121 response is received */ public updateResponseIndicators(): void { - if (!this.tableBody) return; + if (!this.tableBody) {return;} const rows = this.tableBody.querySelectorAll('.event-row'); rows.forEach(row => { diff --git a/client/src/components/HttpRequestExecutor.ts b/client/src/components/HttpRequestExecutor.ts index de83580..7b65d52 100644 --- a/client/src/components/HttpRequestExecutor.ts +++ b/client/src/components/HttpRequestExecutor.ts @@ -6,9 +6,9 @@ */ import { NostrEvent } from '../relay'; -import { HttpClient } from '../services/HttpClient'; +import type { EventManager } from '../services/EventManager'; +import type { HttpClient } from '../services/HttpClient'; import { ToastNotifier } from '../services/ToastNotifier'; -import { EventManager } from '../services/EventManager'; // Result of an HTTP request execution export interface ExecutionResult { diff --git a/client/src/components/Nostr21121Creator.ts b/client/src/components/Nostr21121Creator.ts index e292d7c..ad3a36c 100644 --- a/client/src/components/Nostr21121Creator.ts +++ b/client/src/components/Nostr21121Creator.ts @@ -9,9 +9,10 @@ * - Publishing to relays */ -import { NostrEvent } from '../relay'; import * as nostrTools from 'nostr-tools'; -import { EventManager } from '../services/EventManager'; + +import type { NostrEvent } from '../relay'; +import type { EventManager } from '../services/EventManager'; import { ToastNotifier } from '../services/ToastNotifier'; /** @@ -185,7 +186,7 @@ export class Nostr21121Creator { const pubKey = nostrTools.getPublicKey(privateKeyBytes); // Initialize tags array - let tags: string[][] = []; + const tags: string[][] = []; // Always add reference to the request event if (requestEvent.id) { @@ -197,7 +198,7 @@ export class Nostr21121Creator { // Check if the original event has a p tag (recipient) const pTag = requestEvent.tags.find(tag => tag[0] === 'p'); - let finalContent = responseContent; + const finalContent = responseContent; if (pTag && pTag[1]) { // Add p tag to reference the recipient diff --git a/client/src/components/ResponseViewer.ts b/client/src/components/ResponseViewer.ts index 075b5b6..5b42258 100644 --- a/client/src/components/ResponseViewer.ts +++ b/client/src/components/ResponseViewer.ts @@ -3,10 +3,11 @@ * Handles HTTP response display and 21121 response event creation */ -import { EventManager } from '../services/EventManager'; +import type { EventManager } from '../services/EventManager'; import { HttpFormatter } from '../services/HttpFormatter'; import { ToastNotifier } from '../services/ToastNotifier'; -import { ExecutionResult } from './HttpRequestExecutor'; + +import type { ExecutionResult } from './HttpRequestExecutor'; import { Nostr21121Creator, Creation21121Result } from './Nostr21121Creator'; /** @@ -163,7 +164,7 @@ export class ResponseViewer { * Add a button to create a 21121 response event if not already present */ private addCreate21121Button(): void { - if (!this.modalElement) return; + if (!this.modalElement) {return;} // Create or update status element if (!this.creationStatus) { @@ -180,11 +181,11 @@ export class ResponseViewer { // Check if button already exists const existingButton = this.modalElement.querySelector('.create-21121-btn'); - if (existingButton) return; + if (existingButton) {return;} // Get the modal header const modalHeader = this.modalElement.querySelector('.http-response-header'); - if (!modalHeader) return; + if (!modalHeader) {return;} // Create button const button = document.createElement('button'); @@ -205,7 +206,7 @@ export class ResponseViewer { * Set up event listeners for modal interactions */ private setupModalEventListeners(): void { - if (!this.modalElement) return; + if (!this.modalElement) {return;} // Handle close button click const closeBtn = this.modalElement.querySelector('.close-modal-btn'); @@ -223,7 +224,7 @@ export class ResponseViewer { button.addEventListener('click', () => { // Get the tab ID const tabId = (button as HTMLElement).dataset.tab; - if (!tabId) return; + if (!tabId) {return;} // Remove active class from all buttons and content tabButtons.forEach(btn => btn.classList.remove('active')); @@ -272,7 +273,7 @@ export class ResponseViewer { if (hasResponse) { const shouldOverwrite = confirm('A response already exists for this request. Create another one?'); - if (!shouldOverwrite) return; + if (!shouldOverwrite) {return;} } // Generate a sample response @@ -312,7 +313,7 @@ export class ResponseViewer { * Show a dialog with options for creating a 21121 response */ private showCreateResponseDialog(requestEventId: string, responseContent: string): void { - if (!this.modalElement || !this.creationStatus) return; + if (!this.modalElement || !this.creationStatus) {return;} // Update and show the status element this.creationStatus.className = 'creation-status info'; diff --git a/client/src/components/ServerUI.ts b/client/src/components/ServerUI.ts index 447d159..957659c 100644 --- a/client/src/components/ServerUI.ts +++ b/client/src/components/ServerUI.ts @@ -4,19 +4,21 @@ */ import { EventManager } from '../services/EventManager'; +import { HttpClient } from '../services/HttpClient'; +import { HttpService } from '../services/HttpService'; +import { Nostr21121EventHandler } from '../services/Nostr21121EventHandler'; +import { NostrCacheService } from '../services/NostrCacheService'; import { NostrEventService } from '../services/NostrEventService.updated'; import { NostrRelayService } from '../services/NostrRelayService'; -import { NostrCacheService } from '../services/NostrCacheService'; -import { HttpService } from '../services/HttpService'; -import { HttpClient } from '../services/HttpClient'; +import { NostrService } from '../services/NostrService'; import { ToastNotifier } from '../services/ToastNotifier'; + +import { EventDetail } from './EventDetail'; +import { EventList } from './EventList'; +import { HttpMessagesTable } from './HttpMessagesTable'; import { HttpRequestExecutor } from './HttpRequestExecutor'; import { ResponseViewer } from './ResponseViewer'; -import { EventList } from './EventList'; -import { EventDetail } from './EventDetail'; -import { HttpMessagesTable } from './HttpMessagesTable'; -import { Nostr21121EventHandler } from '../services/Nostr21121EventHandler'; -import { NostrService } from '../services/NostrService'; + /** @@ -232,7 +234,7 @@ export class ServerUI { */ private connectToRelay(): void { const relayUrlInput = document.getElementById(this.options.relayUrlInput) as HTMLInputElement; - if (!relayUrlInput) return; + if (!relayUrlInput) {return;} const relayUrl = relayUrlInput.value.trim() || 'wss://relay.degmods.com'; if (!relayUrl) { diff --git a/client/src/converter.ts b/client/src/converter.ts index 9d83989..9427a5c 100644 --- a/client/src/converter.ts +++ b/client/src/converter.ts @@ -31,7 +31,7 @@ declare global { } // Import nostr-login for encryption/signing -// eslint-disable-next-line no-undef + // Define better types for the NostrLogin module interface NostrLoginModule { signEvent: (event: NostrEvent) => Promise<NostrEvent>; diff --git a/client/src/http-response-viewer.ts b/client/src/http-response-viewer.ts index b80ef20..7e711bd 100644 --- a/client/src/http-response-viewer.ts +++ b/client/src/http-response-viewer.ts @@ -19,7 +19,7 @@ export function initHttpResponseViewer(): void { // Handle tab switching if (target && target.classList.contains('tab-btn')) { const tabContainer = target.closest('.http-response-tabs, .event-detail-tabs'); - if (!tabContainer) return; + if (!tabContainer) {return;} // Get all tab buttons and content in this container const tabButtons = tabContainer.querySelectorAll('.tab-btn'); @@ -31,7 +31,7 @@ export function initHttpResponseViewer(): void { tabContentContainer = tabContainer.closest('.modal-content, .event-details'); } - if (!tabContentContainer) return; + if (!tabContentContainer) {return;} const tabContents = tabContentContainer.querySelectorAll('.tab-content'); @@ -100,24 +100,24 @@ async function executeHttpRequest(button: HTMLElement): Promise<void> { try { // Find the HTTP content const eventDetails = button.closest('.event-details'); - if (!eventDetails) throw new Error('Event details not found'); + if (!eventDetails) {throw new Error('Event details not found');} // Find the HTTP content element - look in both formatted and raw tabs const httpContentElement = eventDetails.querySelector('#raw-http .http-content') || eventDetails.querySelector('.http-content'); - if (!httpContentElement) throw new Error('HTTP content not found'); + if (!httpContentElement) {throw new Error('HTTP content not found');} const httpContent = httpContentElement.textContent || ''; - if (!httpContent.trim()) throw new Error('Empty HTTP content'); + if (!httpContent.trim()) {throw new Error('Empty HTTP content');} // Get the event ID const headerElement = eventDetails.querySelector('.event-detail-header h3'); const eventIdMatch = headerElement?.textContent?.match(/ID: (\w+)\.\.\./); const eventId = eventIdMatch ? eventIdMatch[1] : null; - if (!eventId) throw new Error('Could not determine event ID'); + if (!eventId) {throw new Error('Could not determine event ID');} // Execute the HTTP request const response = await executeRequest(httpContent); @@ -161,7 +161,7 @@ async function executeRequest(requestContent: string): Promise<string> { // Extract the host let host = ''; - let headers: Record<string, string> = {}; + const headers: Record<string, string> = {}; let body = ''; let inHeaders = true; diff --git a/client/src/http-response-viewer.updated.ts b/client/src/http-response-viewer.updated.ts index d6144f0..e700f8a 100644 --- a/client/src/http-response-viewer.updated.ts +++ b/client/src/http-response-viewer.updated.ts @@ -3,12 +3,13 @@ * Handles displaying HTTP responses and 21121 integration * Refactored to use EventManager for centralized event data management */ -import { NostrEvent } from './relay'; -import { HttpFormatter } from './services/HttpFormatter'; -import { ToastNotifier } from './services/ToastNotifier'; -import { EventManager, EventKind, EventChangeType } from './services/EventManager'; -import { HttpService } from './services/HttpService'; +import type { NostrEvent } from './relay'; +import type { EventManager} from './services/EventManager'; +import { EventKind, EventChangeType } from './services/EventManager'; import { HttpClient } from './services/HttpClient'; +import { HttpFormatter } from './services/HttpFormatter'; +import { HttpService } from './services/HttpService'; +import { ToastNotifier } from './services/ToastNotifier'; // Services that will be dynamically imported to avoid circular dependencies let nostrService: any = null; @@ -36,7 +37,7 @@ export function initHttpResponseViewer( // Handle tab switching if (target && target.classList.contains('tab-btn')) { const tabContainer = target.closest('.http-response-tabs, .event-detail-tabs'); - if (!tabContainer) return; + if (!tabContainer) {return;} // Get all tab buttons and content in this container const tabButtons = tabContainer.querySelectorAll('.tab-btn'); @@ -48,7 +49,7 @@ export function initHttpResponseViewer( tabContentContainer = tabContainer.closest('.modal-content, .event-details'); } - if (!tabContentContainer) return; + if (!tabContentContainer) {return;} const tabContents = tabContentContainer.querySelectorAll('.tab-content'); diff --git a/client/src/navbar-diagnostics.ts b/client/src/navbar-diagnostics.ts deleted file mode 100644 index 62dafa6..0000000 --- a/client/src/navbar-diagnostics.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * navbar-diagnostics.ts - * Diagnostic tool to help debug navbar issues - */ - -// Simple IIFE to run diagnostics and attempt to fix navbar issues -(function() { - // Create a diagnostic function that's accessible from the console - function runNavbarDiagnostics() { - console.log('%c[NAVBAR DIAGNOSTICS]', 'background: #ff0000; color: white; padding: 5px; font-size: 16px;'); - - // Check if the container exists - const navbarContainer = document.getElementById('navbarContainer'); - console.log('1. Navbar container exists:', !!navbarContainer); - - // If no container, this is a critical issue - if (!navbarContainer) { - console.error('CRITICAL: Navbar container not found!'); - console.log('Attempting to create navbar container...'); - - // Create a new navbar container - const newContainer = document.createElement('div'); - newContainer.id = 'navbarContainer'; - newContainer.className = 'top-nav'; - - // Insert it at the top of the body - document.body.insertBefore(newContainer, document.body.firstChild); - console.log('Created new navbar container:', !!document.getElementById('navbarContainer')); - } - - // Check if navbarContainer has content - const navbarContent = document.querySelector('.nav-left, .nav-right'); - console.log('2. Navbar has content:', !!navbarContent); - - if (!navbarContent) { - console.log('Navbar has no content. Attempting to fix...'); - - // Try to initialize the navbar - if (window.hasOwnProperty('initializeNavbar')) { - console.log('Calling initializeNavbar function...'); - try { - (window as any).initializeNavbar(); - } catch (error) { - console.error('Error calling initializeNavbar:', error); - } - } else { - console.error('initializeNavbar function not found globally!'); - // Try to manually import and call - console.log('Attempting manual import...'); - try { - // Get the current page - const currentPage = window.location.pathname.split('/').pop() || 'index.html'; - - // Create emergency navbar content - const emergencyNavbar = document.getElementById('navbarContainer'); - if (emergencyNavbar) { - const navbarHtml = ` - <div class="nav-left"> - <a href="./index.html" class="nav-link${currentPage === 'index.html' ? ' active' : ''}">HOME</a> - <a href="./1120_client.html" class="nav-link${currentPage === '1120_client.html' ? ' active' : ''}">CLIENT</a> - <a href="./1120_server.html" class="nav-link${currentPage === '1120_server.html' ? ' active' : ''}">SERVER</a> - <a href="./billboard.html" class="nav-link${currentPage === 'billboard.html' ? ' active' : ''}">BILLBOARD</a> - </div> - <div class="nav-right"> - <a href="./index.html" class="nav-link nav-icon${currentPage === 'index.html' ? ' active' : ''}" title="Documentation">❓</a> - <a href="./profile.html" class="nav-link nav-icon${currentPage === 'profile.html' ? ' active' : ''}" title="Profile">👤</a> - <button id="nuclearResetBtn" class="nuclear-reset-btn" title="Reset All Data">💣</button> - <button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Dark Mode"> - <span id="themeIcon">🌙</span> - </button> - </div>`; - emergencyNavbar.innerHTML = navbarHtml; - console.log('Added emergency navbar HTML!'); - } - } catch (error) { - console.error('Error creating emergency navbar:', error); - } - } - } - - // Check CSS visibility issues - const navbar = document.getElementById('navbarContainer'); - if (navbar) { - const styles = window.getComputedStyle(navbar); - console.log('3. Navbar CSS visibility check:'); - console.log(' - display:', styles.display); - console.log(' - visibility:', styles.visibility); - console.log(' - opacity:', styles.opacity); - console.log(' - height:', styles.height); - - // Fix any CSS issues - if (styles.display === 'none') { - console.log('Fixing display:none issue...'); - navbar.style.display = 'flex'; - } - if (styles.visibility === 'hidden') { - console.log('Fixing visibility:hidden issue...'); - navbar.style.visibility = 'visible'; - } - if (styles.opacity === '0') { - console.log('Fixing opacity:0 issue...'); - navbar.style.opacity = '1'; - } - if (parseFloat(styles.height) === 0) { - console.log('Fixing zero height issue...'); - navbar.style.height = 'auto'; - } - } - - // Final check - const navbarContentAfterFix = document.querySelector('.nav-left, .nav-right'); - console.log('4. Navbar fixed successfully:', !!navbarContentAfterFix); - - // Return result of diagnostics - return { - containerExists: !!navbarContainer, - hasContent: !!navbarContentAfterFix - }; - } - - // Make the diagnostics function globally available - if (typeof window !== 'undefined') { - (window as any).runNavbarDiagnostics = runNavbarDiagnostics; - } - - // Run diagnostics on page load - window.addEventListener('load', function() { - console.log('Running navbar diagnostics on window.load...'); - setTimeout(runNavbarDiagnostics, 1000); - }); - - // Also run it after DOM content is loaded - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { - console.log('Running navbar diagnostics on DOMContentLoaded...'); - runNavbarDiagnostics(); - }); - } else { - // If already loaded, run it now - console.log('Document already loaded, running navbar diagnostics immediately...'); - runNavbarDiagnostics(); - } -})(); - -// Export a dummy function so TypeScript treats this as a module -export function navbarDiagnostics(): void { - console.log('Navbar diagnostics module loaded'); -} \ No newline at end of file diff --git a/client/src/profile.ts b/client/src/profile.ts index 57bb8af..c4cda39 100644 --- a/client/src/profile.ts +++ b/client/src/profile.ts @@ -1,8 +1,6 @@ // External dependencies import * as nostrTools from 'nostr-tools'; import qrcode from 'qrcode-generator'; -import * as authManager from './auth-manager'; -import { SecureStorageService } from './services/SecureStorageService'; // Interface for profile data interface ProfileData { @@ -10,7 +8,8 @@ interface ProfileData { about?: string; picture?: string; nip05?: string; - [key: string]: any; + // Allow additional string properties for Nostr metadata + [key: string]: string | undefined; } // Element references @@ -37,7 +36,7 @@ let currentProfileData: ProfileData = { nip05: '' }; -// Connect to extension +// Connect with Nostr extension directly async function connectWithExtension(): Promise<string | null> { try { if (!window.nostr) { @@ -45,54 +44,36 @@ async function connectWithExtension(): Promise<string | null> { return null; } - // Use the AuthenticationService via auth-manager for a more secure login - const result = await authManager.authService.authenticate(); - - if (result.success && result.session) { - currentPubkey = result.session.pubkey; - - // Log the successful authentication - console.log(`Authentication successful using secure method for pubkey: ${currentPubkey.substring(0, 8)}...`); - - updateConnectionStatus(`Connected with extension using pubkey: ${currentPubkey.substring(0, 8)}...`, true); - showProfile(currentPubkey); - - // Return the pubkey so it can be used by the caller - return currentPubkey; - } else { - // Fall back to the old method if needed - console.log('Secure authentication failed, trying legacy method'); - + // Use the Nostr extension directly + try { const pubkey = await window.nostr.getPublicKey(); if (pubkey) { currentPubkey = pubkey; - // Set the authenticated state using auth-manager - authManager.setAuthenticated(true, pubkey); - console.log(`Authentication successful (legacy) for pubkey: ${pubkey.substring(0, 8)}...`); + // Log the successful authentication + console.log(`Authentication successful using Nostr extension for pubkey: ${currentPubkey.substring(0, 8)}...`); - updateConnectionStatus(`Connected with extension using pubkey: ${pubkey.substring(0, 8)}...`, true); - showProfile(pubkey); + updateConnectionStatus(`Connected with extension using pubkey: ${currentPubkey.substring(0, 8)}...`, true); + showProfile(currentPubkey); // Return the pubkey so it can be used by the caller - return pubkey; + return currentPubkey; } else { - authManager.setAuthenticated(false); - updateConnectionStatus('Failed to get public key from extension', false); + // Authentication failed + updateConnectionStatus(`Authentication failed: Could not get pubkey from extension`, false); return null; } + } catch (error) { + updateConnectionStatus(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`, false); + return null; } } catch (err) { console.error('Error connecting with extension:', err); - // Make sure we set authentication to false on error - authManager.setAuthenticated(false); updateConnectionStatus(`Error: ${err instanceof Error ? err.message : String(err)}`, false); return null; } } -// Manual pubkey functionality removed as users are automatically logged in - // Update connection status UI function updateConnectionStatus(message: string, isConnected: boolean): void { if (!connectionStatus) {return;} @@ -216,7 +197,7 @@ async function fetchProfileData(pubkey: string): Promise<void> { if (typeof msg.data !== 'string') {return;} try { - const data = JSON.parse(msg.data); + const data = JSON.parse(msg.data) as [string, string, Record<string, unknown>]; if (Array.isArray(data) && data[0] === 'EVENT' && data[1] === requestId) { events.push(data[2]); connected = true; @@ -324,7 +305,6 @@ function updateStats(): void { if (!statsRequestsSent || !statsResponsesReceived || !statsRelaysConnected) {return;} // For this demo, just set some placeholder values - // In a real app, you would track these stats in localStorage or a database statsRequestsSent.textContent = '0'; statsResponsesReceived.textContent = '0'; statsRelaysConnected.textContent = '0'; @@ -371,8 +351,6 @@ document.addEventListener('DOMContentLoaded', async () => { statsResponsesReceived = document.getElementById('responsesReceived'); statsRelaysConnected = document.getElementById('relaysConnected'); - // Auto-connect, no manual button needed - // Copy npub button const copyNpubBtn = document.getElementById('copyNpubBtn'); if (copyNpubBtn) { @@ -385,39 +363,19 @@ document.addEventListener('DOMContentLoaded', async () => { refreshProfileBtn.addEventListener('click', refreshProfile); } - // Try to connect automatically + // Try to connect automatically if nostr extension is available if (window.nostr) { - // If extension is available, try to connect with it try { const pubkey = await connectWithExtension(); if (pubkey) { - // We already set the auth state in connectWithExtension, but let's log success console.log(`Successfully authenticated with pubkey: ${pubkey.substring(0, 8)}...`); - - // Store the session expiry in localStorage for UI purposes - const secureStorage = new SecureStorageService(); - const session = authManager.authService.getCurrentSession(); - if (session) { - secureStorage.set('session_info', { - expiresAt: session.expiresAt, - createdAt: session.createdAt - }); - - console.log(`Session expires at ${new Date(session.expiresAt).toLocaleString()}`); - } - - // Make double sure the auth manager has the correct state - authManager.updateAuthStateFromStorage(); - console.log('Auth state after connection:', authManager.isAuthenticated() ? 'Authenticated' : 'Not authenticated'); } else { console.warn('Failed to get pubkey from extension'); - authManager.setAuthenticated(false); } } catch (error) { console.error('Error auto-connecting with extension:', error); - // Clear authentication state on error - authManager.setAuthenticated(false); + // Show profile container even if connection fails if (profileContainer) { profileContainer.classList.remove('hidden'); @@ -427,8 +385,6 @@ document.addEventListener('DOMContentLoaded', async () => { } else { // Even without an extension, show the profile container with default information console.log('No Nostr extension available, showing default profile'); - // Ensure authentication state is cleared - authManager.setAuthenticated(false); if (profileContainer) { profileContainer.classList.remove('hidden'); updateProfileWithDefaults(); diff --git a/client/src/receiver.ts b/client/src/receiver.ts index d52f10c..70a8422 100644 --- a/client/src/receiver.ts +++ b/client/src/receiver.ts @@ -7,10 +7,10 @@ import * as nostrTools from 'nostr-tools'; // Import service classes import { defaultServerConfig } from './config'; import { HttpService } from './services/HttpService'; -import { NostrService } from './services/NostrService'; import { ReceivedEvent } from './services/NostrEventService'; -import { decryptKeyWithNostrExtension, decryptKeyWithNostrTools, decryptWithWebCrypto } from './utils/crypto-utils'; +import { NostrService } from './services/NostrService'; import { UiService } from './services/UiService'; +import { decryptKeyWithNostrExtension, decryptKeyWithNostrTools, decryptWithWebCrypto } from './utils/crypto-utils'; // Module-level service instances let uiService: UiService; diff --git a/client/src/server-ui.ts b/client/src/server-ui.ts index 75f9b55..48071d5 100644 --- a/client/src/server-ui.ts +++ b/client/src/server-ui.ts @@ -2,10 +2,10 @@ * server-ui.ts * Entry point for the 1120 server UI */ -import './navbar-diagnostics'; // Import diagnostics first import './navbar'; // Import navbar component import './navbar-init'; // Import navbar initialization import * as nostrTools from 'nostr-tools'; + import { initServerUI } from './components/ServerUI'; import './debug-events'; // Import debug script @@ -191,7 +191,7 @@ function setupServerIdentityManager(): void { // Toggle format button toggleFormatBtn.addEventListener('click', () => { - if (!serverNsec || !serverPubkeyHex || !serverPubkeyNpub) return; + if (!serverNsec || !serverPubkeyHex || !serverPubkeyNpub) {return;} isShowingNpub = !isShowingNpub; @@ -208,7 +208,7 @@ function setupServerIdentityManager(): void { // Copy button copyServerNpubBtn.addEventListener('click', () => { - if (!serverNsec) return; + if (!serverNsec) {return;} navigator.clipboard.writeText(serverNpubInput.value) .then(() => { diff --git a/client/src/services/AuthenticationService.ts b/client/src/services/AuthenticationService.ts index 67f7772..2250311 100644 --- a/client/src/services/AuthenticationService.ts +++ b/client/src/services/AuthenticationService.ts @@ -1,30 +1,11 @@ /** * AuthenticationService.ts * - * Provides authentication services using Nostr protocol. - * Implements NIP-98 (HTTP Auth) for authenticated requests. + * Stateless authentication service that directly uses the Nostr extension. + * Removed all localStorage and session dependencies. */ -import * as nostrTools from 'nostr-tools'; -import { SecureStorageService } from './SecureStorageService'; - -/** - * Interface for auth session data - */ -export interface AuthSession { - /** User's public key in hex format */ - pubkey: string; - /** User's public key in bech32/npub format */ - npub: string; - /** Signature proving ownership */ - signature: string; - /** When the session was created (timestamp) */ - createdAt: number; - /** When the session expires (timestamp) */ - expiresAt: number; - /** Optional refresh token for extending the session */ - refreshToken?: string; -} +import type * as nostrTools from 'nostr-tools'; /** * Interface for authentication operation results @@ -32,14 +13,15 @@ export interface AuthSession { export interface AuthResult { /** Whether the operation was successful */ success: boolean; - /** The session if successful */ - session?: AuthSession; + /** User's public key if successful */ + pubkey?: string; /** Error message if unsuccessful */ error?: string; } /** - * Interface for NIP-98 HTTP Auth event + * Interface for NIP-98 HTTP Auth event (maintained for backwards compatibility) + * @deprecated This is maintained for backwards compatibility only */ export interface Nip98AuthEvent extends nostrTools.Event { /** Event tags including method, uri, etc. */ @@ -47,119 +29,78 @@ export interface Nip98AuthEvent extends nostrTools.Event { } /** - * HTTP method types for NIP-98 + * HTTP method types for NIP-98 (maintained for backwards compatibility) + * @deprecated This is maintained for backwards compatibility only */ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS'; /** - * Main authentication service class + * Event name for authentication state changes + */ +const AUTH_STATE_CHANGED_EVENT = 'auth-state-changed'; + +/** + * Main authentication service class - uses direct Nostr extension interaction */ export class AuthenticationService { - private secureStorage: SecureStorageService; - private currentSession: AuthSession | null = null; + // Cache for pubkey (in-memory only, not persisted) + private cachedPubkey: string | null = null; - // Storage keys - private readonly SESSION_KEY = 'authSession'; - private readonly REFRESH_TOKEN_KEY = 'refreshToken'; - - // Default duration (24 hours) in milliseconds - private readonly SESSION_DURATION = 24 * 60 * 60 * 1000; - - // Event name for authentication state changes - private readonly AUTH_STATE_CHANGED_EVENT = 'auth-state-changed'; + // Listeners for auth state changes + private listeners: ((authenticated: boolean, pubkey?: string) => void)[] = []; /** * Constructor */ constructor() { - this.secureStorage = new SecureStorageService(); - this.loadSessionFromStorage(); - this.setupEventHandlers(); + // Nothing to initialize - stateless design } - + /** - * Authenticate with Nostr extension and create a signed session + * Authenticate with Nostr extension + * This simply requests the pubkey from the extension * * @returns Promise resolving to authentication result */ public async authenticate(): Promise<AuthResult> { try { - // Check if Nostr extension exists + // Check if a Nostr extension is available if (!window.nostr) { return { success: false, - error: 'No Nostr extension detected. Please install a NIP-07 compatible extension.' + error: 'Nostr extension not found. Please install Alby or nos2x.' }; } - // Get public key from extension + // Directly request the public key from the Nostr extension const pubkey = await window.nostr.getPublicKey(); - if (!pubkey) { + + if (pubkey) { + // Cache the pubkey in memory + this.cachedPubkey = pubkey; + + // Notify listeners of authentication change + this.notifyAuthStateChanged(true, pubkey); + + // Set the global nostrPubkey for backwards compatibility with existing code + if (window) { + window.nostrPubkey = pubkey; + } + return { - success: false, - error: 'Failed to get public key from extension.' - }; - } - - // Generate a challenge the user must sign to prove key ownership - const challenge = `Authenticate for HTTP-over-Nostr at ${window.location.hostname} at ${Date.now()}`; - - // Ask user to sign the challenge - let signature: string; - try { - // Use signEvent since signSchnorr may not be available in all extensions - const challengeEvent = { - kind: 27235, // NIP-98 HTTP Auth kind - created_at: Math.floor(Date.now() / 1000), - tags: [['challenge', challenge]], - content: '', + success: true, pubkey }; - - const signedEvent = await window.nostr.signEvent(challengeEvent); - signature = signedEvent.sig || ''; - } catch (error) { - return { - success: false, - error: 'Failed to sign authentication challenge. User may have rejected the request.' - }; } - // Assume signature is valid if the extension signed it - // The extension handles the verification internally - const isValid = !!signature; - - if (!isValid) { - return { - success: false, - error: 'Signature verification failed.' - }; - } - - // Create session - const now = Date.now(); - const session: AuthSession = { - pubkey, - npub: nostrTools.nip19.npubEncode(pubkey), - signature, - createdAt: now, - expiresAt: now + this.SESSION_DURATION, - refreshToken: this.generateRefreshToken() - }; - - // Store session - this.currentSession = session; - this.persistSession(session); - - // Notify listeners - this.notifyAuthStateChanged(true, pubkey); - return { - success: true, - session + success: false, + error: 'Failed to get public key' }; } catch (error) { console.error('Authentication error:', error); + + // Return error result return { success: false, error: error instanceof Error ? error.message : 'Unknown authentication error' @@ -168,158 +109,103 @@ export class AuthenticationService { } /** - * Check if the user is currently authenticated with a valid session - * - * @returns True if authenticated with a valid session + * Check if the user is currently authenticated by attempting to fetch their pubkey + * @returns Promise resolving to boolean indicating authentication status */ - public isAuthenticated(): boolean { - if (!this.currentSession) { - return false; - } - - // Check if session is expired - if (this.currentSession.expiresAt < Date.now()) { - // Try to refresh the session if possible - if (this.currentSession.refreshToken) { - this.refreshSession().catch(error => { - console.error('Failed to refresh session:', error); - }); + public async checkAuthentication(): Promise<boolean> { + try { + // If we already have a cached pubkey, verify it's still valid + if (this.cachedPubkey) { + return true; } + + // No cached pubkey, try to get it from the extension + if (window.nostr) { + try { + const pubkey = await window.nostr.getPublicKey(); + if (pubkey) { + this.cachedPubkey = pubkey; + // Set the global nostrPubkey for backwards compatibility + if (window) { + window.nostrPubkey = pubkey; + } + return true; + } + } catch (e) { + // Error getting pubkey, user is not authenticated + console.log('Failed to get pubkey from extension:', e); + return false; + } + } + + return false; + } catch { return false; } - - return true; } /** - * Get the current session if authenticated + * Check if the user is currently authenticated (synchronous version) + * This uses the cached pubkey and doesn't make a new request * - * @returns The current session or null if not authenticated + * @returns True if we have a cached pubkey */ - public getCurrentSession(): AuthSession | null { - if (this.isAuthenticated()) { - return this.currentSession; - } - return null; + public isAuthenticated(): boolean { + return this.cachedPubkey !== null; } /** * Get the current user's public key + * If not cached, will try to fetch it from the extension * - * @returns The user's public key or null if not authenticated + * @returns Promise resolving to the user's public key or null + */ + public async getPubkey(): Promise<string | null> { + // If we have a cached pubkey, return it + if (this.cachedPubkey) { + return this.cachedPubkey; + } + + // Try to get pubkey from extension + try { + if (window.nostr) { + const pubkey = await window.nostr.getPublicKey(); + if (pubkey) { + this.cachedPubkey = pubkey; + return pubkey; + } + } + } catch (e) { + console.error('Error getting pubkey:', e); + } + + return null; + } + + /** + * Get the current user's public key (synchronous version) + * This only returns the cached pubkey and doesn't make a new request + * + * @returns The cached pubkey or null */ public getCurrentUserPubkey(): string | null { - return this.currentSession?.pubkey || null; + return this.cachedPubkey; } /** - * Logout the current user + * Clear the cached pubkey (logout) */ public logout(): void { - const wasPreviouslyAuthenticated = this.isAuthenticated(); - const previousPubkey = this.getCurrentUserPubkey(); + // Clear the cached pubkey + this.cachedPubkey = null; - this.currentSession = null; - this.secureStorage.remove(this.SESSION_KEY); - this.secureStorage.remove(this.REFRESH_TOKEN_KEY); - - // Only notify if there was an actual state change - if (wasPreviouslyAuthenticated && previousPubkey) { - this.notifyAuthStateChanged(false, previousPubkey); - } else if (wasPreviouslyAuthenticated) { - this.notifyAuthStateChanged(false); - } - } - - /** - * Create a signed NIP-98 HTTP Auth event - * - * @param method HTTP method - * @param url Request URL - * @param payload Optional request payload/body - * @returns Promise resolving to signed event or null if failed - */ - public async createAuthEvent( - method: HttpMethod, - url: string, - payload?: string - ): Promise<Nip98AuthEvent | null> { - try { - // Ensure user is authenticated - if (!this.isAuthenticated() || !window.nostr) { - return null; - } - - // Parse URL to get hostname and path - const parsedUrl = new URL(url); - - // Create tags according to NIP-98 - const tags: string[][] = [ - ['method', method], - ['uri', url], - ]; - - // Add content-type and digest for POST and PUT requests - if ((method === 'POST' || method === 'PUT') && payload) { - tags.push(['payload', payload]); - tags.push(['content-type', 'application/json']); - - // Add SHA-256 digest of the payload - const digest = await this.sha256Digest(payload); - tags.push(['digest', `SHA-256=${digest}`]); - } - - // Add created timestamp - const created = Math.floor(Date.now() / 1000); - tags.push(['created', created.toString()]); - - // Create the event - const event = { - kind: 27235, // NIP-98 HTTP Auth - created_at: created, - tags, - content: '', - pubkey: this.currentSession!.pubkey - }; - - // Sign the event with NIP-07 extension - const signedEvent = await window.nostr.signEvent(event); - - return signedEvent as Nip98AuthEvent; - } catch (error) { - console.error('Error creating NIP-98 auth event:', error); - return null; - } - } - - /** - * Create HTTP headers for NIP-98 authentication - * - * @param event Signed NIP-98 auth event - * @returns Headers object with Authorization header - */ - public createAuthHeaders(event: Nip98AuthEvent): Headers { - const headers = new Headers(); - const authValue = `Nostr ${JSON.stringify(event)}`; - headers.append('Authorization', authValue); - return headers; - } - - /** - * Extend the current session's expiration - * - * @param extendBy Milliseconds to extend the session by (defaults to SESSION_DURATION) - * @returns True if session was extended - */ - public extendSession(extendBy?: number): boolean { - if (!this.currentSession) { - return false; + // Clear global reference + if (window) { + window.nostrPubkey = undefined; } - const extension = extendBy || this.SESSION_DURATION; - this.currentSession.expiresAt = Date.now() + extension; - this.persistSession(this.currentSession); - return true; + // Notify listeners + this.notifyAuthStateChanged(false); } /** @@ -331,6 +217,10 @@ export class AuthenticationService { public onAuthStateChanged( callback: (authenticated: boolean, pubkey?: string) => void ): () => void { + // Add to internal listeners array + this.listeners.push(callback); + + // Also listen for CustomEvent for backwards compatibility const handler = (event: Event) => { const authEvent = event as CustomEvent; callback( @@ -339,94 +229,19 @@ export class AuthenticationService { ); }; - window.addEventListener(this.AUTH_STATE_CHANGED_EVENT, handler); + window.addEventListener(AUTH_STATE_CHANGED_EVENT, handler); - // Return function to remove the listener + // Return function to remove both listeners return () => { - window.removeEventListener(this.AUTH_STATE_CHANGED_EVENT, handler); - }; - } - - /** - * Attempt to refresh the current session - * - * @returns Promise resolving to true if session was refreshed - */ - private async refreshSession(): Promise<boolean> { - // This would typically extend the session without requiring re-authentication - // For now, we'll just extend the current session - if (!this.currentSession || !this.currentSession.refreshToken) { - return false; - } - - const storedRefreshToken = this.secureStorage.get<string>(this.REFRESH_TOKEN_KEY); - if (!storedRefreshToken || storedRefreshToken !== this.currentSession.refreshToken) { - return false; - } - - // Extend the session - return this.extendSession(); - } - - /** - * Generate a random refresh token - * - * @returns A random string to use as refresh token - */ - private generateRefreshToken(): string { - return Math.random().toString(36).substring(2, 15) + - Math.random().toString(36).substring(2, 15); - } - - /** - * Persist session to secure storage - * - * @param session The session to persist - */ - private persistSession(session: AuthSession): void { - this.secureStorage.set( - this.SESSION_KEY, - session, - { expiresIn: session.expiresAt - Date.now() } - ); - - if (session.refreshToken) { - this.secureStorage.set( - this.REFRESH_TOKEN_KEY, - session.refreshToken, - { expiresIn: (session.expiresAt - Date.now()) + (7 * 24 * 60 * 60 * 1000) } // Refresh token valid for 1 week longer - ); - } - } - - /** - * Load session from secure storage - */ - private loadSessionFromStorage(): void { - const session = this.secureStorage.get<AuthSession>(this.SESSION_KEY); - if (session) { - this.currentSession = session; + // Remove from internal array + const index = this.listeners.indexOf(callback); + if (index !== -1) { + this.listeners.splice(index, 1); + } - // Notify if a valid session is loaded - if (this.isAuthenticated()) { - this.notifyAuthStateChanged(true, session.pubkey); - } - } - } - - /** - * Set up event handlers - */ - private setupEventHandlers(): void { - // Clean up expired items on page load - this.secureStorage.cleanExpired(); - - // Setup beforeunload event to save session - window.addEventListener('beforeunload', () => { - if (this.currentSession) { - this.persistSession(this.currentSession); - } - }); + // Remove window event listener + window.removeEventListener(AUTH_STATE_CHANGED_EVENT, handler); + }; } /** @@ -436,26 +251,50 @@ export class AuthenticationService { * @param pubkey The user's pubkey (if authenticated) */ private notifyAuthStateChanged(authenticated: boolean, pubkey?: string): void { + // Notify internal listeners + for (const listener of this.listeners) { + try { + listener(authenticated, pubkey); + } catch (e) { + console.error('Error in auth state change listener:', e); + } + } + + // Dispatch CustomEvent for backwards compatibility window.dispatchEvent( - new CustomEvent(this.AUTH_STATE_CHANGED_EVENT, { - detail: { authenticated, pubkey } + new CustomEvent(AUTH_STATE_CHANGED_EVENT, { + detail: { authenticated, pubkey } }) ); } /** - * Calculate SHA-256 + * Create a signed NIP-98 HTTP Auth event (stub) * - * @param message Message to hash - * @returns Base64-encoded hash + * @deprecated This method is deprecated and maintained only for backwards compatibility + * @param method HTTP method + * @param url Request URL + * @param payload Optional request payload/body + * @returns Promise resolving to null since this functionality is deprecated */ - private async sha256Digest(message: string): Promise<string> { - const encoder = new TextEncoder(); - const data = encoder.encode(message); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const base64Hash = btoa(String.fromCharCode(...hashArray)); - - return base64Hash; + public async createAuthEvent( + method: HttpMethod, + url: string, + payload?: string + ): Promise<Nip98AuthEvent | null> { + console.warn('AuthenticationService.createAuthEvent is deprecated and returns null'); + return null; + } + + /** + * Create HTTP headers for NIP-98 authentication (stub) + * + * @deprecated This method is deprecated and maintained only for backwards compatibility + * @param event NIP-98 auth event + * @returns Headers object with Authorization header + */ + public createAuthHeaders(event: Nip98AuthEvent): Headers { + console.warn('AuthenticationService.createAuthHeaders is deprecated'); + return new Headers(); } } \ No newline at end of file diff --git a/client/src/services/ClientEventStore.ts b/client/src/services/ClientEventStore.ts index f9bd486..ba1aaf0 100644 --- a/client/src/services/ClientEventStore.ts +++ b/client/src/services/ClientEventStore.ts @@ -7,6 +7,7 @@ */ import type { NostrEvent } from '../relay'; + import { EventChangeType } from './EventManager'; /** diff --git a/client/src/services/EventDetailsRenderer.ts b/client/src/services/EventDetailsRenderer.ts index d97e2c3..471a4e9 100644 --- a/client/src/services/EventDetailsRenderer.ts +++ b/client/src/services/EventDetailsRenderer.ts @@ -3,9 +3,10 @@ * Component for rendering detailed event information */ -import { NostrEvent } from '../relay'; -import { ReceivedEvent } from './NostrEventService'; +import type { NostrEvent } from '../relay'; + import { HttpFormatter } from './HttpFormatter'; +import type { ReceivedEvent } from './NostrEventService'; /** * Class for rendering event details in the UI @@ -84,7 +85,7 @@ export class EventDetailsRenderer { isResponse: boolean, eventTime: string ): void { - if (!this.eventDetails) return; + if (!this.eventDetails) {return;} this.eventDetails.innerHTML = ` <div class="event-details-header"> @@ -209,10 +210,10 @@ ${JSON.stringify(event, null, 2)} * Set up tab button click handlers */ private setupTabButtons(): void { - if (!this.eventDetails) return; + if (!this.eventDetails) {return;} const tabButtons = this.eventDetails.querySelectorAll('.tab-btn'); - if (tabButtons.length === 0) return; + if (tabButtons.length === 0) {return;} tabButtons.forEach(button => { button.addEventListener('click', (e) => { @@ -284,19 +285,19 @@ ${JSON.stringify(event, null, 2)} ): Promise<void> { try { // Skip if the user has navigated to another event - if (this.currentEventId !== eventId) return; + if (this.currentEventId !== eventId) {return;} // 1. Load related events first (fast operation) await this.loadRelatedEvents(eventId, event, isRequest); // Skip if the user has navigated to another event - if (this.currentEventId !== eventId) return; + if (this.currentEventId !== eventId) {return;} // 2. Set up action buttons this.setupActionButtons(eventId, event, isRequest); // Skip if the user has navigated to another event - if (this.currentEventId !== eventId) return; + if (this.currentEventId !== eventId) {return;} // 3. Get the HTTP content const httpContent = receivedEvent.decrypted ? @@ -307,7 +308,7 @@ ${JSON.stringify(event, null, 2)} this.updateRawContent(eventId, httpContent, receivedEvent.decrypted); // Skip if the user has navigated to another event - if (this.currentEventId !== eventId) return; + if (this.currentEventId !== eventId) {return;} // 5. Update formatted content (most expensive operation) this.updateFormattedContent(eventId, httpContent, isRequest, isResponse || is21121Event, receivedEvent.decrypted); @@ -342,7 +343,7 @@ ${JSON.stringify(event, null, 2)} isRequest: boolean ): Promise<void> { const relatedEventsContainer = document.getElementById('related-events-container'); - if (!relatedEventsContainer) return; + if (!relatedEventsContainer) {return;} // Get related events const relatedIds = event.id ? (this.relatedEvents.get(event.id) || []) : []; @@ -395,7 +396,7 @@ ${JSON.stringify(event, null, 2)} isRequest: boolean ): void { const actionsContainer = document.getElementById(`http-actions-${eventId}`); - if (!actionsContainer) return; + if (!actionsContainer) {return;} // Get related events count const relatedIds = event.id ? (this.relatedEvents.get(event.id) || []) : []; @@ -541,11 +542,11 @@ ${JSON.stringify(event, null, 2)} private displayResponse21121Json(event: NostrEvent): void { // Get the JSON container const jsonContainer = document.getElementById('response21121Json'); - if (!jsonContainer) return; + if (!jsonContainer) {return;} // Get the pre element within the container const preElement = jsonContainer.querySelector('pre.json-content'); - if (!preElement) return; + if (!preElement) {return;} // Format the JSON prettily const formattedJson = JSON.stringify(event, null, 2); diff --git a/client/src/services/EventDetailsRenderer.updated.ts b/client/src/services/EventDetailsRenderer.updated.ts index 181270a..384cec1 100644 --- a/client/src/services/EventDetailsRenderer.updated.ts +++ b/client/src/services/EventDetailsRenderer.updated.ts @@ -5,7 +5,9 @@ */ import { NostrEvent } from '../relay'; -import { EventManager, EventChangeType, ManagedEvent } from './EventManager'; + +import type { EventManager} from './EventManager'; +import { EventChangeType, ManagedEvent } from './EventManager'; import { HttpFormatter } from './HttpFormatter'; /** @@ -69,7 +71,7 @@ export class EventDetailsRenderer { * Show empty state when no event is selected */ private showEmptyState(): void { - if (!this.eventDetails) return; + if (!this.eventDetails) {return;} this.eventDetails.innerHTML = ` <div class="empty-state"> @@ -82,7 +84,7 @@ export class EventDetailsRenderer { * Render the details of the currently selected event */ private renderEventDetails(): void { - if (!this.eventDetails) return; + if (!this.eventDetails) {return;} // Get the selected event from the EventManager const managedEvent = this.eventManager.getSelectedEvent(); @@ -99,7 +101,7 @@ export class EventDetailsRenderer { const is21121Event = event.kind === 21121; // Determine the content to display - let httpContent = managedEvent.decrypted ? + const httpContent = managedEvent.decrypted ? managedEvent.decryptedContent || event.content : event.content; @@ -212,7 +214,7 @@ export class EventDetailsRenderer { * Set up tab buttons for switching between raw and formatted views */ private setupTabButtons(): void { - if (!this.eventDetails) return; + if (!this.eventDetails) {return;} const tabButtons = this.eventDetails.querySelectorAll('.tab-btn'); tabButtons.forEach(button => { @@ -240,7 +242,7 @@ export class EventDetailsRenderer { * Set up links to related events */ private setupRelatedEventLinks(): void { - if (!this.eventDetails) return; + if (!this.eventDetails) {return;} const relatedLinks = this.eventDetails.querySelectorAll('.related-event-link'); relatedLinks.forEach(link => { diff --git a/client/src/services/EventListRenderer.ts b/client/src/services/EventListRenderer.ts index a186db2..fe63f7d 100644 --- a/client/src/services/EventListRenderer.ts +++ b/client/src/services/EventListRenderer.ts @@ -4,7 +4,8 @@ */ import * as nostrTools from 'nostr-tools'; -import { NostrEvent } from '../relay'; + +import type { NostrEvent } from '../relay'; /** * Class for rendering events in the UI list diff --git a/client/src/services/EventListRenderer.updated.ts b/client/src/services/EventListRenderer.updated.ts index e5afb29..e40a6b3 100644 --- a/client/src/services/EventListRenderer.updated.ts +++ b/client/src/services/EventListRenderer.updated.ts @@ -5,8 +5,11 @@ */ import * as nostrTools from 'nostr-tools'; -import { NostrEvent } from '../relay'; -import { EventManager, EventChangeType, ManagedEvent } from './EventManager'; + +import type { NostrEvent } from '../relay'; + +import type { EventManager} from './EventManager'; +import { EventChangeType, ManagedEvent } from './EventManager'; /** * Class for rendering events in the UI list @@ -61,7 +64,7 @@ export class EventListRenderer { * Render existing events from the EventManager */ private renderExistingEvents(): void { - if (!this.eventsList) return; + if (!this.eventsList) {return;} // Clear any existing content this.eventsList.innerHTML = ''; @@ -170,7 +173,7 @@ export class EventListRenderer { * @param eventId The ID of the event to update */ private updateEventItem(eventId: string): void { - if (!this.eventsList) return; + if (!this.eventsList) {return;} // Find the existing event item const existingItem = this.eventsList.querySelector(`.event-item[data-id="${eventId}"]`); @@ -188,7 +191,7 @@ export class EventListRenderer { * @param eventId The ID of the event to remove */ private removeEventItem(eventId: string): void { - if (!this.eventsList) return; + if (!this.eventsList) {return;} const eventItem = this.eventsList.querySelector(`.event-item[data-id="${eventId}"]`); if (eventItem) { @@ -210,7 +213,7 @@ export class EventListRenderer { * @param eventId The ID of the event to highlight */ private highlightSelectedEvent(eventId: string): void { - if (!this.eventsList) return; + if (!this.eventsList) {return;} // Remove selected class from all items const allItems = this.eventsList.querySelectorAll('.event-item'); @@ -234,11 +237,11 @@ export class EventListRenderer { private checkIfToServer(event: NostrEvent): boolean { // Get server pubkey from EventManager const serverPubkey = this.eventManager.getServerPubkey(); - if (!serverPubkey) return false; + if (!serverPubkey) {return false;} // Check for p tag to identify recipient const pTag = event.tags.find(tag => tag[0] === 'p'); - if (!pTag || pTag.length <= 1) return false; + if (!pTag || pTag.length <= 1) {return false;} // Check if the p tag matches our server pubkey return (pTag[1] === serverPubkey); @@ -252,7 +255,7 @@ export class EventListRenderer { private getRecipientDisplay(event: NostrEvent): string { // Find recipient if any const pTag = event.tags.find(tag => tag[0] === 'p'); - if (!pTag || pTag.length <= 1) return ''; + if (!pTag || pTag.length <= 1) {return '';} return `<div class="recipient">To: ${pTag[1].substring(0, 8)}...</div>`; } diff --git a/client/src/services/EventManager.initialization.ts b/client/src/services/EventManager.initialization.ts index e3c479a..0e4445e 100644 --- a/client/src/services/EventManager.initialization.ts +++ b/client/src/services/EventManager.initialization.ts @@ -4,12 +4,12 @@ * with UI components and event services. */ -import { EventManager } from './EventManager'; -import { EventListRenderer } from './EventListRenderer.updated'; import { EventDetailsRenderer } from './EventDetailsRenderer.updated'; -import { NostrRelayService } from './NostrRelayService'; +import { EventListRenderer } from './EventListRenderer.updated'; +import { EventManager } from './EventManager'; import { NostrCacheService } from './NostrCacheService'; import { NostrEventService } from './NostrEventService.updated'; +import { NostrRelayService } from './NostrRelayService'; import { RelayStatusManager } from './RelayStatusManager'; /** diff --git a/client/src/services/EventManager.test.ts b/client/src/services/EventManager.test.ts index 5e0e3fc..556484a 100644 --- a/client/src/services/EventManager.test.ts +++ b/client/src/services/EventManager.test.ts @@ -3,8 +3,9 @@ * Test cases and examples for the EventManager service */ +import type { NostrEvent } from '../relay'; + import { EventManager, EventKind, EventChangeType } from './EventManager'; -import { NostrEvent } from '../relay'; /** * Create a sample NostrEvent for testing diff --git a/client/src/services/EventManager.ts b/client/src/services/EventManager.ts index 8791ccb..891214f 100644 --- a/client/src/services/EventManager.ts +++ b/client/src/services/EventManager.ts @@ -3,7 +3,7 @@ * Centralizes event data management for 21120 and 21121 events */ -import { NostrEvent } from '../relay'; +import type { NostrEvent } from '../relay'; // Event types we're managing export enum EventKind { diff --git a/client/src/services/HttpClient.ts b/client/src/services/HttpClient.ts index 46ebdbe..080db07 100644 --- a/client/src/services/HttpClient.ts +++ b/client/src/services/HttpClient.ts @@ -3,7 +3,7 @@ * Service for making HTTP requests */ -import { HttpService } from './HttpService'; +import type { HttpService } from './HttpService'; import { ToastNotifier } from './ToastNotifier'; /** diff --git a/client/src/services/HttpService.ts b/client/src/services/HttpService.ts index f9ade04..bd1dc9d 100644 --- a/client/src/services/HttpService.ts +++ b/client/src/services/HttpService.ts @@ -7,6 +7,7 @@ // Import crypto utilities import { encryptWithWebCrypto, decryptWithWebCrypto } from '../utils/crypto-utils'; + import { ToastNotifier } from './ToastNotifier'; // Interface definitions diff --git a/client/src/services/Nostr21121EventHandler.ts b/client/src/services/Nostr21121EventHandler.ts index 966dc66..dd87af0 100644 --- a/client/src/services/Nostr21121EventHandler.ts +++ b/client/src/services/Nostr21121EventHandler.ts @@ -4,12 +4,14 @@ * Automatically processes incoming 21120 requests to generate 21121 responses */ -import { NostrEvent } from '../relay'; -import { NostrService } from './NostrService'; -import { EventManager, EventChangeType, EventKind } from './EventManager'; -import { HttpClient } from './HttpClient'; -import { ToastNotifier } from './ToastNotifier'; +import type { NostrEvent } from '../relay'; + +import type { EventManager} from './EventManager'; +import { EventChangeType, EventKind } from './EventManager'; +import type { HttpClient } from './HttpClient'; import { Nostr21121Service } from './Nostr21121Service'; +import type { NostrService } from './NostrService'; +import { ToastNotifier } from './ToastNotifier'; /** * Class for handling NIP-21121 HTTP response events @@ -69,7 +71,7 @@ export class Nostr21121EventHandler { */ private async handleNewEvent(eventId: string): Promise<void> { const managedEvent = this.eventManager.getEvent(eventId); - if (!managedEvent) return; + if (!managedEvent) {return;} const event = managedEvent.event; diff --git a/client/src/services/Nostr21121IntegrationHelper.ts b/client/src/services/Nostr21121IntegrationHelper.ts index 12c0244..8362d90 100644 --- a/client/src/services/Nostr21121IntegrationHelper.ts +++ b/client/src/services/Nostr21121IntegrationHelper.ts @@ -3,11 +3,12 @@ * Handles integration between HTTP content and NIP-21121 events */ -import { NostrEvent } from '../../src/relay'; -import { Nostr21121Service } from './Nostr21121Service'; -import { NostrEventService } from './NostrEventService'; -import { ToastNotifier } from './ToastNotifier'; +import type { NostrEvent } from '../../src/relay'; + import { HttpFormatter } from './HttpFormatter'; +import type { Nostr21121Service } from './Nostr21121Service'; +import type { NostrEventService } from './NostrEventService'; +import { ToastNotifier } from './ToastNotifier'; /** * Helper class for NIP-21121 integration with HTTP content @@ -66,7 +67,7 @@ export class Nostr21121IntegrationHelper { try { // Extract the event ID from the header const eventIdMatch = eventDetails.querySelector('h3')?.textContent?.match(/ID: (\w+)\.\.\./); - if (!eventIdMatch || !eventIdMatch[1]) return; + if (!eventIdMatch || !eventIdMatch[1]) {return;} const eventId = eventIdMatch[1]; @@ -81,7 +82,7 @@ export class Nostr21121IntegrationHelper { // Get the HTTP content const httpContent = eventDetails.querySelector('.http-content')?.textContent; - if (!httpContent) return; + if (!httpContent) {return;} // Execute the HTTP request try { @@ -126,7 +127,7 @@ export class Nostr21121IntegrationHelper { */ private displayHttpResponse(responseContent: string): void { // Create or get the modal - let modal = document.getElementById('httpResponseModal'); + const modal = document.getElementById('httpResponseModal'); if (!modal) { console.error('HTTP response modal not found in DOM'); return; @@ -239,7 +240,7 @@ async function executeHttpRequestWithFetch(requestContent: string): Promise<stri // Extract the host let host = ''; - let headers: Record<string, string> = {}; + const headers: Record<string, string> = {}; let body = ''; let inHeaders = true; diff --git a/client/src/services/Nostr21121ResponseHandler.ts b/client/src/services/Nostr21121ResponseHandler.ts index 15d5977..2c82d7b 100644 --- a/client/src/services/Nostr21121ResponseHandler.ts +++ b/client/src/services/Nostr21121ResponseHandler.ts @@ -3,8 +3,9 @@ * Handler for creating and managing NIP-21121 HTTP response events */ -import { NostrEvent } from '../relay'; -import { NostrService } from './NostrService'; +import type { NostrEvent } from '../relay'; + +import type { NostrService } from './NostrService'; import { ToastNotifier } from './ToastNotifier'; /** diff --git a/client/src/services/Nostr21121Service.ts b/client/src/services/Nostr21121Service.ts index b2c774a..d6ed409 100644 --- a/client/src/services/Nostr21121Service.ts +++ b/client/src/services/Nostr21121Service.ts @@ -2,8 +2,9 @@ * NIP-21121 Service for HTTP response events */ -import { NostrEvent } from '../relay'; import * as nostrTools from 'nostr-tools'; + +import type { NostrEvent } from '../relay'; import { encryptWithNostrTools, encryptWithWebCrypto } from '../utils/crypto-utils'; /** @@ -91,7 +92,7 @@ export class Nostr21121Service { console.log(`Using pubkey: ${pubKey.substring(0, 8)}...`); // Initialize tags array - let tags: string[][] = []; + const tags: string[][] = []; // Always add reference to the request event if (requestEvent.id) { diff --git a/client/src/services/Nostr31120Service.ts b/client/src/services/Nostr31120Service.ts index ec4cf30..6627b6b 100644 --- a/client/src/services/Nostr31120Service.ts +++ b/client/src/services/Nostr31120Service.ts @@ -7,9 +7,10 @@ import * as nostrTools from 'nostr-tools'; import type { NostrEvent } from '../relay'; + +import type { NostrCacheService } from './NostrCacheService'; import type { NostrFilter } from './NostrEventService'; import type { NostrRelayService } from './NostrRelayService'; -import type { NostrCacheService } from './NostrCacheService'; /** * Service for working with kind 31120 events (HTTP-over-Nostr server registrations) @@ -153,7 +154,7 @@ export class Nostr31120Service { * @returns Server pubkey or null if not found */ public getServerPubkeyFromEvent(event: NostrEvent): string | null { - if (event.kind !== 31120) return null; + if (event.kind !== 31120) {return null;} // Find the d tag which contains the server pubkey const dTag = event.tags.find(tag => tag[0] === 'd'); diff --git a/client/src/services/NostrEventService.updated.ts b/client/src/services/NostrEventService.updated.ts index 5315cab..037c9da 100644 --- a/client/src/services/NostrEventService.updated.ts +++ b/client/src/services/NostrEventService.updated.ts @@ -9,9 +9,11 @@ import * as nostrTools from 'nostr-tools'; // Project imports import type { NostrEvent } from '../relay'; + +import type { EventManager} from './EventManager'; +import { EventKind, EventChangeType } from './EventManager'; import type { NostrCacheService, ProfileData } from './NostrCacheService'; import type { NostrRelayService } from './NostrRelayService'; -import { EventManager, EventKind, EventChangeType } from './EventManager'; // Interface for a Nostr subscription export interface NostrSubscription { @@ -272,7 +274,7 @@ export class NostrEventService { const events = this.cacheService.getCachedEvents(relayUrl); if (events) { const event = events.find(e => e.id === eventId); - if (event) return event; + if (event) {return event;} } } @@ -280,7 +282,7 @@ export class NostrEventService { const events = this.cacheService.getCachedEvents('memory'); if (events) { const event = events.find(e => e.id === eventId); - if (event) return event; + if (event) {return event;} } return null; diff --git a/client/src/services/NostrService.ts b/client/src/services/NostrService.ts index 456f695..da6e612 100644 --- a/client/src/services/NostrService.ts +++ b/client/src/services/NostrService.ts @@ -3,20 +3,19 @@ * Main service that coordinates Nostr protocol functionality by integrating specialized services */ // Project imports +import * as authManager from '../auth-manager'; import type { NostrEvent } from '../relay'; // Import auth manager to gate network requests -import * as authManager from '../auth-manager'; // Import NIP-98 HTTP Auth types import type { Nip98AuthEvent } from './AuthenticationService'; - +import { Nostr21121Service } from './Nostr21121Service'; +import { Nostr31120Service } from './Nostr31120Service'; import type { ProfileData } from './NostrCacheService'; import { NostrCacheService } from './NostrCacheService'; import type { NostrFilter, NostrSubscription } from './NostrEventService'; import { NostrEventService } from './NostrEventService'; import { NostrRelayService } from './NostrRelayService'; -import { Nostr31120Service } from './Nostr31120Service'; -import { Nostr21121Service } from './Nostr21121Service'; /** * Class for managing Nostr functionality diff --git a/client/src/services/SecureStorageService.ts b/client/src/services/SecureStorageService.ts index 628bf4e..3eccc20 100644 --- a/client/src/services/SecureStorageService.ts +++ b/client/src/services/SecureStorageService.ts @@ -149,7 +149,7 @@ export class SecureStorageService { for (const fullKey of ourKeys) { const storedValue = localStorage.getItem(fullKey); - if (!storedValue) continue; + if (!storedValue) {continue;} try { const storedItem = JSON.parse(storedValue); diff --git a/client/src/services/ToastNotifier.ts b/client/src/services/ToastNotifier.ts index 9240f99..0950ed1 100644 --- a/client/src/services/ToastNotifier.ts +++ b/client/src/services/ToastNotifier.ts @@ -19,7 +19,7 @@ export class ToastNotifier { * Initialize the toast container */ private static initialize(): void { - if (this.initialized) return; + if (this.initialized) {return;} this.toastContainer = document.getElementById('toast-container'); @@ -109,7 +109,7 @@ export class ToastNotifier { public static show(message: string, type: NotificationType = 'info', duration = 5000): void { this.initialize(); - if (!this.toastContainer) return; + if (!this.toastContainer) {return;} // Create toast element const toast = document.createElement('div'); diff --git a/client/src/services/UiService.ts b/client/src/services/UiService.ts index 0bde571..3d6375d 100644 --- a/client/src/services/UiService.ts +++ b/client/src/services/UiService.ts @@ -3,17 +3,19 @@ * Handles UI-related operations and DOM manipulation */ import * as nostrTools from 'nostr-tools'; -import { NostrEvent } from '../relay'; -import { HttpService } from './HttpService'; -import { HttpClient } from './HttpClient'; -import { NostrService } from './NostrService'; -import { ReceivedEvent } from './NostrEventService'; -import { RelayStatusManager } from './RelayStatusManager'; -import { EventListRenderer } from './EventListRenderer'; + +import type { NostrEvent } from '../relay'; + import { EventDetailsRenderer } from './EventDetailsRenderer'; -import { Nostr21121ResponseHandler } from './Nostr21121ResponseHandler'; -import { ToastNotifier } from './ToastNotifier'; +import { EventListRenderer } from './EventListRenderer'; +import { HttpClient } from './HttpClient'; import { HttpFormatter } from './HttpFormatter'; +import type { HttpService } from './HttpService'; +import { Nostr21121ResponseHandler } from './Nostr21121ResponseHandler'; +import type { ReceivedEvent } from './NostrEventService'; +import type { NostrService } from './NostrService'; +import { RelayStatusManager } from './RelayStatusManager'; +import { ToastNotifier } from './ToastNotifier'; /** * Class for managing UI operations @@ -109,7 +111,7 @@ export class UiService { */ public addReceivedEvent(receivedEvent: ReceivedEvent): void { const event = receivedEvent.event; - if (!event || !event.id) return; + if (!event || !event.id) {return;} // Store the event this.receivedEvents.set(event.id, receivedEvent); @@ -131,7 +133,7 @@ export class UiService { * @param event The event to check relations for */ private checkForRelatedEvents(event: NostrEvent): void { - if (!event.id) return; + if (!event.id) {return;} // Check if this is a response with an e tag referencing a request if (event.kind === 21121) { @@ -181,7 +183,7 @@ export class UiService { // Set up event handlers for interactive elements const eventDetailsElement = this.eventDetailsRenderer.getEventDetailsElement(); - if (!eventDetailsElement) return; + if (!eventDetailsElement) {return;} // Handle "Execute HTTP Request" button const executeButtons = eventDetailsElement.querySelectorAll('.execute-http-request-btn'); @@ -295,7 +297,7 @@ export class UiService { const tabId = (button as HTMLElement).dataset.tab; if (tabId) { const tab = dialog.querySelector(`#${tabId}`); - if (tab) tab.classList.add('active'); + if (tab) {tab.classList.add('active');} } }); }); diff --git a/client/src/services/WebSocketManager.ts b/client/src/services/WebSocketManager.ts index b6f0d68..b66b00c 100644 --- a/client/src/services/WebSocketManager.ts +++ b/client/src/services/WebSocketManager.ts @@ -5,8 +5,11 @@ export interface WebSocketOptions { timeout?: number; + // eslint-disable-next-line no-unused-vars onOpen?: (ws: WebSocket) => void; + // eslint-disable-next-line no-unused-vars onMessage?: (parsedData: unknown) => void; + // eslint-disable-next-line no-unused-vars onError?: (evt: Event) => void; onClose?: () => void; } @@ -47,7 +50,9 @@ export class WebSocketManager { // Set up event handlers if (this.ws) { this.ws.onopen = () => { - if (timeoutId) clearTimeout(timeoutId); + if (timeoutId) { + clearTimeout(timeoutId); + } this.connected = true; console.log(`WebSocketManager: Connection to ${url} established successfully`); if (options.onOpen && this.ws) { @@ -84,7 +89,9 @@ export class WebSocketManager { this.ws.onerror = (errorEvt) => { console.error(`WebSocketManager: Error on connection to ${url}:`, errorEvt); - if (timeoutId) clearTimeout(timeoutId); + if (timeoutId) { + clearTimeout(timeoutId); + } if (options.onError) { options.onError(errorEvt); } @@ -95,7 +102,9 @@ export class WebSocketManager { this.ws.onclose = (evt) => { console.log(`WebSocketManager: Connection to ${url} closed. Code: ${evt.code}, Reason: ${evt.reason || 'No reason provided'}, Clean: ${evt.wasClean}`); - if (timeoutId) clearTimeout(timeoutId); + if (timeoutId) { + clearTimeout(timeoutId); + } this.connected = false; // Log the closing information more thoroughly @@ -115,7 +124,9 @@ export class WebSocketManager { } } catch (error) { console.error('Error creating WebSocket connection:', error); - if (timeoutId) clearTimeout(timeoutId); + if (timeoutId) { + clearTimeout(timeoutId); + } this.close(); reject(error); } @@ -127,30 +138,30 @@ export class WebSocketManager { */ public async testConnection(url: string, timeout = 5000): Promise<boolean> { try { - const ws = new WebSocket(url); + const testWs = new WebSocket(url); let connected = false; await new Promise<void>((resolve, reject) => { const timeoutId = setTimeout(() => { if (!connected) { - ws.close(); + testWs.close(); reject(new Error('Connection timeout')); } }, timeout); - ws.onopen = () => { + testWs.onopen = () => { clearTimeout(timeoutId); connected = true; resolve(); }; - ws.onerror = (err) => { + testWs.onerror = (err) => { clearTimeout(timeoutId); reject(new Error(`WebSocket error: ${err.toString()}`)); }; }); - ws.close(); + testWs.close(); return true; } catch { return false; diff --git a/client/src/types/window.d.ts b/client/src/types/window.d.ts index c758e34..9085174 100644 --- a/client/src/types/window.d.ts +++ b/client/src/types/window.d.ts @@ -43,6 +43,16 @@ declare global { // Add global authentication state tracking authInProgress?: boolean; currentSignedEvent?: NostrEvent; + // Add timestamp for last authentication request (for debouncing) + lastAuthRequestTime?: number; + // Add declaration for NostrLogin global object + nostrLogin?: any; + // Add declaration for nostrPubkey global property + nostrPubkey?: string; + // Add login status elements + loginStatus?: HTMLElement; + loginButton?: HTMLElement; + logoutButton?: HTMLElement; } } diff --git a/client/styles.css b/client/styles.css index b509cf0..c8981b7 100644 --- a/client/styles.css +++ b/client/styles.css @@ -1098,12 +1098,39 @@ footer { /* Login container */ .login-container { - margin-bottom: 20px; + display: flex; + align-items: center; + gap: 10px; + margin-right: 20px; } -.login-status { - margin-top: 5px; - font-size: 14px; +#login-status { + font-size: 0.9rem; + padding: 4px 8px; + border-radius: 4px; + background-color: var(--background-secondary); +} + +.auth-button { + padding: 4px 10px; + border-radius: 4px; + border: 1px solid var(--border-color); + background-color: var(--button-primary); + color: white; + cursor: pointer; + font-size: 0.9rem; +} + +.auth-button:hover { + background-color: var(--button-primary-hover); +} + +#logout-button { + background-color: var(--button-secondary); +} + +#logout-button:hover { + background-color: var(--button-secondary-hover); } /* Hidden elements */ @@ -2967,4 +2994,34 @@ footer { .auth-required-message h3 { color: var(--accent-color); margin-top: 0; +} + +/* Authentication container and button styles */ +.auth-container { + display: flex; + align-items: center; + margin-left: auto; + margin-right: 10px; +} + +.auth-btn { + font-size: 0.8em; + padding: 3px 8px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; +} + +.sign-in-btn { + background-color: var(--button-primary, #4285f4); + color: white; + border: none; + font-weight: 500; + padding: 5px 10px; +} + +.sign-in-btn:hover { + background-color: var(--button-hover, #3367d6); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } \ No newline at end of file