parent
6da0b87fba
commit
79e65e3860
client
billboard.html
src
auth-manager.tsbillboard.tsclient-event-handler.tsclient.ts
styles.csscomponents
ClientEventsTable.tsEventDetail.tsEventList.tsHttpMessagesTable.tsHttpRequestExecutor.tsNostr21121Creator.tsResponseViewer.tsServerUI.ts
converter.tshttp-response-viewer.tshttp-response-viewer.updated.tsnavbar-diagnostics.tsprofile.tsreceiver.tsserver-ui.tsservices
AuthenticationService.tsClientEventStore.tsEventDetailsRenderer.tsEventDetailsRenderer.updated.tsEventListRenderer.tsEventListRenderer.updated.tsEventManager.initialization.tsEventManager.test.tsEventManager.tsHttpClient.tsHttpService.tsNostr21121EventHandler.tsNostr21121IntegrationHelper.tsNostr21121ResponseHandler.tsNostr21121Service.tsNostr31120Service.tsNostrEventService.updated.tsNostrService.tsSecureStorageService.tsToastNotifier.tsUiService.tsWebSocketManager.ts
types
@ -38,6 +38,11 @@
|
|||||||
<div id="billboardRelayStatus" class="relay-status">Not connected</div>
|
<div id="billboardRelayStatus" class="relay-status">Not connected</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="billboard-actions">
|
<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>
|
<button id="createBillboardBtn" class="primary-button">+ Add New Billboard</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* auth-manager.ts
|
* auth-manager.ts
|
||||||
* Centralized authentication manager for the entire application.
|
* Centralized authentication manager for the entire application.
|
||||||
* Prevents network requests until the user is properly authenticated.
|
* Refactored to remove localStorage and session dependencies.
|
||||||
*
|
* Uses direct Nostr extension checks to determine auth state.
|
||||||
* Updated to use the new AuthenticationService for more robust authentication.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AuthenticationService } from './services/AuthenticationService';
|
import { AuthenticationService } from './services/AuthenticationService';
|
||||||
@ -11,12 +10,6 @@ import { AuthenticationService } from './services/AuthenticationService';
|
|||||||
// Create singleton instance of AuthenticationService
|
// Create singleton instance of AuthenticationService
|
||||||
const authService = new 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
|
// Direct listeners registry for auth state changes
|
||||||
type AuthStateListener = (authenticated: boolean, pubkey?: string) => void;
|
type AuthStateListener = (authenticated: boolean, pubkey?: string) => void;
|
||||||
const authStateListeners: AuthStateListener[] = [];
|
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
|
// Queue for operations that should run after authentication
|
||||||
const postAuthQueue: Array<() => void> = [];
|
const postAuthQueue: Array<() => void> = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the user is currently authenticated
|
* 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 {
|
export async function checkAuthentication(): Promise<boolean> {
|
||||||
// Use the AuthenticationService
|
return authService.checkAuthentication();
|
||||||
const result = authService.isAuthenticated();
|
|
||||||
|
|
||||||
// Update the backward-compatible flag
|
|
||||||
userAuthenticated = result;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
export function isAuthenticated(): boolean {
|
||||||
const previousState = userAuthenticated;
|
return authService.isAuthenticated();
|
||||||
userAuthenticated = authenticated;
|
}
|
||||||
|
|
||||||
if (authenticated && pubkey) {
|
/**
|
||||||
// For now, just store the pubkey in the AuthenticationService without full authentication
|
* Login with NostrLogin
|
||||||
// This maintains backward compatibility while allowing us to use the newer service
|
* @returns Promise resolving to user's pubkey or null if login failed
|
||||||
const currentSession = authService.getCurrentSession();
|
*/
|
||||||
|
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 (result.success && result.pubkey) {
|
||||||
if (!currentSession) {
|
// Update UI
|
||||||
// This will trigger a full authentication flow when the user next performs an action
|
updateLoginStatus(result.pubkey);
|
||||||
// requiring signature verification
|
|
||||||
localStorage.setItem('userPublicKey', pubkey);
|
// Notify listeners
|
||||||
|
notifyAuthStateListeners(true, result.pubkey);
|
||||||
|
|
||||||
|
// Execute queued operations
|
||||||
|
executePostAuthQueue();
|
||||||
|
|
||||||
|
return result.pubkey;
|
||||||
}
|
}
|
||||||
} else if (!authenticated) {
|
|
||||||
// Log out through the AuthenticationService
|
return null;
|
||||||
authService.logout();
|
} 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
|
* This ensures the operation only runs after successful authentication
|
||||||
*/
|
*/
|
||||||
export function addPostAuthOperation(operation: () => void): void {
|
export function addPostAuthOperation(operation: () => void): void {
|
||||||
if (isAuthenticated()) {
|
if (authService.isAuthenticated()) {
|
||||||
// If already authenticated, run immediately
|
// If already authenticated, run immediately
|
||||||
operation();
|
operation();
|
||||||
} else {
|
} else {
|
||||||
@ -121,26 +138,15 @@ function executePostAuthQueue(): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen for authentication state changes
|
* Listen for authentication state changes
|
||||||
* Supports both direct callback registration and CustomEvent listeners
|
|
||||||
*/
|
*/
|
||||||
export function onAuthStateChanged(callback: AuthStateListener): () => void {
|
export function onAuthStateChanged(callback: AuthStateListener): () => void {
|
||||||
// Add to direct listeners
|
// Add to direct listeners
|
||||||
authStateListeners.push(callback);
|
authStateListeners.push(callback);
|
||||||
|
|
||||||
// Set up CustomEvent listener for backward compatibility
|
// Also register with the auth service
|
||||||
const handler = (event: Event) => {
|
const unsubscribe = authService.onAuthStateChanged(callback);
|
||||||
const authEvent = event as CustomEvent;
|
|
||||||
callback(
|
|
||||||
authEvent.detail.authenticated,
|
|
||||||
authEvent.detail.pubkey
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
// Return a function to remove the listener
|
||||||
window.addEventListener(AUTH_STATE_CHANGED_EVENT, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a function to remove both listeners
|
|
||||||
return () => {
|
return () => {
|
||||||
// Remove from direct listeners
|
// Remove from direct listeners
|
||||||
const index = authStateListeners.indexOf(callback);
|
const index = authStateListeners.indexOf(callback);
|
||||||
@ -148,10 +154,8 @@ export function onAuthStateChanged(callback: AuthStateListener): () => void {
|
|||||||
authStateListeners.splice(index, 1);
|
authStateListeners.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove CustomEvent listener
|
// Also unsubscribe from auth service
|
||||||
if (typeof window !== 'undefined') {
|
unsubscribe();
|
||||||
window.removeEventListener(AUTH_STATE_CHANGED_EVENT, handler);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,126 +166,185 @@ export function clearAuthState(): void {
|
|||||||
// Use the AuthenticationService to log out
|
// Use the AuthenticationService to log out
|
||||||
authService.logout();
|
authService.logout();
|
||||||
|
|
||||||
// Update the backward-compatible flag
|
// Update UI
|
||||||
userAuthenticated = false;
|
updateLoginStatus(null);
|
||||||
|
|
||||||
// Clear temporary auth state in sessionStorage
|
// Notify listeners
|
||||||
[
|
notifyAuthStateListeners(false);
|
||||||
'nostrLoginInitialized',
|
|
||||||
'nostrAuthInProgress',
|
|
||||||
'nostrLoginState',
|
|
||||||
'nostrAuthPending',
|
|
||||||
'nostrLoginStarted'
|
|
||||||
].forEach(key => {
|
|
||||||
sessionStorage.removeItem(key);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* 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 {
|
export function getCurrentUserPubkey(): string | null {
|
||||||
return authService.getCurrentUserPubkey();
|
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 method HTTP method
|
||||||
* @param url Request URL
|
* @param url Request URL
|
||||||
* @param payload Optional request payload
|
* @param payload Optional request payload
|
||||||
* @returns Headers object with Authorization header, or null if not authenticated
|
* @returns Always returns null
|
||||||
*/
|
*/
|
||||||
export async function createAuthHeaders(
|
export async function createAuthHeaders(
|
||||||
method: string,
|
method: string,
|
||||||
url: string,
|
url: string,
|
||||||
payload?: string
|
payload?: string
|
||||||
): Promise<Headers | null> {
|
): Promise<Headers | null> {
|
||||||
if (!isAuthenticated()) {
|
console.warn('auth-manager.createAuthHeaders is deprecated and will always return null');
|
||||||
return null;
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,10 +3,14 @@
|
|||||||
|
|
||||||
import * as nostrTools from 'nostr-tools';
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
|
||||||
|
import * as authManager from './auth-manager';
|
||||||
import { defaultServerConfig } from './config';
|
import { defaultServerConfig } from './config';
|
||||||
import { NostrService } from './services/NostrService';
|
import { NostrService } from './services/NostrService';
|
||||||
import { toggleTheme } from './theme-utils';
|
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
|
// Module-level variables
|
||||||
let nostrService: NostrService;
|
let nostrService: NostrService;
|
||||||
@ -23,6 +27,40 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Make sure authentication state is fresh from localStorage
|
// Make sure authentication state is fresh from localStorage
|
||||||
authManager.updateAuthStateFromStorage();
|
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
|
// Initialize services
|
||||||
// Create NostrService with a status update callback
|
// Create NostrService with a status update callback
|
||||||
nostrService = new NostrService((message: string, className: string) => {
|
nostrService = new NostrService((message: string, className: string) => {
|
||||||
@ -36,7 +74,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Auto-connect to the default relay after a brief delay
|
// Auto-connect to the default relay after a brief delay
|
||||||
setTimeout(autoConnectToDefaultRelay, 500);
|
setTimeout(autoConnectToDefaultRelay, 500);
|
||||||
setTimeout(autoConnectToDefaultRelay, 500);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,6 +86,13 @@ function updateUIBasedOnAuth(): void {
|
|||||||
// First refresh auth state from storage to ensure it's current
|
// First refresh auth state from storage to ensure it's current
|
||||||
authManager.updateAuthStateFromStorage();
|
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
|
// Enable or disable the create button based on auth state
|
||||||
if (authManager.isAuthenticated()) {
|
if (authManager.isAuthenticated()) {
|
||||||
createBillboardBtn.removeAttribute('disabled');
|
createBillboardBtn.removeAttribute('disabled');
|
||||||
@ -94,45 +138,53 @@ function setupUIElements(): void {
|
|||||||
authManager.updateAuthStateFromStorage();
|
authManager.updateAuthStateFromStorage();
|
||||||
|
|
||||||
// Check if user is authenticated
|
// 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 (!authManager.isAuthenticated()) {
|
||||||
if (window.nostr) {
|
try {
|
||||||
// If nostr extension is available, try to authenticate using our improved service
|
console.log("Attempting authentication with NostrLogin...");
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to basic authentication if needed
|
// Use the NostrLogin integration
|
||||||
try {
|
const pubkey = await authManager.loginWithNostrLogin();
|
||||||
const pubkey = await window.nostr.getPublicKey();
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
// Set authentication and proceed
|
console.log(`Successfully authenticated with pubkey: ${pubkey.substring(0, 8)}...`);
|
||||||
authManager.setAuthenticated(true, pubkey);
|
updateUIBasedOnAuth();
|
||||||
console.log(`Direct login successful, pubkey: ${pubkey.substring(0, 8)}...`);
|
handleCreateBillboard();
|
||||||
handleCreateBillboard();
|
return;
|
||||||
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();
|
handleCreateBillboard();
|
||||||
@ -207,20 +259,26 @@ function updateRelayStatus(message: string, className: string): void {
|
|||||||
async function handleConnectRelay(): Promise<void> {
|
async function handleConnectRelay(): Promise<void> {
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
if (!authManager.isAuthenticated()) {
|
if (!authManager.isAuthenticated()) {
|
||||||
updateRelayStatus('Authentication required to connect', 'error');
|
try {
|
||||||
|
// Use AuthManager to authenticate
|
||||||
// Display an authentication prompt
|
updateRelayStatus('Connecting to Nostr extension...', 'connecting');
|
||||||
const billboardContent = document.getElementById('billboardContent');
|
|
||||||
if (billboardContent) {
|
// Try to authenticate using our improved auth-manager
|
||||||
billboardContent.innerHTML = `
|
const pubkey = await authManager.loginWithNostrLogin();
|
||||||
<div class="auth-required-message">
|
|
||||||
<h3>Authentication Required</h3>
|
if (pubkey) {
|
||||||
<p>You need to be logged in to connect to relays.</p>
|
// Authentication successful
|
||||||
<p>Please visit the <a href="profile.html">Profile page</a> to log in with your Nostr extension.</p>
|
updateUIBasedOnAuth();
|
||||||
</div>
|
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;
|
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)
|
* Process a server advertisement event (kind 31120)
|
||||||
* Open the modal for creating a new billboard
|
* Open the modal for creating a new billboard
|
||||||
*/
|
*/
|
||||||
function handleCreateBillboard(): void {
|
async function handleCreateBillboard(): Promise<void> {
|
||||||
// Check if user is logged in
|
// First check if user is logged in
|
||||||
|
authManager.updateAuthStateFromStorage();
|
||||||
|
|
||||||
if (!authManager.isAuthenticated()) {
|
if (!authManager.isAuthenticated()) {
|
||||||
alert('You need to be logged in to create a billboard. Please visit the Profile page to log in.');
|
// If user isn't logged in, try to authenticate with NostrLogin
|
||||||
return;
|
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
|
// Reset form
|
||||||
@ -536,8 +611,23 @@ async function handleSaveBillboard(e: Event): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
// Check if user is logged in using auth manager
|
// Check if user is logged in using auth manager
|
||||||
if (!authManager.isAuthenticated()) {
|
if (!authManager.isAuthenticated()) {
|
||||||
alert('You need to be logged in to publish a billboard. Please visit the Profile page to log in.');
|
// Try to authenticate with NostrLogin
|
||||||
return;
|
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
|
// Get user's pubkey
|
||||||
@ -813,14 +903,30 @@ function processServerEvent(event: nostrTools.Event): void {
|
|||||||
// Add event listener for the "Edit" button
|
// Add event listener for the "Edit" button
|
||||||
const editBtn = eventCard.querySelector('.billboard-edit-btn');
|
const editBtn = eventCard.querySelector('.billboard-edit-btn');
|
||||||
if (editBtn) {
|
if (editBtn) {
|
||||||
editBtn.addEventListener('click', (e) => {
|
editBtn.addEventListener('click', async (e) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
const eventId = target.getAttribute('data-id');
|
const eventId = target.getAttribute('data-id');
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
// Check if user is logged in
|
// Check if user is logged in
|
||||||
const loggedInPubkey = nostrService.getLoggedInPubkey();
|
const loggedInPubkey = nostrService.getLoggedInPubkey();
|
||||||
if (!loggedInPubkey) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -865,7 +971,16 @@ async function autoConnectToDefaultRelay(): Promise<void> {
|
|||||||
// Update auth status from storage
|
// Update auth status from storage
|
||||||
authManager.updateAuthStateFromStorage();
|
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) {
|
if (!authManager.isAuthenticated() && window.nostr) {
|
||||||
try {
|
try {
|
||||||
console.log('Attempting direct authentication with Nostr extension...');
|
console.log('Attempting direct authentication with Nostr extension...');
|
||||||
@ -938,70 +1053,10 @@ async function autoConnectToDefaultRelay(): Promise<void> {
|
|||||||
connectButton.click();
|
connectButton.click();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('User is not authenticated, showing login prompt...');
|
console.log('User is not authenticated, skipping auto-connect...');
|
||||||
// Show a login prompt with direct login option
|
|
||||||
const billboardContent = document.getElementById('billboardContent');
|
|
||||||
|
|
||||||
// Only show the login prompt if no events were loaded from cache
|
// Just update the relay status without showing the auth required message
|
||||||
if (billboardContent && billboardContent.querySelector('.empty-state')) {
|
updateRelayStatus('Authentication required to connect', 'warning');
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1012,7 +1067,7 @@ async function autoConnectToDefaultRelay(): Promise<void> {
|
|||||||
* @returns Server pubkey or null if not found
|
* @returns Server pubkey or null if not found
|
||||||
*/
|
*/
|
||||||
function getServerPubkeyFromEvent(event: nostrTools.Event): string | null {
|
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
|
// Find the d tag which contains the server pubkey
|
||||||
const dTag = event.tags.find(tag => tag[0] === 'd');
|
const dTag = event.tags.find(tag => tag[0] === 'd');
|
||||||
|
@ -9,10 +9,10 @@
|
|||||||
* 4. Listening for incoming 21121 responses and associating them with requests
|
* 4. Listening for incoming 21121 responses and associating them with requests
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ClientEventStore, ClientEventStatus } from './services/ClientEventStore';
|
|
||||||
import { ClientEventsTable } from './components/ClientEventsTable';
|
import { ClientEventsTable } from './components/ClientEventsTable';
|
||||||
import type { NostrEvent } from './relay';
|
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
|
// Create a singleton instance of the client event store
|
||||||
const clientEventStore = new ClientEventStore();
|
const clientEventStore = new ClientEventStore();
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
// IMPORTANT: Immediately import all critical modules and execute them
|
// IMPORTANT: Immediately import all critical modules and execute them
|
||||||
// This ensures they are included in the bundle and executed immediately
|
// This ensures they are included in the bundle and executed immediately
|
||||||
import './navbar-diagnostics'; // Import diagnostics first
|
|
||||||
import './navbar';
|
import './navbar';
|
||||||
import './navbar-init';
|
import './navbar-init';
|
||||||
|
|
||||||
@ -37,16 +36,18 @@ import './navbar-init';
|
|||||||
import * as nostrTools from 'nostr-tools';
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
|
||||||
// Import type definitions
|
// Import type definitions
|
||||||
import type { NostrEvent } from './converter';
|
import * as authManager from './auth-manager';
|
||||||
// Import functions from internal modules
|
|
||||||
import { displayConvertedEvent } from './converter';
|
|
||||||
// Import client events tracking system
|
|
||||||
import {
|
import {
|
||||||
initClientEventHandler,
|
initClientEventHandler,
|
||||||
trackOutgoingEvent,
|
trackOutgoingEvent,
|
||||||
handleIncomingResponse,
|
handleIncomingResponse,
|
||||||
reconnectRelayService
|
reconnectRelayService
|
||||||
} from './client-event-handler';
|
} 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 { publishToRelay, convertNpubToHex, verifyEvent } from './relay';
|
||||||
// Import profile functions (not using direct imports since we'll load modules based on page)
|
// 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
|
// 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 './receiver'; // Import receiver module for relay connections and subscriptions
|
||||||
import './billboard'; // Import billboard module for server registration display
|
import './billboard'; // Import billboard module for server registration display
|
||||||
import { HttpFormatter } from './services/HttpFormatter'; // Import formatter for HTTP content
|
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 { Nostr31120Service } from './services/Nostr31120Service'; // Import our new dedicated service
|
||||||
|
import { NostrService } from './services/NostrService';
|
||||||
import { getUserPubkey } from './services/NostrUtils';
|
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
|
// Immediately initialize the navbar to ensure it's visible on page load
|
||||||
try {
|
try {
|
||||||
@ -68,6 +67,7 @@ try {
|
|||||||
console.error('[CLIENT] Error initializing navbar:', e);
|
console.error('[CLIENT] Error initializing navbar:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { toggleTheme } from './theme-utils';
|
||||||
import {
|
import {
|
||||||
sanitizeText,
|
sanitizeText,
|
||||||
setDefaultHttpRequest,
|
setDefaultHttpRequest,
|
||||||
@ -78,7 +78,6 @@ import {
|
|||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
// Import toggleTheme from theme-utils instead of 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
|
// On page load, always fetch the latest pubkey from window.nostr
|
||||||
getUserPubkey().then(pubkey => {
|
getUserPubkey().then(pubkey => {
|
||||||
@ -157,7 +156,7 @@ async function handleRelaySearch(): Promise<void> {
|
|||||||
if (!authManager.isAuthenticated()) {
|
if (!authManager.isAuthenticated()) {
|
||||||
console.log('Cannot search relay: User not authenticated');
|
console.log('Cannot search relay: User not authenticated');
|
||||||
// Display authentication prompt instead of performing search
|
// Display authentication prompt instead of performing search
|
||||||
await retryAuthentication();
|
await loginWithNostr();
|
||||||
|
|
||||||
// Check if authentication was successful
|
// Check if authentication was successful
|
||||||
if (!authManager.isAuthenticated()) {
|
if (!authManager.isAuthenticated()) {
|
||||||
@ -591,94 +590,45 @@ function loadSavedServer(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize nostr-login
|
* Initialize direct interaction with Nostr extension
|
||||||
* Prevents multiple initializations by checking if it's already been initialized
|
* Simplified to only check extension availability and current auth state
|
||||||
*/
|
*/
|
||||||
function initNostrLogin(): void {
|
function initNostrLogin(): void {
|
||||||
// Check if we've already initialized NostrLogin in this session
|
try {
|
||||||
const nostrLoginInitialized = window.sessionStorage.getItem('nostrLoginInitialized');
|
// Simply check if we're already authenticated with the extension
|
||||||
if (nostrLoginInitialized === 'true') {
|
if (window.nostr) {
|
||||||
console.log("NostrLogin already initialized in this session, skipping");
|
console.log("Checking Nostr extension for authentication state...");
|
||||||
return;
|
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
try {
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
// Initialize NostrLogin without requiring UI elements
|
console.error(`Error initializing Nostr check: ${errorMessage}`);
|
||||||
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);
|
|
||||||
/**
|
/**
|
||||||
* Reset the Nostr login state in case of errors
|
* 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 {
|
function resetNostrLoginState(): void {
|
||||||
console.log("Resetting Nostr login state...");
|
console.log("Resetting Nostr login state...");
|
||||||
|
|
||||||
// Clear our initialization flags
|
// Reset the auth state in our service
|
||||||
window.sessionStorage.removeItem('nostrLoginInitialized');
|
authManager.clearAuthState();
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Nostr login state has been reset. You can now try again.");
|
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
|
* @returns Promise resolving to whether the user is authenticated
|
||||||
*/
|
*/
|
||||||
async function ensureAuthenticated(): Promise<boolean> {
|
async function ensureAuthenticated(): Promise<boolean> {
|
||||||
// Check if we already have a pubkey in localStorage
|
// Check directly with the Auth service, which checks the extension
|
||||||
const savedPubkey = localStorage.getItem('userPublicKey');
|
const isAuth = await authManager.checkAuthentication();
|
||||||
if (savedPubkey) {
|
|
||||||
console.log(`Found saved pubkey: ${savedPubkey.substring(0, 8)}...`);
|
if (isAuth) {
|
||||||
|
// Already authenticated
|
||||||
|
const pubkey = await authManager.getCurrentUserPubkeyAsync();
|
||||||
|
if (pubkey) {
|
||||||
|
// Update UI with the pubkey
|
||||||
|
updateClientPubkeyDisplay(pubkey);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have a pubkey, check if nostr is available
|
// Not authenticated, check if extension is available
|
||||||
if (!window.nostr) {
|
if (!window.nostr) {
|
||||||
console.warn("Nostr extension not available");
|
console.warn("Nostr extension not available");
|
||||||
return false;
|
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 {
|
||||||
// Try to get the user's public key
|
// Try to get the user's public key
|
||||||
console.log("Starting Nostr authentication...");
|
console.log("Starting Nostr authentication...");
|
||||||
const pubkey = await window.nostr.getPublicKey();
|
const result = await authManager.loginWithNostrLogin();
|
||||||
|
|
||||||
if (pubkey) {
|
return result !== null;
|
||||||
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;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during authentication:", 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;
|
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
|
* Handle the publish button click
|
||||||
@ -1127,244 +1040,100 @@ function handleToggleKeyFormat(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize authentication and ensure the user is signed in
|
* Simple function to authenticate using direct Nostr extension check
|
||||||
* This should be called at startup to check authentication status
|
|
||||||
*/
|
*/
|
||||||
/**
|
async function login(): Promise<string | null> {
|
||||||
* 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;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clear any stale auth state
|
// Use the auth service with direct extension check
|
||||||
window.sessionStorage.removeItem('nostrLoginInitialized');
|
const result = await authManager.authService.authenticate();
|
||||||
['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
|
|
||||||
window.sessionStorage.removeItem(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize NostrLogin
|
if (result.success && result.pubkey) {
|
||||||
initNostrLogin();
|
const pubkey = result.pubkey;
|
||||||
|
console.log(`Authenticated with public key: ${pubkey.substring(0, 8)}...`);
|
||||||
// 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...");
|
|
||||||
|
|
||||||
// First try using our enhanced AuthenticationService
|
// Update UI to show pubkey
|
||||||
try {
|
updateClientPubkeyDisplay(pubkey);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to direct extension request
|
return pubkey;
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.authInProgress = false;
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during authentication:", error);
|
console.error('Login failed:', error);
|
||||||
window.authInProgress = false;
|
|
||||||
return null;
|
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 to login with nostr
|
||||||
(function() {
|
* Uses direct extension check without localStorage or sessions
|
||||||
try {
|
*/
|
||||||
const savedPubkey = localStorage.getItem('userPublicKey');
|
async function loginWithNostr(): Promise<void> {
|
||||||
if (savedPubkey) {
|
console.log("Nostr login requested");
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear UI status
|
// Clear UI status
|
||||||
const statusElement = document.getElementById('authStatus');
|
const statusElement = document.getElementById('authStatus');
|
||||||
if (statusElement) {
|
if (statusElement) {
|
||||||
statusElement.textContent = 'Retrying authentication...';
|
statusElement.textContent = 'Logging in...';
|
||||||
statusElement.className = 'status-message loading';
|
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 {
|
try {
|
||||||
|
// Use our direct extension check approach
|
||||||
const result = await authManager.authService.authenticate();
|
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 based on result
|
||||||
|
if (statusElement) {
|
||||||
// Update UI status
|
if (result.success && result.pubkey) {
|
||||||
if (statusElement) {
|
|
||||||
statusElement.textContent = 'Authentication successful!';
|
statusElement.textContent = 'Authentication successful!';
|
||||||
statusElement.className = 'status-message success';
|
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) {
|
} catch (error) {
|
||||||
console.warn("Could not authenticate with AuthenticationService, falling back:", error);
|
console.error("Error during authentication:", error);
|
||||||
}
|
|
||||||
|
// Update UI status on error
|
||||||
// Fall back to standard authentication
|
if (statusElement) {
|
||||||
const pubkey = await safeAuthenticate();
|
|
||||||
|
|
||||||
// Update status
|
|
||||||
if (statusElement) {
|
|
||||||
if (pubkey) {
|
|
||||||
statusElement.textContent = 'Authentication successful!';
|
|
||||||
statusElement.className = 'status-message success';
|
|
||||||
} else {
|
|
||||||
statusElement.textContent = 'Authentication failed. Please check your Nostr extension.';
|
statusElement.textContent = 'Authentication failed. Please check your Nostr extension.';
|
||||||
statusElement.className = 'status-message error';
|
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
|
// Initialize HTTP response viewer with raw/formatted view support and 21121 integration
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
try {
|
try {
|
||||||
@ -1563,7 +1332,7 @@ function handleToggleClientKeyFormat(): void {
|
|||||||
try {
|
try {
|
||||||
if (currentFormat === 'npub') {
|
if (currentFormat === 'npub') {
|
||||||
// Switch to hex format
|
// Switch to hex format
|
||||||
let hexValue = clientPubkeyElement.dataset.hex;
|
const hexValue = clientPubkeyElement.dataset.hex;
|
||||||
|
|
||||||
if (hexValue) {
|
if (hexValue) {
|
||||||
clientPubkeyElement.textContent = hexValue;
|
clientPubkeyElement.textContent = hexValue;
|
||||||
@ -1573,7 +1342,7 @@ function handleToggleClientKeyFormat(): void {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Switch to npub format
|
// Switch to npub format
|
||||||
let npubValue = clientPubkeyElement.dataset.npub;
|
const npubValue = clientPubkeyElement.dataset.npub;
|
||||||
|
|
||||||
if (npubValue) {
|
if (npubValue) {
|
||||||
clientPubkeyElement.textContent = 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 => {
|
const addAuthUI = (): void => {
|
||||||
// Find a good place for the auth elements (navbar or header)
|
// Find a good place for the auth elements (navbar or header)
|
||||||
const navbarRight = document.querySelector('.nav-right, header');
|
const navbarRight = document.querySelector('.nav-right, header');
|
||||||
@ -1628,37 +1397,59 @@ document.addEventListener('DOMContentLoaded', function(): void {
|
|||||||
authContainer.style.marginLeft = 'auto';
|
authContainer.style.marginLeft = 'auto';
|
||||||
authContainer.style.marginRight = '10px';
|
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
|
// Check current auth status
|
||||||
const pubkey = localStorage.getItem('userPublicKey');
|
const pubkey = localStorage.getItem('userPublicKey');
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
authStatus.textContent = 'Authenticated';
|
// User is authenticated, show status
|
||||||
|
const authStatus = document.createElement('div');
|
||||||
|
authStatus.id = 'authStatus';
|
||||||
authStatus.className = 'status-message success';
|
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 {
|
} else {
|
||||||
authStatus.textContent = 'Not authenticated';
|
// User is not authenticated, show sign in button
|
||||||
authStatus.className = 'status-message warning';
|
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
|
// Add container to navbar
|
||||||
navbarRight.appendChild(authContainer);
|
navbarRight.appendChild(authContainer);
|
||||||
}
|
}
|
||||||
@ -1700,7 +1491,7 @@ document.addEventListener('DOMContentLoaded', function(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt authentication
|
// Attempt authentication
|
||||||
await retryAuthentication();
|
await loginWithNostr();
|
||||||
|
|
||||||
// If authentication succeeded, proceed with search
|
// If authentication succeeded, proceed with search
|
||||||
if (authManager.isAuthenticated()) {
|
if (authManager.isAuthenticated()) {
|
||||||
@ -1740,7 +1531,7 @@ document.addEventListener('DOMContentLoaded', function(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt authentication
|
// Attempt authentication
|
||||||
await retryAuthentication();
|
await loginWithNostr();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authManager.isAuthenticated()) {
|
if (authManager.isAuthenticated()) {
|
||||||
|
@ -5,12 +5,15 @@
|
|||||||
* in a table format with a detailed view modal.
|
* 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 { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
import { reconnectRelayService } from '../client-event-handler';
|
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 {
|
export class ClientEventsTable {
|
||||||
private container: HTMLElement | null = null;
|
private container: HTMLElement | null = null;
|
||||||
@ -270,7 +273,7 @@ export class ClientEventsTable {
|
|||||||
try {
|
try {
|
||||||
const npub = nip19.npubEncode(targetServerTag[1]);
|
const npub = nip19.npubEncode(targetServerTag[1]);
|
||||||
targetServer = `${npub.substring(0, 8)}...${npub.substring(npub.length - 4)}`;
|
targetServer = `${npub.substring(0, 8)}...${npub.substring(npub.length - 4)}`;
|
||||||
} catch (e) {
|
} catch {
|
||||||
targetServer = targetServerTag[1].substring(0, 8) + '...';
|
targetServer = targetServerTag[1].substring(0, 8) + '...';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -279,7 +282,7 @@ export class ClientEventsTable {
|
|||||||
const shortEventId = eventId.substring(0, 8) + '...';
|
const shortEventId = eventId.substring(0, 8) + '...';
|
||||||
|
|
||||||
// Determine status indicator
|
// Determine status indicator
|
||||||
let statusHtml = this.getStatusHtml(storedEvent.status);
|
const statusHtml = this.getStatusHtml(storedEvent.status);
|
||||||
|
|
||||||
// Set row HTML
|
// Set row HTML
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
@ -371,15 +374,19 @@ export class ClientEventsTable {
|
|||||||
|
|
||||||
private loadEventData(eventId: string): void {
|
private loadEventData(eventId: string): void {
|
||||||
const storedEvent = this.eventStore.getEvent(eventId);
|
const storedEvent = this.eventStore.getEvent(eventId);
|
||||||
if (!storedEvent || !this.modal) return;
|
if (!storedEvent || !this.modal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get tab content containers
|
// Get tab content containers
|
||||||
const jsonTab = document.getElementById('tab-21120-json-content');
|
const jsonTab = document.getElementById('tab-21120-json-content');
|
||||||
const httpRequestTab = document.getElementById('tab-21120-http-content');
|
const httpRequestTab = document.getElementById('tab-21120-http-content');
|
||||||
const httpResponseTab = document.getElementById('tab-21121-http-content');
|
const httpResponseTab = document.getElementById('tab-21121-http-content');
|
||||||
const jsonResponseTab = document.getElementById('tab-21121-json-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
|
// Populate 21120 JSON tab
|
||||||
jsonTab.innerHTML = `<pre>${JSON.stringify(storedEvent.event, null, 2)}</pre>`;
|
jsonTab.innerHTML = `<pre>${JSON.stringify(storedEvent.event, null, 2)}</pre>`;
|
||||||
@ -387,7 +394,7 @@ export class ClientEventsTable {
|
|||||||
// Populate 21120 HTTP request tab
|
// Populate 21120 HTTP request tab
|
||||||
try {
|
try {
|
||||||
httpRequestTab.innerHTML = HttpFormatter.formatHttpContent(storedEvent.event.content, true, true);
|
httpRequestTab.innerHTML = HttpFormatter.formatHttpContent(storedEvent.event.content, true, true);
|
||||||
} catch (e) {
|
} catch {
|
||||||
httpRequestTab.innerHTML = `<pre>${storedEvent.event.content}</pre>`;
|
httpRequestTab.innerHTML = `<pre>${storedEvent.event.content}</pre>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,7 +414,7 @@ export class ClientEventsTable {
|
|||||||
// Populate 21121 HTTP response tab
|
// Populate 21121 HTTP response tab
|
||||||
try {
|
try {
|
||||||
httpResponseTab.innerHTML = HttpFormatter.formatHttpContent(responseEvent.content, false, true);
|
httpResponseTab.innerHTML = HttpFormatter.formatHttpContent(responseEvent.content, false, true);
|
||||||
} catch (e) {
|
} catch {
|
||||||
httpResponseTab.innerHTML = `<pre>${responseEvent.content}</pre>`;
|
httpResponseTab.innerHTML = `<pre>${responseEvent.content}</pre>`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -517,7 +524,9 @@ export class ClientEventsTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showModal(eventId: string): void {
|
private showModal(eventId: string): void {
|
||||||
if (!this.modal) return;
|
if (!this.modal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.currentEventId = eventId;
|
this.currentEventId = eventId;
|
||||||
|
|
||||||
@ -535,7 +544,9 @@ export class ClientEventsTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private hideModal(): void {
|
private hideModal(): void {
|
||||||
if (!this.modal) return;
|
if (!this.modal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.modal.style.display = 'none';
|
this.modal.style.display = 'none';
|
||||||
this.currentEventId = null;
|
this.currentEventId = null;
|
||||||
@ -543,7 +554,9 @@ export class ClientEventsTable {
|
|||||||
|
|
||||||
private switchTab(event: Event): void {
|
private switchTab(event: Event): void {
|
||||||
const clickedTab = event.currentTarget as HTMLElement;
|
const clickedTab = event.currentTarget as HTMLElement;
|
||||||
if (!clickedTab || !clickedTab.dataset.tabId) return;
|
if (!clickedTab || !clickedTab.dataset.tabId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Hide all tab contents
|
// Hide all tab contents
|
||||||
const tabContents = document.querySelectorAll('.modal-tab-content');
|
const tabContents = document.querySelectorAll('.modal-tab-content');
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../relay';
|
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';
|
import { HttpFormatter } from '../services/HttpFormatter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,7 +93,7 @@ export class EventDetail {
|
|||||||
* Show empty state when no event is selected
|
* Show empty state when no event is selected
|
||||||
*/
|
*/
|
||||||
private showEmptyState(): void {
|
private showEmptyState(): void {
|
||||||
if (!this.container) return;
|
if (!this.container) {return;}
|
||||||
|
|
||||||
this.container.innerHTML = `
|
this.container.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@ -105,7 +106,7 @@ export class EventDetail {
|
|||||||
* Render the details of the currently selected event
|
* Render the details of the currently selected event
|
||||||
*/
|
*/
|
||||||
private renderEventDetail(): void {
|
private renderEventDetail(): void {
|
||||||
if (!this.container) return;
|
if (!this.container) {return;}
|
||||||
|
|
||||||
// Get the selected event from the EventManager
|
// Get the selected event from the EventManager
|
||||||
const managedEvent = this.eventManager.getSelectedEvent();
|
const managedEvent = this.eventManager.getSelectedEvent();
|
||||||
@ -121,7 +122,7 @@ export class EventDetail {
|
|||||||
const isResponse = event.kind === 21121;
|
const isResponse = event.kind === 21121;
|
||||||
|
|
||||||
// Determine the content to display
|
// Determine the content to display
|
||||||
let httpContent = managedEvent.decrypted ?
|
const httpContent = managedEvent.decrypted ?
|
||||||
managedEvent.decryptedContent || event.content :
|
managedEvent.decryptedContent || event.content :
|
||||||
event.content;
|
event.content;
|
||||||
|
|
||||||
@ -223,7 +224,7 @@ export class EventDetail {
|
|||||||
* Set up tab buttons for switching between raw and formatted views
|
* Set up tab buttons for switching between raw and formatted views
|
||||||
*/
|
*/
|
||||||
private setupTabButtons(): void {
|
private setupTabButtons(): void {
|
||||||
if (!this.container) return;
|
if (!this.container) {return;}
|
||||||
|
|
||||||
const tabButtons = this.container.querySelectorAll('.tab-btn');
|
const tabButtons = this.container.querySelectorAll('.tab-btn');
|
||||||
tabButtons.forEach(button => {
|
tabButtons.forEach(button => {
|
||||||
@ -251,7 +252,7 @@ export class EventDetail {
|
|||||||
* Set up event listeners for related events and action buttons
|
* Set up event listeners for related events and action buttons
|
||||||
*/
|
*/
|
||||||
private setupEventListeners(): void {
|
private setupEventListeners(): void {
|
||||||
if (!this.container) return;
|
if (!this.container) {return;}
|
||||||
|
|
||||||
// Related event links
|
// Related event links
|
||||||
const relatedLinks = this.container.querySelectorAll('.related-event-link');
|
const relatedLinks = this.container.querySelectorAll('.related-event-link');
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
* Modular UI component for rendering and managing a list of Nostr events
|
* Modular UI component for rendering and managing a list of Nostr events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../relay';
|
import type { NostrEvent } from '../relay';
|
||||||
import { EventManager, EventChangeType } from '../services/EventManager';
|
import type { EventManager} from '../services/EventManager';
|
||||||
|
import { EventChangeType } from '../services/EventManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for initializing the EventList component
|
* Options for initializing the EventList component
|
||||||
@ -109,7 +110,7 @@ export class EventList {
|
|||||||
* Create the UI structure for the event list and controls
|
* Create the UI structure for the event list and controls
|
||||||
*/
|
*/
|
||||||
private createUIStructure(): void {
|
private createUIStructure(): void {
|
||||||
if (!this.container) return;
|
if (!this.container) {return;}
|
||||||
|
|
||||||
// Create the header with enhanced search and filter controls
|
// Create the header with enhanced search and filter controls
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
@ -343,7 +344,7 @@ export class EventList {
|
|||||||
private saveCurrentFilter(): void {
|
private saveCurrentFilter(): void {
|
||||||
// Create a dialog to name the filter
|
// Create a dialog to name the filter
|
||||||
const filterName = prompt('Enter a name for this filter:', 'Filter ' + (this.savedFilters.length + 1));
|
const filterName = prompt('Enter a name for this filter:', 'Filter ' + (this.savedFilters.length + 1));
|
||||||
if (!filterName) return;
|
if (!filterName) {return;}
|
||||||
|
|
||||||
// Save current filter configuration
|
// Save current filter configuration
|
||||||
const filterConfig = {
|
const filterConfig = {
|
||||||
@ -370,7 +371,7 @@ export class EventList {
|
|||||||
*/
|
*/
|
||||||
private updateSavedFiltersList(): void {
|
private updateSavedFiltersList(): void {
|
||||||
const savedFiltersList = document.getElementById('savedFiltersList');
|
const savedFiltersList = document.getElementById('savedFiltersList');
|
||||||
if (!savedFiltersList) return;
|
if (!savedFiltersList) {return;}
|
||||||
|
|
||||||
if (this.savedFilters.length === 0) {
|
if (this.savedFilters.length === 0) {
|
||||||
savedFiltersList.innerHTML = '<div class="empty-saved-filters">No saved filters</div>';
|
savedFiltersList.innerHTML = '<div class="empty-saved-filters">No saved filters</div>';
|
||||||
@ -409,7 +410,7 @@ export class EventList {
|
|||||||
* Apply a saved filter
|
* Apply a saved filter
|
||||||
*/
|
*/
|
||||||
private applyFilter(index: number): void {
|
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;
|
const filter = this.savedFilters[index].config;
|
||||||
|
|
||||||
@ -450,7 +451,7 @@ export class EventList {
|
|||||||
* Delete a saved filter
|
* Delete a saved filter
|
||||||
*/
|
*/
|
||||||
private deleteFilter(index: number): void {
|
private deleteFilter(index: number): void {
|
||||||
if (index < 0 || index >= this.savedFilters.length) return;
|
if (index < 0 || index >= this.savedFilters.length) {return;}
|
||||||
|
|
||||||
// Remove the filter
|
// Remove the filter
|
||||||
this.savedFilters.splice(index, 1);
|
this.savedFilters.splice(index, 1);
|
||||||
@ -478,7 +479,7 @@ export class EventList {
|
|||||||
*/
|
*/
|
||||||
private setupVirtualScrolling(): void {
|
private setupVirtualScrolling(): void {
|
||||||
const eventsListWrapper = this.eventsListContainer?.parentElement;
|
const eventsListWrapper = this.eventsListContainer?.parentElement;
|
||||||
if (!eventsListWrapper) return;
|
if (!eventsListWrapper) {return;}
|
||||||
|
|
||||||
// Create intersection observer to detect visible events
|
// Create intersection observer to detect visible events
|
||||||
this.scrollObserver = new IntersectionObserver(
|
this.scrollObserver = new IntersectionObserver(
|
||||||
@ -516,7 +517,7 @@ export class EventList {
|
|||||||
* Render all existing events from the EventManager
|
* Render all existing events from the EventManager
|
||||||
*/
|
*/
|
||||||
private renderExistingEvents(): void {
|
private renderExistingEvents(): void {
|
||||||
if (!this.eventsListContainer) return;
|
if (!this.eventsListContainer) {return;}
|
||||||
|
|
||||||
// Clear existing content and tracked events
|
// Clear existing content and tracked events
|
||||||
this.eventsListContainer.innerHTML = '';
|
this.eventsListContainer.innerHTML = '';
|
||||||
@ -537,7 +538,7 @@ export class EventList {
|
|||||||
this.filteredEventIds.sort((a, b) => {
|
this.filteredEventIds.sort((a, b) => {
|
||||||
const eventA = this.eventManager.getEvent(a);
|
const eventA = this.eventManager.getEvent(a);
|
||||||
const eventB = this.eventManager.getEvent(b);
|
const eventB = this.eventManager.getEvent(b);
|
||||||
if (!eventA || !eventB) return 0;
|
if (!eventA || !eventB) {return 0;}
|
||||||
return eventB.receivedAt - eventA.receivedAt;
|
return eventB.receivedAt - eventA.receivedAt;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -555,7 +556,7 @@ export class EventList {
|
|||||||
* Update which events are visible in the virtual scroll view
|
* Update which events are visible in the virtual scroll view
|
||||||
*/
|
*/
|
||||||
private updateVisibleEvents(): void {
|
private updateVisibleEvents(): void {
|
||||||
if (!this.eventsListContainer || !this.eventsListContainer.parentElement) return;
|
if (!this.eventsListContainer || !this.eventsListContainer.parentElement) {return;}
|
||||||
|
|
||||||
const scrollContainer = this.eventsListContainer.parentElement;
|
const scrollContainer = this.eventsListContainer.parentElement;
|
||||||
const scrollTop = scrollContainer.scrollTop;
|
const scrollTop = scrollContainer.scrollTop;
|
||||||
@ -616,14 +617,14 @@ export class EventList {
|
|||||||
// Apply filters to get filtered IDs
|
// Apply filters to get filtered IDs
|
||||||
this.filteredEventIds = this.allEventIds.filter(eventId => {
|
this.filteredEventIds = this.allEventIds.filter(eventId => {
|
||||||
const managedEvent = this.eventManager.getEvent(eventId);
|
const managedEvent = this.eventManager.getEvent(eventId);
|
||||||
if (!managedEvent) return false;
|
if (!managedEvent) {return false;}
|
||||||
|
|
||||||
const event = managedEvent.event;
|
const event = managedEvent.event;
|
||||||
|
|
||||||
// Filter by server if needed
|
// Filter by server if needed
|
||||||
if (!this.showAllEvents) {
|
if (!this.showAllEvents) {
|
||||||
const isToServer = this.checkIfToServer(event);
|
const isToServer = this.checkIfToServer(event);
|
||||||
if (!isToServer) return false;
|
if (!isToServer) {return false;}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by event type
|
// Filter by event type
|
||||||
@ -680,7 +681,7 @@ export class EventList {
|
|||||||
this.filteredEventIds.sort((a, b) => {
|
this.filteredEventIds.sort((a, b) => {
|
||||||
const eventA = this.eventManager.getEvent(a);
|
const eventA = this.eventManager.getEvent(a);
|
||||||
const eventB = this.eventManager.getEvent(b);
|
const eventB = this.eventManager.getEvent(b);
|
||||||
if (!eventA || !eventB) return 0;
|
if (!eventA || !eventB) {return 0;}
|
||||||
return eventB.receivedAt - eventA.receivedAt;
|
return eventB.receivedAt - eventA.receivedAt;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -701,7 +702,7 @@ export class EventList {
|
|||||||
|
|
||||||
// Legacy code for backward compatibility
|
// Legacy code for backward compatibility
|
||||||
const eventsList = document.getElementById('eventsList');
|
const eventsList = document.getElementById('eventsList');
|
||||||
if (!eventsList) return;
|
if (!eventsList) {return;}
|
||||||
|
|
||||||
const items = eventsList.querySelectorAll('.event-item');
|
const items = eventsList.querySelectorAll('.event-item');
|
||||||
let visibleCount = 0;
|
let visibleCount = 0;
|
||||||
@ -739,7 +740,7 @@ export class EventList {
|
|||||||
* Show empty state when there are no events
|
* Show empty state when there are no events
|
||||||
*/
|
*/
|
||||||
private showEmptyState(): void {
|
private showEmptyState(): void {
|
||||||
if (!this.eventsListContainer) return;
|
if (!this.eventsListContainer) {return;}
|
||||||
|
|
||||||
// Clear virtual scrolling data
|
// Clear virtual scrolling data
|
||||||
this.filteredEventIds = [];
|
this.filteredEventIds = [];
|
||||||
@ -762,11 +763,11 @@ export class EventList {
|
|||||||
private checkIfToServer(event: NostrEvent): boolean {
|
private checkIfToServer(event: NostrEvent): boolean {
|
||||||
// Get server pubkey from EventManager
|
// Get server pubkey from EventManager
|
||||||
const serverPubkey = this.eventManager.getServerPubkey();
|
const serverPubkey = this.eventManager.getServerPubkey();
|
||||||
if (!serverPubkey) return false;
|
if (!serverPubkey) {return false;}
|
||||||
|
|
||||||
// Check for p tag to identify recipient
|
// Check for p tag to identify recipient
|
||||||
const pTag = event.tags.find(tag => tag[0] === 'p');
|
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
|
// Check if the p tag matches our server pubkey
|
||||||
return (pTag[1] === serverPubkey);
|
return (pTag[1] === serverPubkey);
|
||||||
@ -778,7 +779,7 @@ export class EventList {
|
|||||||
private getRecipientDisplay(event: NostrEvent): string {
|
private getRecipientDisplay(event: NostrEvent): string {
|
||||||
// Find recipient if any
|
// Find recipient if any
|
||||||
const pTag = event.tags.find(tag => tag[0] === 'p');
|
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>`;
|
return `<div class="recipient">To: ${pTag[1].substring(0, 8)}...</div>`;
|
||||||
}
|
}
|
||||||
@ -788,11 +789,11 @@ export class EventList {
|
|||||||
*/
|
*/
|
||||||
private renderEventItem(eventId: string): HTMLElement | null {
|
private renderEventItem(eventId: string): HTMLElement | null {
|
||||||
const eventsList = document.getElementById('eventsList');
|
const eventsList = document.getElementById('eventsList');
|
||||||
if (!eventsList) return null;
|
if (!eventsList) {return null;}
|
||||||
|
|
||||||
// Get the event from EventManager
|
// Get the event from EventManager
|
||||||
const managedEvent = this.eventManager.getEvent(eventId);
|
const managedEvent = this.eventManager.getEvent(eventId);
|
||||||
if (!managedEvent) return null;
|
if (!managedEvent) {return null;}
|
||||||
|
|
||||||
const event = managedEvent.event;
|
const event = managedEvent.event;
|
||||||
|
|
||||||
@ -861,10 +862,10 @@ export class EventList {
|
|||||||
const statusCode = statusMatch[1];
|
const statusCode = statusMatch[1];
|
||||||
let statusClass = '';
|
let statusClass = '';
|
||||||
|
|
||||||
if (statusCode.startsWith('2')) statusClass = 'status-success';
|
if (statusCode.startsWith('2')) {statusClass = 'status-success';}
|
||||||
else if (statusCode.startsWith('3')) statusClass = 'status-redirect';
|
else if (statusCode.startsWith('3')) {statusClass = 'status-redirect';}
|
||||||
else if (statusCode.startsWith('4')) statusClass = 'status-client-error';
|
else if (statusCode.startsWith('4')) {statusClass = 'status-client-error';}
|
||||||
else if (statusCode.startsWith('5')) statusClass = 'status-server-error';
|
else if (statusCode.startsWith('5')) {statusClass = 'status-server-error';}
|
||||||
|
|
||||||
httpContext = `<div class="http-preview ${statusClass}">Status: ${statusCode}</div>`;
|
httpContext = `<div class="http-preview ${statusClass}">Status: ${statusCode}</div>`;
|
||||||
}
|
}
|
||||||
@ -1094,7 +1095,7 @@ export class EventList {
|
|||||||
* Highlight the selected event in the UI
|
* Highlight the selected event in the UI
|
||||||
*/
|
*/
|
||||||
private highlightSelectedEvent(eventId: string): void {
|
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
|
// 1. Remove selected class from all visible items
|
||||||
this.visibleEvents.forEach((element) => {
|
this.visibleEvents.forEach((element) => {
|
||||||
@ -1172,7 +1173,7 @@ export class EventList {
|
|||||||
private displayResponsesForRequest(requestId: string): void {
|
private displayResponsesForRequest(requestId: string): void {
|
||||||
// Find the responses list container
|
// Find the responses list container
|
||||||
const responsesListContainer = document.getElementById('responsesList');
|
const responsesListContainer = document.getElementById('responsesList');
|
||||||
if (!responsesListContainer) return;
|
if (!responsesListContainer) {return;}
|
||||||
|
|
||||||
// Get related response events from EventManager
|
// Get related response events from EventManager
|
||||||
const responses = this.eventManager.getResponsesForRequest(requestId);
|
const responses = this.eventManager.getResponsesForRequest(requestId);
|
||||||
@ -1210,10 +1211,10 @@ export class EventList {
|
|||||||
const statusCode = statusMatch[1];
|
const statusCode = statusMatch[1];
|
||||||
let statusClass = '';
|
let statusClass = '';
|
||||||
|
|
||||||
if (statusCode.startsWith('2')) statusClass = 'status-success';
|
if (statusCode.startsWith('2')) {statusClass = 'status-success';}
|
||||||
else if (statusCode.startsWith('3')) statusClass = 'status-redirect';
|
else if (statusCode.startsWith('3')) {statusClass = 'status-redirect';}
|
||||||
else if (statusCode.startsWith('4')) statusClass = 'status-client-error';
|
else if (statusCode.startsWith('4')) {statusClass = 'status-client-error';}
|
||||||
else if (statusCode.startsWith('5')) statusClass = 'status-server-error';
|
else if (statusCode.startsWith('5')) {statusClass = 'status-server-error';}
|
||||||
|
|
||||||
statusInfo = `<span class="status-code ${statusClass}">Status: ${statusCode}</span>`;
|
statusInfo = `<span class="status-code ${statusClass}">Status: ${statusCode}</span>`;
|
||||||
}
|
}
|
||||||
@ -1253,14 +1254,14 @@ export class EventList {
|
|||||||
*/
|
*/
|
||||||
private displayResponseJson(event: NostrEvent): void {
|
private displayResponseJson(event: NostrEvent): void {
|
||||||
const jsonContainer = document.getElementById('response21121Json');
|
const jsonContainer = document.getElementById('response21121Json');
|
||||||
if (!jsonContainer) return;
|
if (!jsonContainer) {return;}
|
||||||
|
|
||||||
// Make container visible
|
// Make container visible
|
||||||
jsonContainer.style.display = 'block';
|
jsonContainer.style.display = 'block';
|
||||||
|
|
||||||
// Get the pre element
|
// Get the pre element
|
||||||
const pre = jsonContainer.querySelector('pre.json-content');
|
const pre = jsonContainer.querySelector('pre.json-content');
|
||||||
if (!pre) return;
|
if (!pre) {return;}
|
||||||
|
|
||||||
// Format JSON with indentation
|
// Format JSON with indentation
|
||||||
pre.textContent = JSON.stringify(event, null, 2);
|
pre.textContent = JSON.stringify(event, null, 2);
|
||||||
|
@ -5,9 +5,10 @@
|
|||||||
* Includes a modal dialog to show detailed information when a row is clicked
|
* 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 { EventChangeType, EventKind } from '../services/EventManager';
|
||||||
import type { EventManager } from '../services/EventManager';
|
import type { EventManager } from '../services/EventManager';
|
||||||
import { nip19 } from 'nostr-tools';
|
|
||||||
import { HttpFormatter } from '../services/HttpFormatter';
|
import { HttpFormatter } from '../services/HttpFormatter';
|
||||||
|
|
||||||
export class HttpMessagesTable {
|
export class HttpMessagesTable {
|
||||||
@ -302,7 +303,7 @@ export class HttpMessagesTable {
|
|||||||
* @param eventId The ID of the event to show details for
|
* @param eventId The ID of the event to show details for
|
||||||
*/
|
*/
|
||||||
private showModal(eventId: string): void {
|
private showModal(eventId: string): void {
|
||||||
if (!this.modal) return;
|
if (!this.modal) {return;}
|
||||||
|
|
||||||
this.currentEventId = eventId;
|
this.currentEventId = eventId;
|
||||||
|
|
||||||
@ -323,7 +324,7 @@ export class HttpMessagesTable {
|
|||||||
* Hide the modal dialog
|
* Hide the modal dialog
|
||||||
*/
|
*/
|
||||||
private hideModal(): void {
|
private hideModal(): void {
|
||||||
if (!this.modal) return;
|
if (!this.modal) {return;}
|
||||||
|
|
||||||
this.modal.style.display = 'none';
|
this.modal.style.display = 'none';
|
||||||
this.currentEventId = null;
|
this.currentEventId = null;
|
||||||
@ -335,7 +336,7 @@ export class HttpMessagesTable {
|
|||||||
*/
|
*/
|
||||||
private switchTab(event: Event): void {
|
private switchTab(event: Event): void {
|
||||||
const clickedTab = event.currentTarget as HTMLElement;
|
const clickedTab = event.currentTarget as HTMLElement;
|
||||||
if (!clickedTab || !clickedTab.dataset.tabId) return;
|
if (!clickedTab || !clickedTab.dataset.tabId) {return;}
|
||||||
|
|
||||||
// Hide all tab contents
|
// Hide all tab contents
|
||||||
const tabContents = document.querySelectorAll('.modal-tab-content');
|
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
|
* This can be called when a new 21121 response is received
|
||||||
*/
|
*/
|
||||||
public updateResponseIndicators(): void {
|
public updateResponseIndicators(): void {
|
||||||
if (!this.tableBody) return;
|
if (!this.tableBody) {return;}
|
||||||
|
|
||||||
const rows = this.tableBody.querySelectorAll('.event-row');
|
const rows = this.tableBody.querySelectorAll('.event-row');
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../relay';
|
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 { ToastNotifier } from '../services/ToastNotifier';
|
||||||
import { EventManager } from '../services/EventManager';
|
|
||||||
|
|
||||||
// Result of an HTTP request execution
|
// Result of an HTTP request execution
|
||||||
export interface ExecutionResult {
|
export interface ExecutionResult {
|
||||||
|
@ -9,9 +9,10 @@
|
|||||||
* - Publishing to relays
|
* - Publishing to relays
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../relay';
|
|
||||||
import * as nostrTools from 'nostr-tools';
|
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';
|
import { ToastNotifier } from '../services/ToastNotifier';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -185,7 +186,7 @@ export class Nostr21121Creator {
|
|||||||
const pubKey = nostrTools.getPublicKey(privateKeyBytes);
|
const pubKey = nostrTools.getPublicKey(privateKeyBytes);
|
||||||
|
|
||||||
// Initialize tags array
|
// Initialize tags array
|
||||||
let tags: string[][] = [];
|
const tags: string[][] = [];
|
||||||
|
|
||||||
// Always add reference to the request event
|
// Always add reference to the request event
|
||||||
if (requestEvent.id) {
|
if (requestEvent.id) {
|
||||||
@ -197,7 +198,7 @@ export class Nostr21121Creator {
|
|||||||
|
|
||||||
// Check if the original event has a p tag (recipient)
|
// Check if the original event has a p tag (recipient)
|
||||||
const pTag = requestEvent.tags.find(tag => tag[0] === 'p');
|
const pTag = requestEvent.tags.find(tag => tag[0] === 'p');
|
||||||
let finalContent = responseContent;
|
const finalContent = responseContent;
|
||||||
|
|
||||||
if (pTag && pTag[1]) {
|
if (pTag && pTag[1]) {
|
||||||
// Add p tag to reference the recipient
|
// Add p tag to reference the recipient
|
||||||
|
@ -3,10 +3,11 @@
|
|||||||
* Handles HTTP response display and 21121 response event creation
|
* 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 { HttpFormatter } from '../services/HttpFormatter';
|
||||||
import { ToastNotifier } from '../services/ToastNotifier';
|
import { ToastNotifier } from '../services/ToastNotifier';
|
||||||
import { ExecutionResult } from './HttpRequestExecutor';
|
|
||||||
|
import type { ExecutionResult } from './HttpRequestExecutor';
|
||||||
import { Nostr21121Creator, Creation21121Result } from './Nostr21121Creator';
|
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
|
* Add a button to create a 21121 response event if not already present
|
||||||
*/
|
*/
|
||||||
private addCreate21121Button(): void {
|
private addCreate21121Button(): void {
|
||||||
if (!this.modalElement) return;
|
if (!this.modalElement) {return;}
|
||||||
|
|
||||||
// Create or update status element
|
// Create or update status element
|
||||||
if (!this.creationStatus) {
|
if (!this.creationStatus) {
|
||||||
@ -180,11 +181,11 @@ export class ResponseViewer {
|
|||||||
|
|
||||||
// Check if button already exists
|
// Check if button already exists
|
||||||
const existingButton = this.modalElement.querySelector('.create-21121-btn');
|
const existingButton = this.modalElement.querySelector('.create-21121-btn');
|
||||||
if (existingButton) return;
|
if (existingButton) {return;}
|
||||||
|
|
||||||
// Get the modal header
|
// Get the modal header
|
||||||
const modalHeader = this.modalElement.querySelector('.http-response-header');
|
const modalHeader = this.modalElement.querySelector('.http-response-header');
|
||||||
if (!modalHeader) return;
|
if (!modalHeader) {return;}
|
||||||
|
|
||||||
// Create button
|
// Create button
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
@ -205,7 +206,7 @@ export class ResponseViewer {
|
|||||||
* Set up event listeners for modal interactions
|
* Set up event listeners for modal interactions
|
||||||
*/
|
*/
|
||||||
private setupModalEventListeners(): void {
|
private setupModalEventListeners(): void {
|
||||||
if (!this.modalElement) return;
|
if (!this.modalElement) {return;}
|
||||||
|
|
||||||
// Handle close button click
|
// Handle close button click
|
||||||
const closeBtn = this.modalElement.querySelector('.close-modal-btn');
|
const closeBtn = this.modalElement.querySelector('.close-modal-btn');
|
||||||
@ -223,7 +224,7 @@ export class ResponseViewer {
|
|||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
// Get the tab ID
|
// Get the tab ID
|
||||||
const tabId = (button as HTMLElement).dataset.tab;
|
const tabId = (button as HTMLElement).dataset.tab;
|
||||||
if (!tabId) return;
|
if (!tabId) {return;}
|
||||||
|
|
||||||
// Remove active class from all buttons and content
|
// Remove active class from all buttons and content
|
||||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
@ -272,7 +273,7 @@ export class ResponseViewer {
|
|||||||
|
|
||||||
if (hasResponse) {
|
if (hasResponse) {
|
||||||
const shouldOverwrite = confirm('A response already exists for this request. Create another one?');
|
const shouldOverwrite = confirm('A response already exists for this request. Create another one?');
|
||||||
if (!shouldOverwrite) return;
|
if (!shouldOverwrite) {return;}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a sample response
|
// Generate a sample response
|
||||||
@ -312,7 +313,7 @@ export class ResponseViewer {
|
|||||||
* Show a dialog with options for creating a 21121 response
|
* Show a dialog with options for creating a 21121 response
|
||||||
*/
|
*/
|
||||||
private showCreateResponseDialog(requestEventId: string, responseContent: string): void {
|
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
|
// Update and show the status element
|
||||||
this.creationStatus.className = 'creation-status info';
|
this.creationStatus.className = 'creation-status info';
|
||||||
|
@ -4,19 +4,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventManager } from '../services/EventManager';
|
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 { NostrEventService } from '../services/NostrEventService.updated';
|
||||||
import { NostrRelayService } from '../services/NostrRelayService';
|
import { NostrRelayService } from '../services/NostrRelayService';
|
||||||
import { NostrCacheService } from '../services/NostrCacheService';
|
import { NostrService } from '../services/NostrService';
|
||||||
import { HttpService } from '../services/HttpService';
|
|
||||||
import { HttpClient } from '../services/HttpClient';
|
|
||||||
import { ToastNotifier } from '../services/ToastNotifier';
|
import { ToastNotifier } from '../services/ToastNotifier';
|
||||||
|
|
||||||
|
import { EventDetail } from './EventDetail';
|
||||||
|
import { EventList } from './EventList';
|
||||||
|
import { HttpMessagesTable } from './HttpMessagesTable';
|
||||||
import { HttpRequestExecutor } from './HttpRequestExecutor';
|
import { HttpRequestExecutor } from './HttpRequestExecutor';
|
||||||
import { ResponseViewer } from './ResponseViewer';
|
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 {
|
private connectToRelay(): void {
|
||||||
const relayUrlInput = document.getElementById(this.options.relayUrlInput) as HTMLInputElement;
|
const relayUrlInput = document.getElementById(this.options.relayUrlInput) as HTMLInputElement;
|
||||||
if (!relayUrlInput) return;
|
if (!relayUrlInput) {return;}
|
||||||
|
|
||||||
const relayUrl = relayUrlInput.value.trim() || 'wss://relay.degmods.com';
|
const relayUrl = relayUrlInput.value.trim() || 'wss://relay.degmods.com';
|
||||||
if (!relayUrl) {
|
if (!relayUrl) {
|
||||||
|
@ -31,7 +31,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Import nostr-login for encryption/signing
|
// Import nostr-login for encryption/signing
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
// Define better types for the NostrLogin module
|
// Define better types for the NostrLogin module
|
||||||
interface NostrLoginModule {
|
interface NostrLoginModule {
|
||||||
signEvent: (event: NostrEvent) => Promise<NostrEvent>;
|
signEvent: (event: NostrEvent) => Promise<NostrEvent>;
|
||||||
|
@ -19,7 +19,7 @@ export function initHttpResponseViewer(): void {
|
|||||||
// Handle tab switching
|
// Handle tab switching
|
||||||
if (target && target.classList.contains('tab-btn')) {
|
if (target && target.classList.contains('tab-btn')) {
|
||||||
const tabContainer = target.closest('.http-response-tabs, .event-detail-tabs');
|
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
|
// Get all tab buttons and content in this container
|
||||||
const tabButtons = tabContainer.querySelectorAll('.tab-btn');
|
const tabButtons = tabContainer.querySelectorAll('.tab-btn');
|
||||||
@ -31,7 +31,7 @@ export function initHttpResponseViewer(): void {
|
|||||||
tabContentContainer = tabContainer.closest('.modal-content, .event-details');
|
tabContentContainer = tabContainer.closest('.modal-content, .event-details');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tabContentContainer) return;
|
if (!tabContentContainer) {return;}
|
||||||
|
|
||||||
const tabContents = tabContentContainer.querySelectorAll('.tab-content');
|
const tabContents = tabContentContainer.querySelectorAll('.tab-content');
|
||||||
|
|
||||||
@ -100,24 +100,24 @@ async function executeHttpRequest(button: HTMLElement): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
// Find the HTTP content
|
// Find the HTTP content
|
||||||
const eventDetails = button.closest('.event-details');
|
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
|
// Find the HTTP content element - look in both formatted and raw tabs
|
||||||
const httpContentElement =
|
const httpContentElement =
|
||||||
eventDetails.querySelector('#raw-http .http-content') ||
|
eventDetails.querySelector('#raw-http .http-content') ||
|
||||||
eventDetails.querySelector('.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 || '';
|
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
|
// Get the event ID
|
||||||
const headerElement = eventDetails.querySelector('.event-detail-header h3');
|
const headerElement = eventDetails.querySelector('.event-detail-header h3');
|
||||||
const eventIdMatch = headerElement?.textContent?.match(/ID: (\w+)\.\.\./);
|
const eventIdMatch = headerElement?.textContent?.match(/ID: (\w+)\.\.\./);
|
||||||
const eventId = eventIdMatch ? eventIdMatch[1] : null;
|
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
|
// Execute the HTTP request
|
||||||
const response = await executeRequest(httpContent);
|
const response = await executeRequest(httpContent);
|
||||||
@ -161,7 +161,7 @@ async function executeRequest(requestContent: string): Promise<string> {
|
|||||||
|
|
||||||
// Extract the host
|
// Extract the host
|
||||||
let host = '';
|
let host = '';
|
||||||
let headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
let body = '';
|
let body = '';
|
||||||
let inHeaders = true;
|
let inHeaders = true;
|
||||||
|
|
||||||
|
@ -3,12 +3,13 @@
|
|||||||
* Handles displaying HTTP responses and 21121 integration
|
* Handles displaying HTTP responses and 21121 integration
|
||||||
* Refactored to use EventManager for centralized event data management
|
* Refactored to use EventManager for centralized event data management
|
||||||
*/
|
*/
|
||||||
import { NostrEvent } from './relay';
|
import type { NostrEvent } from './relay';
|
||||||
import { HttpFormatter } from './services/HttpFormatter';
|
import type { EventManager} from './services/EventManager';
|
||||||
import { ToastNotifier } from './services/ToastNotifier';
|
import { EventKind, EventChangeType } from './services/EventManager';
|
||||||
import { EventManager, EventKind, EventChangeType } from './services/EventManager';
|
|
||||||
import { HttpService } from './services/HttpService';
|
|
||||||
import { HttpClient } from './services/HttpClient';
|
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
|
// Services that will be dynamically imported to avoid circular dependencies
|
||||||
let nostrService: any = null;
|
let nostrService: any = null;
|
||||||
@ -36,7 +37,7 @@ export function initHttpResponseViewer(
|
|||||||
// Handle tab switching
|
// Handle tab switching
|
||||||
if (target && target.classList.contains('tab-btn')) {
|
if (target && target.classList.contains('tab-btn')) {
|
||||||
const tabContainer = target.closest('.http-response-tabs, .event-detail-tabs');
|
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
|
// Get all tab buttons and content in this container
|
||||||
const tabButtons = tabContainer.querySelectorAll('.tab-btn');
|
const tabButtons = tabContainer.querySelectorAll('.tab-btn');
|
||||||
@ -48,7 +49,7 @@ export function initHttpResponseViewer(
|
|||||||
tabContentContainer = tabContainer.closest('.modal-content, .event-details');
|
tabContentContainer = tabContainer.closest('.modal-content, .event-details');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tabContentContainer) return;
|
if (!tabContentContainer) {return;}
|
||||||
|
|
||||||
const tabContents = tabContentContainer.querySelectorAll('.tab-content');
|
const tabContents = tabContentContainer.querySelectorAll('.tab-content');
|
||||||
|
|
||||||
|
@ -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');
|
|
||||||
}
|
|
@ -1,8 +1,6 @@
|
|||||||
// External dependencies
|
// External dependencies
|
||||||
import * as nostrTools from 'nostr-tools';
|
import * as nostrTools from 'nostr-tools';
|
||||||
import qrcode from 'qrcode-generator';
|
import qrcode from 'qrcode-generator';
|
||||||
import * as authManager from './auth-manager';
|
|
||||||
import { SecureStorageService } from './services/SecureStorageService';
|
|
||||||
|
|
||||||
// Interface for profile data
|
// Interface for profile data
|
||||||
interface ProfileData {
|
interface ProfileData {
|
||||||
@ -10,7 +8,8 @@ interface ProfileData {
|
|||||||
about?: string;
|
about?: string;
|
||||||
picture?: string;
|
picture?: string;
|
||||||
nip05?: string;
|
nip05?: string;
|
||||||
[key: string]: any;
|
// Allow additional string properties for Nostr metadata
|
||||||
|
[key: string]: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Element references
|
// Element references
|
||||||
@ -37,7 +36,7 @@ let currentProfileData: ProfileData = {
|
|||||||
nip05: ''
|
nip05: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect to extension
|
// Connect with Nostr extension directly
|
||||||
async function connectWithExtension(): Promise<string | null> {
|
async function connectWithExtension(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
if (!window.nostr) {
|
if (!window.nostr) {
|
||||||
@ -45,54 +44,36 @@ async function connectWithExtension(): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the AuthenticationService via auth-manager for a more secure login
|
// Use the Nostr extension directly
|
||||||
const result = await authManager.authService.authenticate();
|
try {
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
const pubkey = await window.nostr.getPublicKey();
|
const pubkey = await window.nostr.getPublicKey();
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
currentPubkey = pubkey;
|
currentPubkey = pubkey;
|
||||||
|
|
||||||
// Set the authenticated state using auth-manager
|
// Log the successful authentication
|
||||||
authManager.setAuthenticated(true, pubkey);
|
console.log(`Authentication successful using Nostr extension for pubkey: ${currentPubkey.substring(0, 8)}...`);
|
||||||
console.log(`Authentication successful (legacy) for pubkey: ${pubkey.substring(0, 8)}...`);
|
|
||||||
|
|
||||||
updateConnectionStatus(`Connected with extension using pubkey: ${pubkey.substring(0, 8)}...`, true);
|
updateConnectionStatus(`Connected with extension using pubkey: ${currentPubkey.substring(0, 8)}...`, true);
|
||||||
showProfile(pubkey);
|
showProfile(currentPubkey);
|
||||||
|
|
||||||
// Return the pubkey so it can be used by the caller
|
// Return the pubkey so it can be used by the caller
|
||||||
return pubkey;
|
return currentPubkey;
|
||||||
} else {
|
} else {
|
||||||
authManager.setAuthenticated(false);
|
// Authentication failed
|
||||||
updateConnectionStatus('Failed to get public key from extension', false);
|
updateConnectionStatus(`Authentication failed: Could not get pubkey from extension`, false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateConnectionStatus(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`, false);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error connecting with extension:', 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);
|
updateConnectionStatus(`Error: ${err instanceof Error ? err.message : String(err)}`, false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual pubkey functionality removed as users are automatically logged in
|
|
||||||
|
|
||||||
// Update connection status UI
|
// Update connection status UI
|
||||||
function updateConnectionStatus(message: string, isConnected: boolean): void {
|
function updateConnectionStatus(message: string, isConnected: boolean): void {
|
||||||
if (!connectionStatus) {return;}
|
if (!connectionStatus) {return;}
|
||||||
@ -216,7 +197,7 @@ async function fetchProfileData(pubkey: string): Promise<void> {
|
|||||||
if (typeof msg.data !== 'string') {return;}
|
if (typeof msg.data !== 'string') {return;}
|
||||||
|
|
||||||
try {
|
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) {
|
if (Array.isArray(data) && data[0] === 'EVENT' && data[1] === requestId) {
|
||||||
events.push(data[2]);
|
events.push(data[2]);
|
||||||
connected = true;
|
connected = true;
|
||||||
@ -324,7 +305,6 @@ function updateStats(): void {
|
|||||||
if (!statsRequestsSent || !statsResponsesReceived || !statsRelaysConnected) {return;}
|
if (!statsRequestsSent || !statsResponsesReceived || !statsRelaysConnected) {return;}
|
||||||
|
|
||||||
// For this demo, just set some placeholder values
|
// 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';
|
statsRequestsSent.textContent = '0';
|
||||||
statsResponsesReceived.textContent = '0';
|
statsResponsesReceived.textContent = '0';
|
||||||
statsRelaysConnected.textContent = '0';
|
statsRelaysConnected.textContent = '0';
|
||||||
@ -371,8 +351,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
statsResponsesReceived = document.getElementById('responsesReceived');
|
statsResponsesReceived = document.getElementById('responsesReceived');
|
||||||
statsRelaysConnected = document.getElementById('relaysConnected');
|
statsRelaysConnected = document.getElementById('relaysConnected');
|
||||||
|
|
||||||
// Auto-connect, no manual button needed
|
|
||||||
|
|
||||||
// Copy npub button
|
// Copy npub button
|
||||||
const copyNpubBtn = document.getElementById('copyNpubBtn');
|
const copyNpubBtn = document.getElementById('copyNpubBtn');
|
||||||
if (copyNpubBtn) {
|
if (copyNpubBtn) {
|
||||||
@ -385,39 +363,19 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
refreshProfileBtn.addEventListener('click', refreshProfile);
|
refreshProfileBtn.addEventListener('click', refreshProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to connect automatically
|
// Try to connect automatically if nostr extension is available
|
||||||
if (window.nostr) {
|
if (window.nostr) {
|
||||||
// If extension is available, try to connect with it
|
|
||||||
try {
|
try {
|
||||||
const pubkey = await connectWithExtension();
|
const pubkey = await connectWithExtension();
|
||||||
|
|
||||||
if (pubkey) {
|
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)}...`);
|
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 {
|
} else {
|
||||||
console.warn('Failed to get pubkey from extension');
|
console.warn('Failed to get pubkey from extension');
|
||||||
authManager.setAuthenticated(false);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error auto-connecting with extension:', error);
|
console.error('Error auto-connecting with extension:', error);
|
||||||
// Clear authentication state on error
|
|
||||||
authManager.setAuthenticated(false);
|
|
||||||
// Show profile container even if connection fails
|
// Show profile container even if connection fails
|
||||||
if (profileContainer) {
|
if (profileContainer) {
|
||||||
profileContainer.classList.remove('hidden');
|
profileContainer.classList.remove('hidden');
|
||||||
@ -427,8 +385,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
} else {
|
} else {
|
||||||
// Even without an extension, show the profile container with default information
|
// Even without an extension, show the profile container with default information
|
||||||
console.log('No Nostr extension available, showing default profile');
|
console.log('No Nostr extension available, showing default profile');
|
||||||
// Ensure authentication state is cleared
|
|
||||||
authManager.setAuthenticated(false);
|
|
||||||
if (profileContainer) {
|
if (profileContainer) {
|
||||||
profileContainer.classList.remove('hidden');
|
profileContainer.classList.remove('hidden');
|
||||||
updateProfileWithDefaults();
|
updateProfileWithDefaults();
|
||||||
|
@ -7,10 +7,10 @@ import * as nostrTools from 'nostr-tools';
|
|||||||
// Import service classes
|
// Import service classes
|
||||||
import { defaultServerConfig } from './config';
|
import { defaultServerConfig } from './config';
|
||||||
import { HttpService } from './services/HttpService';
|
import { HttpService } from './services/HttpService';
|
||||||
import { NostrService } from './services/NostrService';
|
|
||||||
import { ReceivedEvent } from './services/NostrEventService';
|
import { ReceivedEvent } from './services/NostrEventService';
|
||||||
import { decryptKeyWithNostrExtension, decryptKeyWithNostrTools, decryptWithWebCrypto } from './utils/crypto-utils';
|
import { NostrService } from './services/NostrService';
|
||||||
import { UiService } from './services/UiService';
|
import { UiService } from './services/UiService';
|
||||||
|
import { decryptKeyWithNostrExtension, decryptKeyWithNostrTools, decryptWithWebCrypto } from './utils/crypto-utils';
|
||||||
|
|
||||||
// Module-level service instances
|
// Module-level service instances
|
||||||
let uiService: UiService;
|
let uiService: UiService;
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
* server-ui.ts
|
* server-ui.ts
|
||||||
* Entry point for the 1120 server UI
|
* Entry point for the 1120 server UI
|
||||||
*/
|
*/
|
||||||
import './navbar-diagnostics'; // Import diagnostics first
|
|
||||||
import './navbar'; // Import navbar component
|
import './navbar'; // Import navbar component
|
||||||
import './navbar-init'; // Import navbar initialization
|
import './navbar-init'; // Import navbar initialization
|
||||||
import * as nostrTools from 'nostr-tools';
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
|
||||||
import { initServerUI } from './components/ServerUI';
|
import { initServerUI } from './components/ServerUI';
|
||||||
import './debug-events'; // Import debug script
|
import './debug-events'; // Import debug script
|
||||||
|
|
||||||
@ -191,7 +191,7 @@ function setupServerIdentityManager(): void {
|
|||||||
|
|
||||||
// Toggle format button
|
// Toggle format button
|
||||||
toggleFormatBtn.addEventListener('click', () => {
|
toggleFormatBtn.addEventListener('click', () => {
|
||||||
if (!serverNsec || !serverPubkeyHex || !serverPubkeyNpub) return;
|
if (!serverNsec || !serverPubkeyHex || !serverPubkeyNpub) {return;}
|
||||||
|
|
||||||
isShowingNpub = !isShowingNpub;
|
isShowingNpub = !isShowingNpub;
|
||||||
|
|
||||||
@ -208,7 +208,7 @@ function setupServerIdentityManager(): void {
|
|||||||
|
|
||||||
// Copy button
|
// Copy button
|
||||||
copyServerNpubBtn.addEventListener('click', () => {
|
copyServerNpubBtn.addEventListener('click', () => {
|
||||||
if (!serverNsec) return;
|
if (!serverNsec) {return;}
|
||||||
|
|
||||||
navigator.clipboard.writeText(serverNpubInput.value)
|
navigator.clipboard.writeText(serverNpubInput.value)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -1,30 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* AuthenticationService.ts
|
* AuthenticationService.ts
|
||||||
*
|
*
|
||||||
* Provides authentication services using Nostr protocol.
|
* Stateless authentication service that directly uses the Nostr extension.
|
||||||
* Implements NIP-98 (HTTP Auth) for authenticated requests.
|
* Removed all localStorage and session dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as nostrTools from 'nostr-tools';
|
import type * 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for authentication operation results
|
* Interface for authentication operation results
|
||||||
@ -32,14 +13,15 @@ export interface AuthSession {
|
|||||||
export interface AuthResult {
|
export interface AuthResult {
|
||||||
/** Whether the operation was successful */
|
/** Whether the operation was successful */
|
||||||
success: boolean;
|
success: boolean;
|
||||||
/** The session if successful */
|
/** User's public key if successful */
|
||||||
session?: AuthSession;
|
pubkey?: string;
|
||||||
/** Error message if unsuccessful */
|
/** Error message if unsuccessful */
|
||||||
error?: string;
|
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 {
|
export interface Nip98AuthEvent extends nostrTools.Event {
|
||||||
/** Event tags including method, uri, etc. */
|
/** 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';
|
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 {
|
export class AuthenticationService {
|
||||||
private secureStorage: SecureStorageService;
|
// Cache for pubkey (in-memory only, not persisted)
|
||||||
private currentSession: AuthSession | null = null;
|
private cachedPubkey: string | null = null;
|
||||||
|
|
||||||
// Storage keys
|
// Listeners for auth state changes
|
||||||
private readonly SESSION_KEY = 'authSession';
|
private listeners: ((authenticated: boolean, pubkey?: string) => void)[] = [];
|
||||||
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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.secureStorage = new SecureStorageService();
|
// Nothing to initialize - stateless design
|
||||||
this.loadSessionFromStorage();
|
|
||||||
this.setupEventHandlers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* @returns Promise resolving to authentication result
|
||||||
*/
|
*/
|
||||||
public async authenticate(): Promise<AuthResult> {
|
public async authenticate(): Promise<AuthResult> {
|
||||||
try {
|
try {
|
||||||
// Check if Nostr extension exists
|
// Check if a Nostr extension is available
|
||||||
if (!window.nostr) {
|
if (!window.nostr) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
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();
|
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 {
|
return {
|
||||||
success: false,
|
success: true,
|
||||||
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: '',
|
|
||||||
pubkey
|
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 {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
session
|
error: 'Failed to get public key'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Authentication error:', error);
|
console.error('Authentication error:', error);
|
||||||
|
|
||||||
|
// Return error result
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Unknown authentication error'
|
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
|
* Check if the user is currently authenticated by attempting to fetch their pubkey
|
||||||
*
|
* @returns Promise resolving to boolean indicating authentication status
|
||||||
* @returns True if authenticated with a valid session
|
|
||||||
*/
|
*/
|
||||||
public isAuthenticated(): boolean {
|
public async checkAuthentication(): Promise<boolean> {
|
||||||
if (!this.currentSession) {
|
try {
|
||||||
return false;
|
// If we already have a cached pubkey, verify it's still valid
|
||||||
}
|
if (this.cachedPubkey) {
|
||||||
|
return true;
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 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 {
|
public isAuthenticated(): boolean {
|
||||||
if (this.isAuthenticated()) {
|
return this.cachedPubkey !== null;
|
||||||
return this.currentSession;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current user's public key
|
* 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 {
|
public getCurrentUserPubkey(): string | null {
|
||||||
return this.currentSession?.pubkey || null;
|
return this.cachedPubkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout the current user
|
* Clear the cached pubkey (logout)
|
||||||
*/
|
*/
|
||||||
public logout(): void {
|
public logout(): void {
|
||||||
const wasPreviouslyAuthenticated = this.isAuthenticated();
|
// Clear the cached pubkey
|
||||||
const previousPubkey = this.getCurrentUserPubkey();
|
this.cachedPubkey = null;
|
||||||
|
|
||||||
this.currentSession = null;
|
// Clear global reference
|
||||||
this.secureStorage.remove(this.SESSION_KEY);
|
if (window) {
|
||||||
this.secureStorage.remove(this.REFRESH_TOKEN_KEY);
|
window.nostrPubkey = undefined;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const extension = extendBy || this.SESSION_DURATION;
|
// Notify listeners
|
||||||
this.currentSession.expiresAt = Date.now() + extension;
|
this.notifyAuthStateChanged(false);
|
||||||
this.persistSession(this.currentSession);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -331,6 +217,10 @@ export class AuthenticationService {
|
|||||||
public onAuthStateChanged(
|
public onAuthStateChanged(
|
||||||
callback: (authenticated: boolean, pubkey?: string) => void
|
callback: (authenticated: boolean, pubkey?: string) => void
|
||||||
): () => void {
|
): () => void {
|
||||||
|
// Add to internal listeners array
|
||||||
|
this.listeners.push(callback);
|
||||||
|
|
||||||
|
// Also listen for CustomEvent for backwards compatibility
|
||||||
const handler = (event: Event) => {
|
const handler = (event: Event) => {
|
||||||
const authEvent = event as CustomEvent;
|
const authEvent = event as CustomEvent;
|
||||||
callback(
|
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 () => {
|
return () => {
|
||||||
window.removeEventListener(this.AUTH_STATE_CHANGED_EVENT, handler);
|
// Remove from internal array
|
||||||
};
|
const index = this.listeners.indexOf(callback);
|
||||||
}
|
if (index !== -1) {
|
||||||
|
this.listeners.splice(index, 1);
|
||||||
/**
|
}
|
||||||
* 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;
|
|
||||||
|
|
||||||
// Notify if a valid session is loaded
|
// Remove window event listener
|
||||||
if (this.isAuthenticated()) {
|
window.removeEventListener(AUTH_STATE_CHANGED_EVENT, handler);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -436,26 +251,50 @@ export class AuthenticationService {
|
|||||||
* @param pubkey The user's pubkey (if authenticated)
|
* @param pubkey The user's pubkey (if authenticated)
|
||||||
*/
|
*/
|
||||||
private notifyAuthStateChanged(authenticated: boolean, pubkey?: string): void {
|
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(
|
window.dispatchEvent(
|
||||||
new CustomEvent(this.AUTH_STATE_CHANGED_EVENT, {
|
new CustomEvent(AUTH_STATE_CHANGED_EVENT, {
|
||||||
detail: { authenticated, pubkey }
|
detail: { authenticated, pubkey }
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate SHA-256
|
* Create a signed NIP-98 HTTP Auth event (stub)
|
||||||
*
|
*
|
||||||
* @param message Message to hash
|
* @deprecated This method is deprecated and maintained only for backwards compatibility
|
||||||
* @returns Base64-encoded hash
|
* @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> {
|
public async createAuthEvent(
|
||||||
const encoder = new TextEncoder();
|
method: HttpMethod,
|
||||||
const data = encoder.encode(message);
|
url: string,
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
payload?: string
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
): Promise<Nip98AuthEvent | null> {
|
||||||
const base64Hash = btoa(String.fromCharCode(...hashArray));
|
console.warn('AuthenticationService.createAuthEvent is deprecated and returns null');
|
||||||
|
return null;
|
||||||
return base64Hash;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NostrEvent } from '../relay';
|
import type { NostrEvent } from '../relay';
|
||||||
|
|
||||||
import { EventChangeType } from './EventManager';
|
import { EventChangeType } from './EventManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,9 +3,10 @@
|
|||||||
* Component for rendering detailed event information
|
* Component for rendering detailed event information
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../relay';
|
import type { NostrEvent } from '../relay';
|
||||||
import { ReceivedEvent } from './NostrEventService';
|
|
||||||
import { HttpFormatter } from './HttpFormatter';
|
import { HttpFormatter } from './HttpFormatter';
|
||||||
|
import type { ReceivedEvent } from './NostrEventService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for rendering event details in the UI
|
* Class for rendering event details in the UI
|
||||||
@ -84,7 +85,7 @@ export class EventDetailsRenderer {
|
|||||||
isResponse: boolean,
|
isResponse: boolean,
|
||||||
eventTime: string
|
eventTime: string
|
||||||
): void {
|
): void {
|
||||||
if (!this.eventDetails) return;
|
if (!this.eventDetails) {return;}
|
||||||
|
|
||||||
this.eventDetails.innerHTML = `
|
this.eventDetails.innerHTML = `
|
||||||
<div class="event-details-header">
|
<div class="event-details-header">
|
||||||
@ -209,10 +210,10 @@ ${JSON.stringify(event, null, 2)}
|
|||||||
* Set up tab button click handlers
|
* Set up tab button click handlers
|
||||||
*/
|
*/
|
||||||
private setupTabButtons(): void {
|
private setupTabButtons(): void {
|
||||||
if (!this.eventDetails) return;
|
if (!this.eventDetails) {return;}
|
||||||
|
|
||||||
const tabButtons = this.eventDetails.querySelectorAll('.tab-btn');
|
const tabButtons = this.eventDetails.querySelectorAll('.tab-btn');
|
||||||
if (tabButtons.length === 0) return;
|
if (tabButtons.length === 0) {return;}
|
||||||
|
|
||||||
tabButtons.forEach(button => {
|
tabButtons.forEach(button => {
|
||||||
button.addEventListener('click', (e) => {
|
button.addEventListener('click', (e) => {
|
||||||
@ -284,19 +285,19 @@ ${JSON.stringify(event, null, 2)}
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Skip if the user has navigated to another event
|
// 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)
|
// 1. Load related events first (fast operation)
|
||||||
await this.loadRelatedEvents(eventId, event, isRequest);
|
await this.loadRelatedEvents(eventId, event, isRequest);
|
||||||
|
|
||||||
// Skip if the user has navigated to another event
|
// Skip if the user has navigated to another event
|
||||||
if (this.currentEventId !== eventId) return;
|
if (this.currentEventId !== eventId) {return;}
|
||||||
|
|
||||||
// 2. Set up action buttons
|
// 2. Set up action buttons
|
||||||
this.setupActionButtons(eventId, event, isRequest);
|
this.setupActionButtons(eventId, event, isRequest);
|
||||||
|
|
||||||
// Skip if the user has navigated to another event
|
// Skip if the user has navigated to another event
|
||||||
if (this.currentEventId !== eventId) return;
|
if (this.currentEventId !== eventId) {return;}
|
||||||
|
|
||||||
// 3. Get the HTTP content
|
// 3. Get the HTTP content
|
||||||
const httpContent = receivedEvent.decrypted ?
|
const httpContent = receivedEvent.decrypted ?
|
||||||
@ -307,7 +308,7 @@ ${JSON.stringify(event, null, 2)}
|
|||||||
this.updateRawContent(eventId, httpContent, receivedEvent.decrypted);
|
this.updateRawContent(eventId, httpContent, receivedEvent.decrypted);
|
||||||
|
|
||||||
// Skip if the user has navigated to another event
|
// 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)
|
// 5. Update formatted content (most expensive operation)
|
||||||
this.updateFormattedContent(eventId, httpContent, isRequest, isResponse || is21121Event, receivedEvent.decrypted);
|
this.updateFormattedContent(eventId, httpContent, isRequest, isResponse || is21121Event, receivedEvent.decrypted);
|
||||||
@ -342,7 +343,7 @@ ${JSON.stringify(event, null, 2)}
|
|||||||
isRequest: boolean
|
isRequest: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const relatedEventsContainer = document.getElementById('related-events-container');
|
const relatedEventsContainer = document.getElementById('related-events-container');
|
||||||
if (!relatedEventsContainer) return;
|
if (!relatedEventsContainer) {return;}
|
||||||
|
|
||||||
// Get related events
|
// Get related events
|
||||||
const relatedIds = event.id ? (this.relatedEvents.get(event.id) || []) : [];
|
const relatedIds = event.id ? (this.relatedEvents.get(event.id) || []) : [];
|
||||||
@ -395,7 +396,7 @@ ${JSON.stringify(event, null, 2)}
|
|||||||
isRequest: boolean
|
isRequest: boolean
|
||||||
): void {
|
): void {
|
||||||
const actionsContainer = document.getElementById(`http-actions-${eventId}`);
|
const actionsContainer = document.getElementById(`http-actions-${eventId}`);
|
||||||
if (!actionsContainer) return;
|
if (!actionsContainer) {return;}
|
||||||
|
|
||||||
// Get related events count
|
// Get related events count
|
||||||
const relatedIds = event.id ? (this.relatedEvents.get(event.id) || []) : [];
|
const relatedIds = event.id ? (this.relatedEvents.get(event.id) || []) : [];
|
||||||
@ -541,11 +542,11 @@ ${JSON.stringify(event, null, 2)}
|
|||||||
private displayResponse21121Json(event: NostrEvent): void {
|
private displayResponse21121Json(event: NostrEvent): void {
|
||||||
// Get the JSON container
|
// Get the JSON container
|
||||||
const jsonContainer = document.getElementById('response21121Json');
|
const jsonContainer = document.getElementById('response21121Json');
|
||||||
if (!jsonContainer) return;
|
if (!jsonContainer) {return;}
|
||||||
|
|
||||||
// Get the pre element within the container
|
// Get the pre element within the container
|
||||||
const preElement = jsonContainer.querySelector('pre.json-content');
|
const preElement = jsonContainer.querySelector('pre.json-content');
|
||||||
if (!preElement) return;
|
if (!preElement) {return;}
|
||||||
|
|
||||||
// Format the JSON prettily
|
// Format the JSON prettily
|
||||||
const formattedJson = JSON.stringify(event, null, 2);
|
const formattedJson = JSON.stringify(event, null, 2);
|
||||||
|
@ -5,7 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../relay';
|
import { NostrEvent } from '../relay';
|
||||||
import { EventManager, EventChangeType, ManagedEvent } from './EventManager';
|
|
||||||
|
import type { EventManager} from './EventManager';
|
||||||
|
import { EventChangeType, ManagedEvent } from './EventManager';
|
||||||
import { HttpFormatter } from './HttpFormatter';
|
import { HttpFormatter } from './HttpFormatter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,7 +71,7 @@ export class EventDetailsRenderer {
|
|||||||
* Show empty state when no event is selected
|
* Show empty state when no event is selected
|
||||||
*/
|
*/
|
||||||
private showEmptyState(): void {
|
private showEmptyState(): void {
|
||||||
if (!this.eventDetails) return;
|
if (!this.eventDetails) {return;}
|
||||||
|
|
||||||
this.eventDetails.innerHTML = `
|
this.eventDetails.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@ -82,7 +84,7 @@ export class EventDetailsRenderer {
|
|||||||
* Render the details of the currently selected event
|
* Render the details of the currently selected event
|
||||||
*/
|
*/
|
||||||
private renderEventDetails(): void {
|
private renderEventDetails(): void {
|
||||||
if (!this.eventDetails) return;
|
if (!this.eventDetails) {return;}
|
||||||
|
|
||||||
// Get the selected event from the EventManager
|
// Get the selected event from the EventManager
|
||||||
const managedEvent = this.eventManager.getSelectedEvent();
|
const managedEvent = this.eventManager.getSelectedEvent();
|
||||||
@ -99,7 +101,7 @@ export class EventDetailsRenderer {
|
|||||||
const is21121Event = event.kind === 21121;
|
const is21121Event = event.kind === 21121;
|
||||||
|
|
||||||
// Determine the content to display
|
// Determine the content to display
|
||||||
let httpContent = managedEvent.decrypted ?
|
const httpContent = managedEvent.decrypted ?
|
||||||
managedEvent.decryptedContent || event.content :
|
managedEvent.decryptedContent || event.content :
|
||||||
event.content;
|
event.content;
|
||||||
|
|
||||||
@ -212,7 +214,7 @@ export class EventDetailsRenderer {
|
|||||||
* Set up tab buttons for switching between raw and formatted views
|
* Set up tab buttons for switching between raw and formatted views
|
||||||
*/
|
*/
|
||||||
private setupTabButtons(): void {
|
private setupTabButtons(): void {
|
||||||
if (!this.eventDetails) return;
|
if (!this.eventDetails) {return;}
|
||||||
|
|
||||||
const tabButtons = this.eventDetails.querySelectorAll('.tab-btn');
|
const tabButtons = this.eventDetails.querySelectorAll('.tab-btn');
|
||||||
tabButtons.forEach(button => {
|
tabButtons.forEach(button => {
|
||||||
@ -240,7 +242,7 @@ export class EventDetailsRenderer {
|
|||||||
* Set up links to related events
|
* Set up links to related events
|
||||||
*/
|
*/
|
||||||
private setupRelatedEventLinks(): void {
|
private setupRelatedEventLinks(): void {
|
||||||
if (!this.eventDetails) return;
|
if (!this.eventDetails) {return;}
|
||||||
|
|
||||||
const relatedLinks = this.eventDetails.querySelectorAll('.related-event-link');
|
const relatedLinks = this.eventDetails.querySelectorAll('.related-event-link');
|
||||||
relatedLinks.forEach(link => {
|
relatedLinks.forEach(link => {
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as nostrTools from 'nostr-tools';
|
import * as nostrTools from 'nostr-tools';
|
||||||
import { NostrEvent } from '../relay';
|
|
||||||
|
import type { NostrEvent } from '../relay';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for rendering events in the UI list
|
* Class for rendering events in the UI list
|
||||||
|
@ -5,8 +5,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as nostrTools from 'nostr-tools';
|
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
|
* Class for rendering events in the UI list
|
||||||
@ -61,7 +64,7 @@ export class EventListRenderer {
|
|||||||
* Render existing events from the EventManager
|
* Render existing events from the EventManager
|
||||||
*/
|
*/
|
||||||
private renderExistingEvents(): void {
|
private renderExistingEvents(): void {
|
||||||
if (!this.eventsList) return;
|
if (!this.eventsList) {return;}
|
||||||
|
|
||||||
// Clear any existing content
|
// Clear any existing content
|
||||||
this.eventsList.innerHTML = '';
|
this.eventsList.innerHTML = '';
|
||||||
@ -170,7 +173,7 @@ export class EventListRenderer {
|
|||||||
* @param eventId The ID of the event to update
|
* @param eventId The ID of the event to update
|
||||||
*/
|
*/
|
||||||
private updateEventItem(eventId: string): void {
|
private updateEventItem(eventId: string): void {
|
||||||
if (!this.eventsList) return;
|
if (!this.eventsList) {return;}
|
||||||
|
|
||||||
// Find the existing event item
|
// Find the existing event item
|
||||||
const existingItem = this.eventsList.querySelector(`.event-item[data-id="${eventId}"]`);
|
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
|
* @param eventId The ID of the event to remove
|
||||||
*/
|
*/
|
||||||
private removeEventItem(eventId: string): void {
|
private removeEventItem(eventId: string): void {
|
||||||
if (!this.eventsList) return;
|
if (!this.eventsList) {return;}
|
||||||
|
|
||||||
const eventItem = this.eventsList.querySelector(`.event-item[data-id="${eventId}"]`);
|
const eventItem = this.eventsList.querySelector(`.event-item[data-id="${eventId}"]`);
|
||||||
if (eventItem) {
|
if (eventItem) {
|
||||||
@ -210,7 +213,7 @@ export class EventListRenderer {
|
|||||||
* @param eventId The ID of the event to highlight
|
* @param eventId The ID of the event to highlight
|
||||||
*/
|
*/
|
||||||
private highlightSelectedEvent(eventId: string): void {
|
private highlightSelectedEvent(eventId: string): void {
|
||||||
if (!this.eventsList) return;
|
if (!this.eventsList) {return;}
|
||||||
|
|
||||||
// Remove selected class from all items
|
// Remove selected class from all items
|
||||||
const allItems = this.eventsList.querySelectorAll('.event-item');
|
const allItems = this.eventsList.querySelectorAll('.event-item');
|
||||||
@ -234,11 +237,11 @@ export class EventListRenderer {
|
|||||||
private checkIfToServer(event: NostrEvent): boolean {
|
private checkIfToServer(event: NostrEvent): boolean {
|
||||||
// Get server pubkey from EventManager
|
// Get server pubkey from EventManager
|
||||||
const serverPubkey = this.eventManager.getServerPubkey();
|
const serverPubkey = this.eventManager.getServerPubkey();
|
||||||
if (!serverPubkey) return false;
|
if (!serverPubkey) {return false;}
|
||||||
|
|
||||||
// Check for p tag to identify recipient
|
// Check for p tag to identify recipient
|
||||||
const pTag = event.tags.find(tag => tag[0] === 'p');
|
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
|
// Check if the p tag matches our server pubkey
|
||||||
return (pTag[1] === serverPubkey);
|
return (pTag[1] === serverPubkey);
|
||||||
@ -252,7 +255,7 @@ export class EventListRenderer {
|
|||||||
private getRecipientDisplay(event: NostrEvent): string {
|
private getRecipientDisplay(event: NostrEvent): string {
|
||||||
// Find recipient if any
|
// Find recipient if any
|
||||||
const pTag = event.tags.find(tag => tag[0] === 'p');
|
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>`;
|
return `<div class="recipient">To: ${pTag[1].substring(0, 8)}...</div>`;
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
* with UI components and event services.
|
* with UI components and event services.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventManager } from './EventManager';
|
|
||||||
import { EventListRenderer } from './EventListRenderer.updated';
|
|
||||||
import { EventDetailsRenderer } from './EventDetailsRenderer.updated';
|
import { EventDetailsRenderer } from './EventDetailsRenderer.updated';
|
||||||
import { NostrRelayService } from './NostrRelayService';
|
import { EventListRenderer } from './EventListRenderer.updated';
|
||||||
|
import { EventManager } from './EventManager';
|
||||||
import { NostrCacheService } from './NostrCacheService';
|
import { NostrCacheService } from './NostrCacheService';
|
||||||
import { NostrEventService } from './NostrEventService.updated';
|
import { NostrEventService } from './NostrEventService.updated';
|
||||||
|
import { NostrRelayService } from './NostrRelayService';
|
||||||
import { RelayStatusManager } from './RelayStatusManager';
|
import { RelayStatusManager } from './RelayStatusManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
* Test cases and examples for the EventManager service
|
* Test cases and examples for the EventManager service
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { NostrEvent } from '../relay';
|
||||||
|
|
||||||
import { EventManager, EventKind, EventChangeType } from './EventManager';
|
import { EventManager, EventKind, EventChangeType } from './EventManager';
|
||||||
import { NostrEvent } from '../relay';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a sample NostrEvent for testing
|
* Create a sample NostrEvent for testing
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* Centralizes event data management for 21120 and 21121 events
|
* Centralizes event data management for 21120 and 21121 events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../relay';
|
import type { NostrEvent } from '../relay';
|
||||||
|
|
||||||
// Event types we're managing
|
// Event types we're managing
|
||||||
export enum EventKind {
|
export enum EventKind {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* Service for making HTTP requests
|
* Service for making HTTP requests
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpService } from './HttpService';
|
import type { HttpService } from './HttpService';
|
||||||
import { ToastNotifier } from './ToastNotifier';
|
import { ToastNotifier } from './ToastNotifier';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
// Import crypto utilities
|
// Import crypto utilities
|
||||||
import { encryptWithWebCrypto, decryptWithWebCrypto } from '../utils/crypto-utils';
|
import { encryptWithWebCrypto, decryptWithWebCrypto } from '../utils/crypto-utils';
|
||||||
|
|
||||||
import { ToastNotifier } from './ToastNotifier';
|
import { ToastNotifier } from './ToastNotifier';
|
||||||
|
|
||||||
// Interface definitions
|
// Interface definitions
|
||||||
|
@ -4,12 +4,14 @@
|
|||||||
* Automatically processes incoming 21120 requests to generate 21121 responses
|
* Automatically processes incoming 21120 requests to generate 21121 responses
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../relay';
|
import type { NostrEvent } from '../relay';
|
||||||
import { NostrService } from './NostrService';
|
|
||||||
import { EventManager, EventChangeType, EventKind } from './EventManager';
|
import type { EventManager} from './EventManager';
|
||||||
import { HttpClient } from './HttpClient';
|
import { EventChangeType, EventKind } from './EventManager';
|
||||||
import { ToastNotifier } from './ToastNotifier';
|
import type { HttpClient } from './HttpClient';
|
||||||
import { Nostr21121Service } from './Nostr21121Service';
|
import { Nostr21121Service } from './Nostr21121Service';
|
||||||
|
import type { NostrService } from './NostrService';
|
||||||
|
import { ToastNotifier } from './ToastNotifier';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling NIP-21121 HTTP response events
|
* Class for handling NIP-21121 HTTP response events
|
||||||
@ -69,7 +71,7 @@ export class Nostr21121EventHandler {
|
|||||||
*/
|
*/
|
||||||
private async handleNewEvent(eventId: string): Promise<void> {
|
private async handleNewEvent(eventId: string): Promise<void> {
|
||||||
const managedEvent = this.eventManager.getEvent(eventId);
|
const managedEvent = this.eventManager.getEvent(eventId);
|
||||||
if (!managedEvent) return;
|
if (!managedEvent) {return;}
|
||||||
|
|
||||||
const event = managedEvent.event;
|
const event = managedEvent.event;
|
||||||
|
|
||||||
|
@ -3,11 +3,12 @@
|
|||||||
* Handles integration between HTTP content and NIP-21121 events
|
* Handles integration between HTTP content and NIP-21121 events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../../src/relay';
|
import type { NostrEvent } from '../../src/relay';
|
||||||
import { Nostr21121Service } from './Nostr21121Service';
|
|
||||||
import { NostrEventService } from './NostrEventService';
|
|
||||||
import { ToastNotifier } from './ToastNotifier';
|
|
||||||
import { HttpFormatter } from './HttpFormatter';
|
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
|
* Helper class for NIP-21121 integration with HTTP content
|
||||||
@ -66,7 +67,7 @@ export class Nostr21121IntegrationHelper {
|
|||||||
try {
|
try {
|
||||||
// Extract the event ID from the header
|
// Extract the event ID from the header
|
||||||
const eventIdMatch = eventDetails.querySelector('h3')?.textContent?.match(/ID: (\w+)\.\.\./);
|
const eventIdMatch = eventDetails.querySelector('h3')?.textContent?.match(/ID: (\w+)\.\.\./);
|
||||||
if (!eventIdMatch || !eventIdMatch[1]) return;
|
if (!eventIdMatch || !eventIdMatch[1]) {return;}
|
||||||
|
|
||||||
const eventId = eventIdMatch[1];
|
const eventId = eventIdMatch[1];
|
||||||
|
|
||||||
@ -81,7 +82,7 @@ export class Nostr21121IntegrationHelper {
|
|||||||
|
|
||||||
// Get the HTTP content
|
// Get the HTTP content
|
||||||
const httpContent = eventDetails.querySelector('.http-content')?.textContent;
|
const httpContent = eventDetails.querySelector('.http-content')?.textContent;
|
||||||
if (!httpContent) return;
|
if (!httpContent) {return;}
|
||||||
|
|
||||||
// Execute the HTTP request
|
// Execute the HTTP request
|
||||||
try {
|
try {
|
||||||
@ -126,7 +127,7 @@ export class Nostr21121IntegrationHelper {
|
|||||||
*/
|
*/
|
||||||
private displayHttpResponse(responseContent: string): void {
|
private displayHttpResponse(responseContent: string): void {
|
||||||
// Create or get the modal
|
// Create or get the modal
|
||||||
let modal = document.getElementById('httpResponseModal');
|
const modal = document.getElementById('httpResponseModal');
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
console.error('HTTP response modal not found in DOM');
|
console.error('HTTP response modal not found in DOM');
|
||||||
return;
|
return;
|
||||||
@ -239,7 +240,7 @@ async function executeHttpRequestWithFetch(requestContent: string): Promise<stri
|
|||||||
|
|
||||||
// Extract the host
|
// Extract the host
|
||||||
let host = '';
|
let host = '';
|
||||||
let headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
let body = '';
|
let body = '';
|
||||||
let inHeaders = true;
|
let inHeaders = true;
|
||||||
|
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
* Handler for creating and managing NIP-21121 HTTP response events
|
* Handler for creating and managing NIP-21121 HTTP response events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../relay';
|
import type { NostrEvent } from '../relay';
|
||||||
import { NostrService } from './NostrService';
|
|
||||||
|
import type { NostrService } from './NostrService';
|
||||||
import { ToastNotifier } from './ToastNotifier';
|
import { ToastNotifier } from './ToastNotifier';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
* NIP-21121 Service for HTTP response events
|
* NIP-21121 Service for HTTP response events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NostrEvent } from '../relay';
|
|
||||||
import * as nostrTools from 'nostr-tools';
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
|
||||||
|
import type { NostrEvent } from '../relay';
|
||||||
import { encryptWithNostrTools, encryptWithWebCrypto } from '../utils/crypto-utils';
|
import { encryptWithNostrTools, encryptWithWebCrypto } from '../utils/crypto-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,7 +92,7 @@ export class Nostr21121Service {
|
|||||||
console.log(`Using pubkey: ${pubKey.substring(0, 8)}...`);
|
console.log(`Using pubkey: ${pubKey.substring(0, 8)}...`);
|
||||||
|
|
||||||
// Initialize tags array
|
// Initialize tags array
|
||||||
let tags: string[][] = [];
|
const tags: string[][] = [];
|
||||||
|
|
||||||
// Always add reference to the request event
|
// Always add reference to the request event
|
||||||
if (requestEvent.id) {
|
if (requestEvent.id) {
|
||||||
|
@ -7,9 +7,10 @@
|
|||||||
import * as nostrTools from 'nostr-tools';
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
|
||||||
import type { NostrEvent } from '../relay';
|
import type { NostrEvent } from '../relay';
|
||||||
|
|
||||||
|
import type { NostrCacheService } from './NostrCacheService';
|
||||||
import type { NostrFilter } from './NostrEventService';
|
import type { NostrFilter } from './NostrEventService';
|
||||||
import type { NostrRelayService } from './NostrRelayService';
|
import type { NostrRelayService } from './NostrRelayService';
|
||||||
import type { NostrCacheService } from './NostrCacheService';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for working with kind 31120 events (HTTP-over-Nostr server registrations)
|
* 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
|
* @returns Server pubkey or null if not found
|
||||||
*/
|
*/
|
||||||
public getServerPubkeyFromEvent(event: NostrEvent): string | null {
|
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
|
// Find the d tag which contains the server pubkey
|
||||||
const dTag = event.tags.find(tag => tag[0] === 'd');
|
const dTag = event.tags.find(tag => tag[0] === 'd');
|
||||||
|
@ -9,9 +9,11 @@ import * as nostrTools from 'nostr-tools';
|
|||||||
|
|
||||||
// Project imports
|
// Project imports
|
||||||
import type { NostrEvent } from '../relay';
|
import type { NostrEvent } from '../relay';
|
||||||
|
|
||||||
|
import type { EventManager} from './EventManager';
|
||||||
|
import { EventKind, EventChangeType } from './EventManager';
|
||||||
import type { NostrCacheService, ProfileData } from './NostrCacheService';
|
import type { NostrCacheService, ProfileData } from './NostrCacheService';
|
||||||
import type { NostrRelayService } from './NostrRelayService';
|
import type { NostrRelayService } from './NostrRelayService';
|
||||||
import { EventManager, EventKind, EventChangeType } from './EventManager';
|
|
||||||
|
|
||||||
// Interface for a Nostr subscription
|
// Interface for a Nostr subscription
|
||||||
export interface NostrSubscription {
|
export interface NostrSubscription {
|
||||||
@ -272,7 +274,7 @@ export class NostrEventService {
|
|||||||
const events = this.cacheService.getCachedEvents(relayUrl);
|
const events = this.cacheService.getCachedEvents(relayUrl);
|
||||||
if (events) {
|
if (events) {
|
||||||
const event = events.find(e => e.id === eventId);
|
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');
|
const events = this.cacheService.getCachedEvents('memory');
|
||||||
if (events) {
|
if (events) {
|
||||||
const event = events.find(e => e.id === eventId);
|
const event = events.find(e => e.id === eventId);
|
||||||
if (event) return event;
|
if (event) {return event;}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -3,20 +3,19 @@
|
|||||||
* Main service that coordinates Nostr protocol functionality by integrating specialized services
|
* Main service that coordinates Nostr protocol functionality by integrating specialized services
|
||||||
*/
|
*/
|
||||||
// Project imports
|
// Project imports
|
||||||
|
import * as authManager from '../auth-manager';
|
||||||
import type { NostrEvent } from '../relay';
|
import type { NostrEvent } from '../relay';
|
||||||
|
|
||||||
// Import auth manager to gate network requests
|
// Import auth manager to gate network requests
|
||||||
import * as authManager from '../auth-manager';
|
|
||||||
// Import NIP-98 HTTP Auth types
|
// Import NIP-98 HTTP Auth types
|
||||||
import type { Nip98AuthEvent } from './AuthenticationService';
|
import type { Nip98AuthEvent } from './AuthenticationService';
|
||||||
|
import { Nostr21121Service } from './Nostr21121Service';
|
||||||
|
import { Nostr31120Service } from './Nostr31120Service';
|
||||||
import type { ProfileData } from './NostrCacheService';
|
import type { ProfileData } from './NostrCacheService';
|
||||||
import { NostrCacheService } from './NostrCacheService';
|
import { NostrCacheService } from './NostrCacheService';
|
||||||
import type { NostrFilter, NostrSubscription } from './NostrEventService';
|
import type { NostrFilter, NostrSubscription } from './NostrEventService';
|
||||||
import { NostrEventService } from './NostrEventService';
|
import { NostrEventService } from './NostrEventService';
|
||||||
import { NostrRelayService } from './NostrRelayService';
|
import { NostrRelayService } from './NostrRelayService';
|
||||||
import { Nostr31120Service } from './Nostr31120Service';
|
|
||||||
import { Nostr21121Service } from './Nostr21121Service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for managing Nostr functionality
|
* Class for managing Nostr functionality
|
||||||
|
@ -149,7 +149,7 @@ export class SecureStorageService {
|
|||||||
|
|
||||||
for (const fullKey of ourKeys) {
|
for (const fullKey of ourKeys) {
|
||||||
const storedValue = localStorage.getItem(fullKey);
|
const storedValue = localStorage.getItem(fullKey);
|
||||||
if (!storedValue) continue;
|
if (!storedValue) {continue;}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const storedItem = JSON.parse(storedValue);
|
const storedItem = JSON.parse(storedValue);
|
||||||
|
@ -19,7 +19,7 @@ export class ToastNotifier {
|
|||||||
* Initialize the toast container
|
* Initialize the toast container
|
||||||
*/
|
*/
|
||||||
private static initialize(): void {
|
private static initialize(): void {
|
||||||
if (this.initialized) return;
|
if (this.initialized) {return;}
|
||||||
|
|
||||||
this.toastContainer = document.getElementById('toast-container');
|
this.toastContainer = document.getElementById('toast-container');
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ export class ToastNotifier {
|
|||||||
public static show(message: string, type: NotificationType = 'info', duration = 5000): void {
|
public static show(message: string, type: NotificationType = 'info', duration = 5000): void {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
|
|
||||||
if (!this.toastContainer) return;
|
if (!this.toastContainer) {return;}
|
||||||
|
|
||||||
// Create toast element
|
// Create toast element
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
|
@ -3,17 +3,19 @@
|
|||||||
* Handles UI-related operations and DOM manipulation
|
* Handles UI-related operations and DOM manipulation
|
||||||
*/
|
*/
|
||||||
import * as nostrTools from 'nostr-tools';
|
import * as nostrTools from 'nostr-tools';
|
||||||
import { NostrEvent } from '../relay';
|
|
||||||
import { HttpService } from './HttpService';
|
import type { NostrEvent } from '../relay';
|
||||||
import { HttpClient } from './HttpClient';
|
|
||||||
import { NostrService } from './NostrService';
|
|
||||||
import { ReceivedEvent } from './NostrEventService';
|
|
||||||
import { RelayStatusManager } from './RelayStatusManager';
|
|
||||||
import { EventListRenderer } from './EventListRenderer';
|
|
||||||
import { EventDetailsRenderer } from './EventDetailsRenderer';
|
import { EventDetailsRenderer } from './EventDetailsRenderer';
|
||||||
import { Nostr21121ResponseHandler } from './Nostr21121ResponseHandler';
|
import { EventListRenderer } from './EventListRenderer';
|
||||||
import { ToastNotifier } from './ToastNotifier';
|
import { HttpClient } from './HttpClient';
|
||||||
import { HttpFormatter } from './HttpFormatter';
|
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
|
* Class for managing UI operations
|
||||||
@ -109,7 +111,7 @@ export class UiService {
|
|||||||
*/
|
*/
|
||||||
public addReceivedEvent(receivedEvent: ReceivedEvent): void {
|
public addReceivedEvent(receivedEvent: ReceivedEvent): void {
|
||||||
const event = receivedEvent.event;
|
const event = receivedEvent.event;
|
||||||
if (!event || !event.id) return;
|
if (!event || !event.id) {return;}
|
||||||
|
|
||||||
// Store the event
|
// Store the event
|
||||||
this.receivedEvents.set(event.id, receivedEvent);
|
this.receivedEvents.set(event.id, receivedEvent);
|
||||||
@ -131,7 +133,7 @@ export class UiService {
|
|||||||
* @param event The event to check relations for
|
* @param event The event to check relations for
|
||||||
*/
|
*/
|
||||||
private checkForRelatedEvents(event: NostrEvent): void {
|
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
|
// Check if this is a response with an e tag referencing a request
|
||||||
if (event.kind === 21121) {
|
if (event.kind === 21121) {
|
||||||
@ -181,7 +183,7 @@ export class UiService {
|
|||||||
|
|
||||||
// Set up event handlers for interactive elements
|
// Set up event handlers for interactive elements
|
||||||
const eventDetailsElement = this.eventDetailsRenderer.getEventDetailsElement();
|
const eventDetailsElement = this.eventDetailsRenderer.getEventDetailsElement();
|
||||||
if (!eventDetailsElement) return;
|
if (!eventDetailsElement) {return;}
|
||||||
|
|
||||||
// Handle "Execute HTTP Request" button
|
// Handle "Execute HTTP Request" button
|
||||||
const executeButtons = eventDetailsElement.querySelectorAll('.execute-http-request-btn');
|
const executeButtons = eventDetailsElement.querySelectorAll('.execute-http-request-btn');
|
||||||
@ -295,7 +297,7 @@ export class UiService {
|
|||||||
const tabId = (button as HTMLElement).dataset.tab;
|
const tabId = (button as HTMLElement).dataset.tab;
|
||||||
if (tabId) {
|
if (tabId) {
|
||||||
const tab = dialog.querySelector(`#${tabId}`);
|
const tab = dialog.querySelector(`#${tabId}`);
|
||||||
if (tab) tab.classList.add('active');
|
if (tab) {tab.classList.add('active');}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,8 +5,11 @@
|
|||||||
|
|
||||||
export interface WebSocketOptions {
|
export interface WebSocketOptions {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
onOpen?: (ws: WebSocket) => void;
|
onOpen?: (ws: WebSocket) => void;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
onMessage?: (parsedData: unknown) => void;
|
onMessage?: (parsedData: unknown) => void;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
onError?: (evt: Event) => void;
|
onError?: (evt: Event) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
@ -47,7 +50,9 @@ export class WebSocketManager {
|
|||||||
// Set up event handlers
|
// Set up event handlers
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
this.ws.onopen = () => {
|
this.ws.onopen = () => {
|
||||||
if (timeoutId) clearTimeout(timeoutId);
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
console.log(`WebSocketManager: Connection to ${url} established successfully`);
|
console.log(`WebSocketManager: Connection to ${url} established successfully`);
|
||||||
if (options.onOpen && this.ws) {
|
if (options.onOpen && this.ws) {
|
||||||
@ -84,7 +89,9 @@ export class WebSocketManager {
|
|||||||
|
|
||||||
this.ws.onerror = (errorEvt) => {
|
this.ws.onerror = (errorEvt) => {
|
||||||
console.error(`WebSocketManager: Error on connection to ${url}:`, errorEvt);
|
console.error(`WebSocketManager: Error on connection to ${url}:`, errorEvt);
|
||||||
if (timeoutId) clearTimeout(timeoutId);
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
if (options.onError) {
|
if (options.onError) {
|
||||||
options.onError(errorEvt);
|
options.onError(errorEvt);
|
||||||
}
|
}
|
||||||
@ -95,7 +102,9 @@ export class WebSocketManager {
|
|||||||
|
|
||||||
this.ws.onclose = (evt) => {
|
this.ws.onclose = (evt) => {
|
||||||
console.log(`WebSocketManager: Connection to ${url} closed. Code: ${evt.code}, Reason: ${evt.reason || 'No reason provided'}, Clean: ${evt.wasClean}`);
|
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;
|
this.connected = false;
|
||||||
|
|
||||||
// Log the closing information more thoroughly
|
// Log the closing information more thoroughly
|
||||||
@ -115,7 +124,9 @@ export class WebSocketManager {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating WebSocket connection:', error);
|
console.error('Error creating WebSocket connection:', error);
|
||||||
if (timeoutId) clearTimeout(timeoutId);
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
this.close();
|
this.close();
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
@ -127,30 +138,30 @@ export class WebSocketManager {
|
|||||||
*/
|
*/
|
||||||
public async testConnection(url: string, timeout = 5000): Promise<boolean> {
|
public async testConnection(url: string, timeout = 5000): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const ws = new WebSocket(url);
|
const testWs = new WebSocket(url);
|
||||||
let connected = false;
|
let connected = false;
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
ws.close();
|
testWs.close();
|
||||||
reject(new Error('Connection timeout'));
|
reject(new Error('Connection timeout'));
|
||||||
}
|
}
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
ws.onopen = () => {
|
testWs.onopen = () => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
connected = true;
|
connected = true;
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (err) => {
|
testWs.onerror = (err) => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
reject(new Error(`WebSocket error: ${err.toString()}`));
|
reject(new Error(`WebSocket error: ${err.toString()}`));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.close();
|
testWs.close();
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
10
client/src/types/window.d.ts
vendored
10
client/src/types/window.d.ts
vendored
@ -43,6 +43,16 @@ declare global {
|
|||||||
// Add global authentication state tracking
|
// Add global authentication state tracking
|
||||||
authInProgress?: boolean;
|
authInProgress?: boolean;
|
||||||
currentSignedEvent?: NostrEvent;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1098,12 +1098,39 @@ footer {
|
|||||||
|
|
||||||
/* Login container */
|
/* Login container */
|
||||||
.login-container {
|
.login-container {
|
||||||
margin-bottom: 20px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-status {
|
#login-status {
|
||||||
margin-top: 5px;
|
font-size: 0.9rem;
|
||||||
font-size: 14px;
|
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 */
|
/* Hidden elements */
|
||||||
@ -2967,4 +2994,34 @@ footer {
|
|||||||
.auth-required-message h3 {
|
.auth-required-message h3 {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
margin-top: 0;
|
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);
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user