diff --git a/api/index.html b/api/index.html index 1463260..1e0095d 100644 --- a/api/index.html +++ b/api/index.html @@ -192,6 +192,92 @@ display: none; } + .section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + border-bottom: 1px solid black; + padding-bottom: 10px; + } + + .auth-rules-controls { + margin-bottom: 15px; + } + + .section-header .status { + margin: 0; + padding: 5px 10px; + min-width: auto; + font-size: 12px; + } + + .section-header .status:before { + content: ''; + } + + /* Auth Rule Input Sections Styling */ + .auth-rule-section { + border: 1px solid black; + padding: 15px; + margin: 15px 0; + background-color: white; + } + + .auth-rule-section h3 { + margin: 0 0 10px 0; + font-size: 14px; + font-weight: bold; + border-left: 4px solid black; + padding-left: 8px; + } + + .auth-rule-section p { + margin: 0 0 15px 0; + font-size: 13px; + color: #666; + } + + .rule-status { + margin-top: 10px; + padding: 8px; + border: 1px solid #ccc; + font-size: 12px; + min-height: 20px; + background-color: #f9f9f9; + } + + .rule-status.success { + border-color: #4CAF50; + background-color: #E8F5E8; + color: #2E7D32; + } + + .rule-status.error { + border-color: #F44336; + background-color: #FFEBEE; + color: #C62828; + } + + .rule-status.warning { + border-color: #FF9800; + background-color: #FFF3E0; + color: #E65100; + } + + .warning-box { + border: 2px solid #FF9800; + background-color: #FFF3E0; + padding: 10px; + margin: 10px 0; + font-size: 13px; + color: #E65100; + } + + .warning-box strong { + color: #D84315; + } + #login-section { text-align: center; padding: 20px; @@ -270,6 +356,96 @@ + + +
@@ -334,17 +510,14 @@
+ - + + + - - - - - - - - + + @@ -420,6 +593,110 @@ } } + // Check for existing authentication state with multiple API methods and retry logic + async function checkExistingAuthWithRetries() { + console.log('Starting authentication state detection with retry logic...'); + + const maxAttempts = 10; + const delay = 500; // ms between attempts + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + console.log(`Authentication detection attempt ${attempt}/${maxAttempts}`); + + try { + // Method 1: Try window.NOSTR_LOGIN_LITE.getAuthState() + if (window.NOSTR_LOGIN_LITE && typeof window.NOSTR_LOGIN_LITE.getAuthState === 'function') { + console.log('Trying window.NOSTR_LOGIN_LITE.getAuthState()...'); + const authState = window.NOSTR_LOGIN_LITE.getAuthState(); + if (authState && authState.pubkey) { + console.log('✅ Auth state found via NOSTR_LOGIN_LITE.getAuthState():', authState.pubkey); + await restoreAuthenticationState(authState.pubkey); + return true; + } + } + + // Method 2: Try nlLite.getPublicKey() + if (nlLite && typeof nlLite.getPublicKey === 'function') { + console.log('Trying nlLite.getPublicKey()...'); + const pubkey = await nlLite.getPublicKey(); + if (pubkey && pubkey.length === 64) { + console.log('✅ Pubkey found via nlLite.getPublicKey():', pubkey); + await restoreAuthenticationState(pubkey); + return true; + } + } + + // Method 3: Try window.nostr.getPublicKey() (NIP-07) + if (window.nostr && typeof window.nostr.getPublicKey === 'function') { + console.log('Trying window.nostr.getPublicKey()...'); + const pubkey = await window.nostr.getPublicKey(); + if (pubkey && pubkey.length === 64) { + console.log('✅ Pubkey found via window.nostr.getPublicKey():', pubkey); + await restoreAuthenticationState(pubkey); + return true; + } + } + + // Method 4: Check localStorage directly for NOSTR_LOGIN_LITE data + const localStorageData = localStorage.getItem('NOSTR_LOGIN_LITE_DATA'); + if (localStorageData) { + try { + const parsedData = JSON.parse(localStorageData); + if (parsedData.pubkey) { + console.log('✅ Pubkey found in localStorage:', parsedData.pubkey); + await restoreAuthenticationState(parsedData.pubkey); + return true; + } + } catch (parseError) { + console.log('Failed to parse localStorage data:', parseError.message); + } + } + + console.log(`❌ Attempt ${attempt}: No authentication found via any method`); + + // Wait before next attempt (except for last attempt) + if (attempt < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + + } catch (error) { + console.log(`❌ Attempt ${attempt} failed:`, error.message); + if (attempt < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + console.log('🔍 Authentication detection completed - no existing auth found after all attempts'); + return false; + } + + // Helper function to restore authentication state + async function restoreAuthenticationState(pubkey) { + console.log('🔄 Restoring authentication state for pubkey:', pubkey); + + userPubkey = pubkey; + isLoggedIn = true; + + // Show main interface + showMainInterface(); + loadUserProfile(); + + // Automatically fetch configuration after restoring login + setTimeout(() => { + fetchConfiguration().catch(error => { + console.log('Auto-fetch configuration failed after restore: ' + error.message); + }); + }, 1000); + + console.log('✅ Authentication state restored successfully'); + } + + // Legacy function for backward compatibility + async function checkExistingAuth() { + return await checkExistingAuthWithRetries(); + } + // Initialize NOSTR_LOGIN_LITE async function initializeApp() { try { @@ -428,6 +705,7 @@ methods: { extension: true, local: true, + seedphrase: true, readonly: true, connect: true, remote: true, @@ -438,8 +716,8 @@ hPosition: 1, // 0.0-1.0 or '95%' from left vPosition: 0, // 0.0-1.0 or '50%' from top appearance: { - style: 'pill', // 'pill', 'square', 'circle', 'minimal' - icon: '', // Clean display without icon placeholders + style: 'square', // 'pill', 'square', 'circle', 'minimal' + // icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET] text: 'Login' }, behavior: { @@ -447,16 +725,24 @@ showUserInfo: true, autoSlide: true }, - getUserInfo: true, // Enable profile fetching - getUserRelay: [ // Specific relays for profile fetching - 'wss://relay.laantungir.net' - ] + animation: { + slideDirection: 'auto' // 'auto', 'left', 'right', 'up', 'down' + } + } }); nlLite = window.NOSTR_LOGIN_LITE; console.log('Nostr login system initialized'); + // Check for existing authentication state after initialization + const wasAlreadyLoggedIn = await checkExistingAuth(); + if (wasAlreadyLoggedIn) { + console.log('User was already logged in, main interface restored'); + } else { + console.log('No existing authentication found, showing login interface'); + } + // Listen for authentication events window.addEventListener('nlMethodSelected', handleAuthEvent); @@ -1189,6 +1475,648 @@ logPanel.innerHTML = '
SYSTEM: Log cleared.
'; }); + // ================================ + // AUTH RULES MANAGEMENT FUNCTIONS + // ================================ + + // Global auth rules state + let currentAuthRules = []; + let editingAuthRule = null; + + // DOM elements for auth rules + const authRulesSection = document.getElementById('authRulesSection'); + const authRulesStatus = document.getElementById('authRulesStatus'); + const viewAuthRulesBtn = document.getElementById('viewAuthRulesBtn'); + const addAuthRuleBtn = document.getElementById('addAuthRuleBtn'); + const refreshAuthRulesBtn = document.getElementById('refreshAuthRulesBtn'); + const authRulesTableContainer = document.getElementById('authRulesTableContainer'); + const authRulesTableBody = document.getElementById('authRulesTableBody'); + const authRuleFormContainer = document.getElementById('authRuleFormContainer'); + const authRuleForm = document.getElementById('authRuleForm'); + const authRuleFormTitle = document.getElementById('authRuleFormTitle'); + const saveAuthRuleBtn = document.getElementById('saveAuthRuleBtn'); + const cancelAuthRuleBtn = document.getElementById('cancelAuthRuleBtn'); + const authRulesStatusDisplay = document.getElementById('authRulesStatusDisplay'); + const authSystemStatus = document.getElementById('authSystemStatus'); + const authRulesCount = document.getElementById('authRulesCount'); + + // Show auth rules section after login + function showAuthRulesSection() { + if (authRulesSection) { + authRulesSection.style.display = 'block'; + updateAuthRulesStatus('ready'); + log('Auth rules section is now available', 'INFO'); + } + } + + // Hide auth rules section on logout + function hideAuthRulesSection() { + if (authRulesSection) { + authRulesSection.style.display = 'none'; + authRulesTableContainer.style.display = 'none'; + authRuleFormContainer.style.display = 'none'; + authRulesStatusDisplay.style.display = 'none'; + currentAuthRules = []; + editingAuthRule = null; + log('Auth rules section hidden', 'INFO'); + } + } + + // Update auth rules status indicator + function updateAuthRulesStatus(status) { + if (!authRulesStatus) return; + + switch (status) { + case 'ready': + authRulesStatus.textContent = 'READY'; + authRulesStatus.className = 'status disconnected'; + break; + case 'loading': + authRulesStatus.textContent = 'LOADING'; + authRulesStatus.className = 'status connected'; + break; + case 'loaded': + authRulesStatus.textContent = 'LOADED'; + authRulesStatus.className = 'status connected'; + break; + case 'error': + authRulesStatus.textContent = 'ERROR'; + authRulesStatus.className = 'status error'; + break; + } + } + + // Load auth rules from relay (placeholder - will be implemented with WebSocket) + async function loadAuthRules() { + try { + log('Loading auth rules...', 'INFO'); + updateAuthRulesStatus('loading'); + + // TODO: Implement actual auth rules loading via WebSocket/HTTP + // For now, show empty state + currentAuthRules = []; + displayAuthRules(currentAuthRules); + updateAuthRulesStatus('loaded'); + + log('Auth rules loaded (placeholder implementation)', 'INFO'); + + } catch (error) { + log(`Failed to load auth rules: ${error.message}`, 'ERROR'); + updateAuthRulesStatus('error'); + } + } + + // Display auth rules in the table + function displayAuthRules(rules) { + if (!authRulesTableBody) return; + + authRulesTableBody.innerHTML = ''; + + if (rules.length === 0) { + const row = document.createElement('tr'); + row.innerHTML = `No auth rules configured`; + authRulesTableBody.appendChild(row); + return; + } + + rules.forEach((rule, index) => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${rule.rule_type} + ${rule.pattern_type || rule.operation || '-'} + ${rule.pattern_value || rule.rule_target || '-'} + ${rule.action || 'allow'} + ${rule.enabled !== false ? 'Active' : 'Inactive'} + +
+ + +
+ + `; + authRulesTableBody.appendChild(row); + }); + + // Update status display + if (authRulesCount) { + const activeRules = rules.filter(r => r.enabled !== false).length; + authRulesCount.textContent = `Total Rules: ${rules.length} (${activeRules} active)`; + } + } + + // Show auth rules table + function showAuthRulesTable() { + if (authRulesTableContainer) { + authRulesTableContainer.style.display = 'block'; + authRulesStatusDisplay.style.display = 'block'; + loadAuthRules(); + log('Auth rules table displayed', 'INFO'); + } + } + + // Show add auth rule form + function showAddAuthRuleForm() { + if (authRuleFormContainer && authRuleFormTitle) { + editingAuthRule = null; + authRuleFormTitle.textContent = 'Add Auth Rule'; + authRuleForm.reset(); + authRuleFormContainer.style.display = 'block'; + log('Opened add auth rule form', 'INFO'); + } + } + + // Show edit auth rule form + function editAuthRule(index) { + if (index < 0 || index >= currentAuthRules.length) return; + + const rule = currentAuthRules[index]; + editingAuthRule = { ...rule, index: index }; + + if (authRuleFormTitle && authRuleForm) { + authRuleFormTitle.textContent = 'Edit Auth Rule'; + + // Populate form fields + document.getElementById('authRuleType').value = rule.rule_type || ''; + document.getElementById('authPatternType').value = rule.pattern_type || rule.operation || ''; + document.getElementById('authPatternValue').value = rule.pattern_value || rule.rule_target || ''; + document.getElementById('authRuleAction').value = rule.action || 'allow'; + document.getElementById('authRuleDescription').value = rule.description || ''; + + authRuleFormContainer.style.display = 'block'; + log(`Editing auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO'); + } + } + + // Delete auth rule + async function deleteAuthRule(index) { + if (index < 0 || index >= currentAuthRules.length) return; + + const rule = currentAuthRules[index]; + const confirmMsg = `Delete auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}?`; + + if (!confirm(confirmMsg)) return; + + try { + log(`Deleting auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO'); + + // TODO: Implement actual rule deletion via WebSocket kind 33335 event + // For now, just remove from local array + currentAuthRules.splice(index, 1); + displayAuthRules(currentAuthRules); + + log('Auth rule deleted (placeholder implementation)', 'INFO'); + + } catch (error) { + log(`Failed to delete auth rule: ${error.message}`, 'ERROR'); + } + } + + // Hide auth rule form + function hideAuthRuleForm() { + if (authRuleFormContainer) { + authRuleFormContainer.style.display = 'none'; + editingAuthRule = null; + log('Auth rule form hidden', 'INFO'); + } + } + + // Validate auth rule form + function validateAuthRuleForm() { + const ruleType = document.getElementById('authRuleType').value; + const patternType = document.getElementById('authPatternType').value; + const patternValue = document.getElementById('authPatternValue').value.trim(); + const action = document.getElementById('authRuleAction').value; + + if (!ruleType) { + alert('Please select a rule type'); + return false; + } + + if (!patternType) { + alert('Please select a pattern type'); + return false; + } + + if (!patternValue) { + alert('Please enter a pattern value'); + return false; + } + + if (!action) { + alert('Please select an action'); + return false; + } + + // Validate pubkey format for pubkey rules + if ((ruleType === 'pubkey_whitelist' || ruleType === 'pubkey_blacklist') && + patternValue.length !== 64) { + alert('Pubkey must be exactly 64 hex characters'); + return false; + } + + // Validate hex format for pubkey rules + if ((ruleType === 'pubkey_whitelist' || ruleType === 'pubkey_blacklist')) { + const hexPattern = /^[0-9a-fA-F]+$/; + if (!hexPattern.test(patternValue)) { + alert('Pubkey must contain only hex characters (0-9, a-f, A-F)'); + return false; + } + } + + return true; + } + + // Save auth rule (add or update) + async function saveAuthRule(event) { + event.preventDefault(); + + if (!validateAuthRuleForm()) return; + + try { + const ruleData = { + rule_type: document.getElementById('authRuleType').value, + pattern_type: document.getElementById('authPatternType').value, + pattern_value: document.getElementById('authPatternValue').value.trim(), + action: document.getElementById('authRuleAction').value, + description: document.getElementById('authRuleDescription').value.trim() || null, + enabled: true + }; + + if (editingAuthRule) { + log(`Updating auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO'); + + // TODO: Implement actual rule update via WebSocket kind 33335 event + // For now, just update local array + currentAuthRules[editingAuthRule.index] = { ...ruleData, id: editingAuthRule.id || Date.now() }; + + log('Auth rule updated (placeholder implementation)', 'INFO'); + } else { + log(`Adding new auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO'); + + // TODO: Implement actual rule creation via WebSocket kind 33335 event + // For now, just add to local array + currentAuthRules.push({ ...ruleData, id: Date.now() }); + + log('Auth rule added (placeholder implementation)', 'INFO'); + } + + displayAuthRules(currentAuthRules); + hideAuthRuleForm(); + + } catch (error) { + log(`Failed to save auth rule: ${error.message}`, 'ERROR'); + } + } + + // Update existing logout and showMainInterface functions to handle auth rules + const originalLogout = logout; + logout = async function() { + hideAuthRulesSection(); + await originalLogout(); + }; + + const originalShowMainInterface = showMainInterface; + showMainInterface = function() { + originalShowMainInterface(); + showAuthRulesSection(); + }; + + // Auth rules event handlers + if (viewAuthRulesBtn) { + viewAuthRulesBtn.addEventListener('click', function(e) { + e.preventDefault(); + showAuthRulesTable(); + }); + } + + if (addAuthRuleBtn) { + addAuthRuleBtn.addEventListener('click', function(e) { + e.preventDefault(); + showAddAuthRuleForm(); + }); + } + + if (refreshAuthRulesBtn) { + refreshAuthRulesBtn.addEventListener('click', function(e) { + e.preventDefault(); + loadAuthRules(); + }); + } + + if (authRuleForm) { + authRuleForm.addEventListener('submit', saveAuthRule); + } + + if (cancelAuthRuleBtn) { + cancelAuthRuleBtn.addEventListener('click', function(e) { + e.preventDefault(); + hideAuthRuleForm(); + }); + } + + // ================================ + // STREAMLINED AUTH RULE FUNCTIONS + // ================================ + + // Utility function to convert nsec to hex pubkey + function nsecToHex(input) { + if (!input || input.trim().length === 0) { + return null; + } + + const trimmed = input.trim(); + + // If it's already 64-char hex, return as-is + if (/^[0-9a-fA-F]{64}$/.test(trimmed)) { + return trimmed; + } + + // If it starts with nsec1, try to decode + if (trimmed.startsWith('nsec1')) { + try { + if (window.NostrTools && window.NostrTools.nip19 && window.NostrTools.nip19.decode) { + const decoded = window.NostrTools.nip19.decode(trimmed); + if (decoded.type === 'nsec') { + // Convert bytes to hex + const hexPubkey = Array.from(decoded.data) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + return hexPubkey; + } + } + } catch (error) { + console.error('Failed to decode nsec:', error); + return null; + } + } + + return null; // Invalid format + } + + // Add blacklist rule + function addBlacklistRule() { + const input = document.getElementById('blacklistPubkey'); + const statusDiv = document.getElementById('blacklistStatus'); + + if (!input || !statusDiv) return; + + const inputValue = input.value.trim(); + if (!inputValue) { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = 'Please enter a pubkey or nsec'; + return; + } + + // Convert nsec to hex if needed + const hexPubkey = nsecToHex(inputValue); + if (!hexPubkey) { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = 'Invalid pubkey format. Please enter nsec1... or 64-character hex'; + return; + } + + // Validate hex length + if (hexPubkey.length !== 64) { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = 'Invalid pubkey length. Must be exactly 64 characters'; + return; + } + + statusDiv.className = 'rule-status'; + statusDiv.textContent = 'Adding to blacklist...'; + + // Create auth rule data + const ruleData = { + rule_type: 'pubkey_blacklist', + pattern_type: 'Global', + pattern_value: hexPubkey, + action: 'deny' + }; + + // Add to WebSocket queue for processing + addAuthRuleViaWebSocket(ruleData) + .then(() => { + statusDiv.className = 'rule-status success'; + statusDiv.textContent = `Pubkey ${hexPubkey.substring(0, 16)}... added to blacklist`; + input.value = ''; + + // Refresh auth rules display if visible + if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { + loadAuthRules(); + } + }) + .catch(error => { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = `Failed to add rule: ${error.message}`; + }); + } + + // Add whitelist rule + function addWhitelistRule() { + const input = document.getElementById('whitelistPubkey'); + const statusDiv = document.getElementById('whitelistStatus'); + const warningDiv = document.getElementById('whitelistWarning'); + + if (!input || !statusDiv) return; + + const inputValue = input.value.trim(); + if (!inputValue) { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = 'Please enter a pubkey or nsec'; + return; + } + + // Convert nsec to hex if needed + const hexPubkey = nsecToHex(inputValue); + if (!hexPubkey) { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = 'Invalid pubkey format. Please enter nsec1... or 64-character hex'; + return; + } + + // Validate hex length + if (hexPubkey.length !== 64) { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = 'Invalid pubkey length. Must be exactly 64 characters'; + return; + } + + // Show warning about whitelist-only mode + if (warningDiv) { + warningDiv.style.display = 'block'; + } + + statusDiv.className = 'rule-status warning'; + statusDiv.textContent = 'Adding to whitelist (will enable whitelist-only mode)...'; + + // Create auth rule data + const ruleData = { + rule_type: 'pubkey_whitelist', + pattern_type: 'Global', + pattern_value: hexPubkey, + action: 'allow' + }; + + // Add to WebSocket queue for processing + addAuthRuleViaWebSocket(ruleData) + .then(() => { + statusDiv.className = 'rule-status success'; + statusDiv.textContent = `Pubkey ${hexPubkey.substring(0, 16)}... added to whitelist`; + input.value = ''; + + // Refresh auth rules display if visible + if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { + loadAuthRules(); + } + }) + .catch(error => { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = `Failed to add rule: ${error.message}`; + }); + } + + // Add hash blacklist rule + function addHashBlacklistRule() { + const input = document.getElementById('hashBlacklist'); + const statusDiv = document.getElementById('hashStatus'); + + if (!input || !statusDiv) return; + + const inputValue = input.value.trim(); + if (!inputValue) { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = 'Please enter a SHA-256 hash'; + return; + } + + // Validate hash format (64-char hex) + if (!/^[0-9a-fA-F]{64}$/.test(inputValue)) { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = 'Invalid hash format. Must be 64-character hex SHA-256 hash'; + return; + } + + statusDiv.className = 'rule-status'; + statusDiv.textContent = 'Adding content hash to blacklist...'; + + // Create auth rule data + const ruleData = { + rule_type: 'hash_blacklist', + pattern_type: 'Global', + pattern_value: inputValue.toLowerCase(), // Normalize to lowercase + action: 'deny' + }; + + // Add to WebSocket queue for processing + addAuthRuleViaWebSocket(ruleData) + .then(() => { + statusDiv.className = 'rule-status success'; + statusDiv.textContent = `Content hash ${inputValue.substring(0, 16)}... added to blacklist`; + input.value = ''; + + // Refresh auth rules display if visible + if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { + loadAuthRules(); + } + }) + .catch(error => { + statusDiv.className = 'rule-status error'; + statusDiv.textContent = `Failed to add rule: ${error.message}`; + }); + } + + // Add auth rule via WebSocket (kind 33335 event) + async function addAuthRuleViaWebSocket(ruleData) { + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to add auth rules'); + } + + if (!configWebSocket || configWebSocket.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket connection not available'); + } + + try { + log(`Adding auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value.substring(0, 16)}...`, 'INFO'); + + // Create kind 33335 auth rule event + const authEvent = { + kind: 33335, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['d', 'auth-rules'], // Addressable event identifier + [ruleData.rule_type, ruleData.pattern_type, ruleData.pattern_value] + ], + content: JSON.stringify({ + action: 'add', + rule_type: ruleData.rule_type, + pattern_type: ruleData.pattern_type, + pattern_value: ruleData.pattern_value, + rule_action: ruleData.action + }) + }; + + // DEBUG: Log the complete event structure being sent + console.log('=== AUTH RULE EVENT DEBUG ==='); + console.log('Rule Data:', ruleData); + console.log('Auth Event (before signing):', JSON.stringify(authEvent, null, 2)); + console.log('Auth Event Tags:', authEvent.tags); + console.log('Auth Event Content:', authEvent.content); + console.log('=== END AUTH RULE EVENT DEBUG ==='); + + // Sign the event + const signedEvent = await window.nostr.signEvent(authEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + // Send EVENT message via existing WebSocket + return new Promise((resolve, reject) => { + const eventMessage = ["EVENT", signedEvent]; + + // Set up temporary message handler for OK response + const originalOnMessage = configWebSocket.onmessage; + let timeoutId = setTimeout(() => { + configWebSocket.onmessage = originalOnMessage; + reject(new Error('Timeout waiting for auth rule response')); + }, 10000); + + configWebSocket.onmessage = function(event) { + try { + const message = JSON.parse(event.data); + if (Array.isArray(message) && message[0] === "OK" && message[1] === signedEvent.id) { + clearTimeout(timeoutId); + configWebSocket.onmessage = originalOnMessage; + + if (message[2]) { + resolve(); + log('Auth rule added successfully', 'INFO'); + } else { + reject(new Error(message[3] || 'Auth rule rejected by relay')); + } + return; + } + } catch (parseError) { + // Ignore parse errors, pass to original handler + } + + // Call original handler for other messages + if (originalOnMessage) { + originalOnMessage.call(this, event); + } + }; + + // Send the event + configWebSocket.send(JSON.stringify(eventMessage)); + log('Auth rule event sent via WebSocket', 'INFO'); + }); + + } catch (error) { + log(`Failed to add auth rule: ${error.message}`, 'ERROR'); + throw error; + } + } + // Initialize the app document.addEventListener('DOMContentLoaded', () => { console.log('C-Relay Admin API interface loaded'); diff --git a/relay.pid b/relay.pid index 1ccf48a..2dadd49 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -295261 +1307796 diff --git a/src/config.c b/src/config.c index 8d385d5..c55c253 100644 --- a/src/config.c +++ b/src/config.c @@ -2156,15 +2156,28 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s // Handle kind 33335 auth rule events int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size) { + log_info("=== SERVER-SIDE AUTH RULE EVENT DEBUG ==="); + + // Print the entire received event for debugging + char* debug_event_str = cJSON_Print(event); + if (debug_event_str) { + printf("Received Auth Event JSON: %s\n", debug_event_str); + free(debug_event_str); + } + cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (!tags_obj || !cJSON_IsArray(tags_obj)) { + log_error("Auth event missing or invalid tags array"); snprintf(error_message, error_size, "invalid: auth rule event must have tags"); return -1; } + printf("Tags array size: %d\n", cJSON_GetArraySize(tags_obj)); + // Extract action from content or tags cJSON* content_obj = cJSON_GetObjectItem(event, "content"); const char* content = content_obj ? cJSON_GetStringValue(content_obj) : ""; + printf("Event content: '%s'\n", content); // Parse the action from content (should be "add" or "remove") cJSON* content_json = cJSON_Parse(content); @@ -2176,6 +2189,7 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz } cJSON_Delete(content_json); } + printf("Parsed action: '%s'\n", action); // Begin transaction for atomic auth rule updates int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); @@ -2185,11 +2199,33 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz } int rules_processed = 0; + int tags_examined = 0; + int tags_skipped = 0; // Process each tag as an auth rule specification cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags_obj) { - if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 3) { + tags_examined++; + + printf("Examining tag #%d:\n", tags_examined); + char* tag_debug_str = cJSON_Print(tag); + if (tag_debug_str) { + printf(" Tag JSON: %s\n", tag_debug_str); + free(tag_debug_str); + } + + if (!cJSON_IsArray(tag)) { + printf(" SKIPPED: Not an array\n"); + tags_skipped++; + continue; + } + + int tag_size = cJSON_GetArraySize(tag); + printf(" Tag array size: %d\n", tag_size); + + if (tag_size < 3) { + printf(" SKIPPED: Array size < 3 (need at least 3 elements for auth rules)\n"); + tags_skipped++; continue; } @@ -2200,6 +2236,8 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz if (!cJSON_IsString(rule_type_obj) || !cJSON_IsString(pattern_type_obj) || !cJSON_IsString(pattern_value_obj)) { + printf(" SKIPPED: One or more elements are not strings\n"); + tags_skipped++; continue; } @@ -2207,18 +2245,42 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz const char* pattern_type = cJSON_GetStringValue(pattern_type_obj); const char* pattern_value = cJSON_GetStringValue(pattern_value_obj); + printf(" Extracted rule: type='%s', pattern_type='%s', pattern_value='%s'\n", + rule_type, pattern_type, pattern_value); + + // Map rule_type to correct action (FIX THE BUG HERE) + const char* mapped_action = "allow"; // default + if (strcmp(rule_type, "pubkey_blacklist") == 0 || strcmp(rule_type, "hash_blacklist") == 0) { + mapped_action = "deny"; + } else if (strcmp(rule_type, "pubkey_whitelist") == 0) { + mapped_action = "allow"; + } + printf(" Mapped action for rule_type '%s': '%s'\n", rule_type, mapped_action); + // Process the auth rule based on action if (strcmp(action, "add") == 0) { - if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) { + printf(" Attempting to add rule to database...\n"); + if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, mapped_action) == 0) { + printf(" SUCCESS: Rule added to database\n"); rules_processed++; + } else { + printf(" FAILED: Could not add rule to database\n"); } } else if (strcmp(action, "remove") == 0) { + printf(" Attempting to remove rule from database...\n"); if (remove_auth_rule_from_config(rule_type, pattern_type, pattern_value) == 0) { + printf(" SUCCESS: Rule removed from database\n"); rules_processed++; + } else { + printf(" FAILED: Could not remove rule from database\n"); } } } + printf("Processing summary: examined=%d, skipped=%d, processed=%d\n", + tags_examined, tags_skipped, rules_processed); + log_info("=== END SERVER-SIDE AUTH RULE EVENT DEBUG ==="); + if (rules_processed > 0) { sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);