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