2151 lines
84 KiB
HTML
2151 lines
84 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>C-Relay Admin API</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Courier New', monospace;
|
|
background-color: white;
|
|
color: black;
|
|
line-height: 1.4;
|
|
padding: 20px;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
border-bottom: 2px solid black;
|
|
padding-bottom: 10px;
|
|
margin-bottom: 30px;
|
|
font-weight: normal;
|
|
font-size: 24px;
|
|
}
|
|
|
|
h2 {
|
|
margin: 30px 0 15px 0;
|
|
font-weight: normal;
|
|
border-left: 4px solid black;
|
|
padding-left: 10px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.section {
|
|
border: 1px solid black;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.input-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
|
|
input,
|
|
textarea,
|
|
select,
|
|
button {
|
|
width: 100%;
|
|
padding: 8px;
|
|
border: 1px solid black;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 14px;
|
|
background-color: white;
|
|
color: black;
|
|
}
|
|
|
|
button {
|
|
background-color: black;
|
|
color: white;
|
|
cursor: pointer;
|
|
margin: 5px 0;
|
|
font-weight: bold;
|
|
}
|
|
|
|
button:hover {
|
|
background-color: #333;
|
|
}
|
|
|
|
button:disabled {
|
|
background-color: #ccc;
|
|
color: #666;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.status {
|
|
padding: 10px;
|
|
margin: 10px 0;
|
|
border: 1px solid black;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.status.connected {
|
|
background-color: black;
|
|
color: white;
|
|
}
|
|
|
|
.status.disconnected {
|
|
background-color: white;
|
|
color: black;
|
|
}
|
|
|
|
.status.authenticated {
|
|
background-color: black;
|
|
color: white;
|
|
}
|
|
|
|
.status.error {
|
|
background-color: white;
|
|
color: black;
|
|
border: 2px solid black;
|
|
}
|
|
|
|
.config-table {
|
|
border: 1px solid black;
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.config-table th,
|
|
.config-table td {
|
|
border: 1px solid black;
|
|
padding: 8px;
|
|
text-align: left;
|
|
}
|
|
|
|
.config-table th {
|
|
background-color: black;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.json-display {
|
|
background-color: white;
|
|
border: 1px solid black;
|
|
padding: 10px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.log-panel {
|
|
height: 200px;
|
|
overflow-y: auto;
|
|
border: 1px solid black;
|
|
padding: 10px;
|
|
font-size: 12px;
|
|
background-color: white;
|
|
}
|
|
|
|
.log-entry {
|
|
margin-bottom: 5px;
|
|
border-bottom: 1px solid #ccc;
|
|
padding-bottom: 5px;
|
|
}
|
|
|
|
.log-timestamp {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.inline-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.inline-buttons button {
|
|
flex: 1;
|
|
}
|
|
|
|
.user-info {
|
|
padding: 10px;
|
|
border: 1px solid black;
|
|
margin: 10px 0;
|
|
background-color: white;
|
|
}
|
|
|
|
.user-pubkey {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
word-break: break-all;
|
|
margin: 5px 0;
|
|
}
|
|
|
|
.hidden {
|
|
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;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
body {
|
|
padding: 10px;
|
|
}
|
|
|
|
.inline-buttons {
|
|
flex-direction: column;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 20px;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<h1>C-RELAY ADMIN API</h1>
|
|
|
|
<!-- Testing Section - Always Visible -->
|
|
<div class="section">
|
|
<h2>DEBUG - TEST FETCH WITHOUT LOGIN</h2>
|
|
<div class="input-group">
|
|
<label for="relay-url">Relay URL:</label>
|
|
<input type="text" id="relay-url" value="ws://localhost:8888" placeholder="ws://localhost:8888">
|
|
</div>
|
|
<div class="status disconnected" id="relay-status">READY TO FETCH</div>
|
|
<button type="button" id="fetch-config-btn">FETCH CONFIGURATION (NO LOGIN)</button>
|
|
<div class="status disconnected" id="config-status">NO CONFIGURATION LOADED</div>
|
|
|
|
<div id="config-display" class="hidden">
|
|
<div id="config-view-mode">
|
|
<table class="config-table" id="config-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Parameter</th>
|
|
<th>Value</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="config-table-body">
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="inline-buttons">
|
|
<button type="button" id="edit-config-btn">EDIT CONFIGURATION</button>
|
|
<button type="button" id="copy-config-btn">COPY CONFIGURATION</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="config-edit-mode" class="hidden">
|
|
<h3>Edit Configuration</h3>
|
|
<div id="config-form" class="section">
|
|
<!-- Dynamic form will be generated here -->
|
|
</div>
|
|
|
|
<div class="inline-buttons">
|
|
<button type="button" id="save-config-btn">SAVE & PUBLISH</button>
|
|
<button type="button" id="cancel-edit-btn">CANCEL</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="config-raw-display">
|
|
<h3>Raw Event JSON:</h3>
|
|
<div class="json-display" id="raw-config-json"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Auth Rules Management -->
|
|
<div class="section" id="authRulesSection" style="display: none;">
|
|
<div class="section-header">
|
|
<h2>AUTH RULES MANAGEMENT</h2>
|
|
<div class="status" id="authRulesStatus">●</div>
|
|
</div>
|
|
|
|
<div class="auth-rules-controls">
|
|
<div class="inline-buttons">
|
|
<button id="viewAuthRulesBtn" class="btn">VIEW RULES</button>
|
|
<button id="refreshAuthRulesBtn" class="btn">REFRESH</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Auth Rules Table -->
|
|
<div id="authRulesTableContainer" style="display: none;">
|
|
<table class="config-table" id="authRulesTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Rule Type</th>
|
|
<th>Pattern Type</th>
|
|
<th>Pattern Value</th>
|
|
<th>Action</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="authRulesTableBody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Streamlined Auth Rule Input Sections -->
|
|
<div id="authRuleInputSections" style="display: block;">
|
|
|
|
<!-- Blacklist Section -->
|
|
<div class="auth-rule-section">
|
|
<h3>BLACKLIST PUBKEY</h3>
|
|
<p>Block a specific user from all operations</p>
|
|
<div class="input-group">
|
|
<label for="blacklistPubkey">Pubkey (nsec or hex):</label>
|
|
<input type="text" id="blacklistPubkey" placeholder="nsec1... or 64-character hex pubkey">
|
|
<small id="blacklistHelp">Enter nsec (will auto-convert) or 64-character hex pubkey</small>
|
|
</div>
|
|
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO BLACKLIST</button>
|
|
<div id="blacklistStatus" class="rule-status"></div>
|
|
</div>
|
|
|
|
<!-- Whitelist Section -->
|
|
<div class="auth-rule-section">
|
|
<h3>WHITELIST PUBKEY</h3>
|
|
<p>Allow only specific users (converts relay to whitelist-only mode)</p>
|
|
<div class="input-group">
|
|
<label for="whitelistPubkey">Pubkey (nsec or hex):</label>
|
|
<input type="text" id="whitelistPubkey" placeholder="nsec1... or 64-character hex pubkey">
|
|
<small id="whitelistHelp">Enter nsec (will auto-convert) or 64-character hex pubkey</small>
|
|
</div>
|
|
<div id="whitelistWarning" class="warning-box" style="display: none;">
|
|
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only mode.
|
|
Only whitelisted users will be able to interact with the relay.
|
|
</div>
|
|
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO WHITELIST</button>
|
|
<div id="whitelistStatus" class="rule-status"></div>
|
|
</div>
|
|
|
|
<!-- Hash Blacklist Section -->
|
|
<div class="auth-rule-section">
|
|
<h3>BLACKLIST CONTENT HASH</h3>
|
|
<p>Block specific content by SHA-256 hash</p>
|
|
<div class="input-group">
|
|
<label for="hashBlacklist">Content Hash (SHA-256):</label>
|
|
<input type="text" id="hashBlacklist" placeholder="64-character hex SHA-256 hash">
|
|
<small id="hashHelp">Enter 64-character hex SHA-256 hash of content to block</small>
|
|
</div>
|
|
<button type="button" id="addHashBlacklistBtn" onclick="addHashBlacklistRule()">BLOCK CONTENT HASH</button>
|
|
<div id="hashStatus" class="rule-status"></div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Auth Rules Status Display -->
|
|
<div id="authRulesStatusDisplay" style="display: none;">
|
|
<h3>Auth System Status</h3>
|
|
<div class="status" id="authSystemStatus">CHECKING...</div>
|
|
<div class="json-display" id="authRulesCount">
|
|
Rules: Loading...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Login Section -->
|
|
<div id="login-section">
|
|
<div class="section">
|
|
<h2>NOSTR AUTHENTICATION</h2>
|
|
<p>Please login with your Nostr identity to access the admin interface.</p>
|
|
<!-- nostr-lite login UI will be injected here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Interface (hidden until logged in) -->
|
|
<div id="main-interface" class="hidden">
|
|
|
|
<!-- User Info Section -->
|
|
<div class="section">
|
|
<h2>LOGGED IN USER</h2>
|
|
<div class="user-info">
|
|
<div><strong>Name:</strong> <span id="user-name">Loading...</span></div>
|
|
<div><strong>Public Key:</strong>
|
|
<div class="user-pubkey" id="user-pubkey">Loading...</div>
|
|
</div>
|
|
<div><strong>About:</strong> <span id="user-about">Loading...</span></div>
|
|
</div>
|
|
<button type="button" id="logout-btn">LOGOUT</button>
|
|
</div>
|
|
|
|
<!-- Command Section -->
|
|
<div class="section">
|
|
<h2>ADMIN COMMANDS</h2>
|
|
<div class="input-group">
|
|
<label for="command-type">Command Type:</label>
|
|
<select id="command-type">
|
|
<option value="">Select command type...</option>
|
|
<option value="update_config">Update Configuration</option>
|
|
<option value="restart_relay">Restart Relay</option>
|
|
<option value="get_status">Get Status</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<label for="command-payload">Command Payload (JSON):</label>
|
|
<textarea id="command-payload" rows="4" placeholder='{"param": "value"}'></textarea>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<label>Event Preview:</label>
|
|
<div class="json-display" id="event-preview">No event constructed</div>
|
|
</div>
|
|
|
|
<button type="button" id="send-command-btn">SEND COMMAND</button>
|
|
</div>
|
|
|
|
<!-- Log Section -->
|
|
<div class="section">
|
|
<h2>EVENT LOG</h2>
|
|
<div class="log-panel" id="log-panel">
|
|
<div class="log-entry">
|
|
<span class="log-timestamp">SYSTEM:</span> Interface loaded. Login and connect to relay to begin.
|
|
</div>
|
|
</div>
|
|
<button type="button" id="clear-log-btn">CLEAR LOG</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<!-- Load the official nostr-tools bundle first -->
|
|
<!-- <script src="./nostr.bundle.js"></script> -->
|
|
<script src="https://laantungir.net/nostr-login-lite/nostr.bundle.js"></script>
|
|
|
|
<!-- Load NOSTR_LOGIN_LITE main library -->
|
|
<script src="https://laantungir.net/nostr-login-lite/nostr-lite.js"></script>
|
|
<!-- <script src="./nostr-lite.js"></script> -->
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
// Global error handler to prevent page refreshes
|
|
window.addEventListener('error', function (e) {
|
|
console.error('Global error caught:', e.error);
|
|
console.error('Error message:', e.message);
|
|
console.error('Error filename:', e.filename);
|
|
console.error('Error line:', e.lineno);
|
|
e.preventDefault(); // Prevent default browser error handling
|
|
return true; // Prevent page refresh
|
|
});
|
|
|
|
window.addEventListener('unhandledrejection', function (e) {
|
|
console.error('Unhandled promise rejection:', e.reason);
|
|
e.preventDefault(); // Prevent default browser error handling
|
|
return true; // Prevent page refresh
|
|
});
|
|
|
|
// Global state
|
|
let nlLite = null;
|
|
let userPubkey = null;
|
|
let isLoggedIn = false;
|
|
let currentConfig = null;
|
|
let relayPool = null;
|
|
|
|
// DOM elements
|
|
const loginSection = document.getElementById('login-section');
|
|
const mainInterface = document.getElementById('main-interface');
|
|
const userName = document.getElementById('user-name');
|
|
const userPubkeyDisplay = document.getElementById('user-pubkey');
|
|
const userAbout = document.getElementById('user-about');
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|
const relayUrl = document.getElementById('relay-url');
|
|
const relayStatus = document.getElementById('relay-status');
|
|
const fetchConfigBtn = document.getElementById('fetch-config-btn');
|
|
const configStatus = document.getElementById('config-status');
|
|
const configDisplay = document.getElementById('config-display');
|
|
const configViewMode = document.getElementById('config-view-mode');
|
|
const configEditMode = document.getElementById('config-edit-mode');
|
|
const configTableBody = document.getElementById('config-table-body');
|
|
const configForm = document.getElementById('config-form');
|
|
const rawConfigJson = document.getElementById('raw-config-json');
|
|
const copyConfigBtn = document.getElementById('copy-config-btn');
|
|
const editConfigBtn = document.getElementById('edit-config-btn');
|
|
const saveConfigBtn = document.getElementById('save-config-btn');
|
|
const cancelEditBtn = document.getElementById('cancel-edit-btn');
|
|
const commandType = document.getElementById('command-type');
|
|
const commandPayload = document.getElementById('command-payload');
|
|
const eventPreview = document.getElementById('event-preview');
|
|
const sendCommandBtn = document.getElementById('send-command-btn');
|
|
const logPanel = document.getElementById('log-panel');
|
|
const clearLogBtn = document.getElementById('clear-log-btn');
|
|
|
|
// Utility functions
|
|
function log(message, type = 'INFO') {
|
|
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
|
|
const logMessage = `${timestamp} [${type}]: ${message}`;
|
|
|
|
// Always log to browser console so we don't lose logs on refresh
|
|
console.log(logMessage);
|
|
|
|
// Also log to UI if elements exist
|
|
if (logPanel) {
|
|
const logEntry = document.createElement('div');
|
|
logEntry.className = 'log-entry';
|
|
logEntry.innerHTML = `<span class="log-timestamp">${timestamp} [${type}]:</span> ${message}`;
|
|
logPanel.appendChild(logEntry);
|
|
logPanel.scrollTop = logPanel.scrollHeight;
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
await window.NOSTR_LOGIN_LITE.init({
|
|
theme: 'default',
|
|
methods: {
|
|
extension: true,
|
|
local: true,
|
|
seedphrase: true,
|
|
readonly: true,
|
|
connect: true,
|
|
remote: true,
|
|
otp: true
|
|
},
|
|
floatingTab: {
|
|
enabled: true,
|
|
hPosition: 1, // 0.0-1.0 or '95%' from left
|
|
vPosition: 0, // 0.0-1.0 or '50%' from top
|
|
appearance: {
|
|
style: 'square', // 'pill', 'square', 'circle', 'minimal'
|
|
// icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
|
|
text: 'Login'
|
|
},
|
|
behavior: {
|
|
hideWhenAuthenticated: false,
|
|
showUserInfo: true,
|
|
autoSlide: true
|
|
},
|
|
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);
|
|
|
|
} catch (error) {
|
|
console.log('Failed to initialize Nostr login: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Handle authentication events
|
|
function handleAuthEvent(event) {
|
|
const { pubkey, method, error } = event.detail;
|
|
|
|
if (method && pubkey) {
|
|
userPubkey = pubkey;
|
|
isLoggedIn = true;
|
|
console.log(`Login successful! Method: ${method}`);
|
|
console.log(`Public key: ${pubkey}`);
|
|
|
|
showMainInterface();
|
|
loadUserProfile();
|
|
|
|
// Automatically fetch configuration after login
|
|
setTimeout(() => {
|
|
fetchConfiguration().catch(error => {
|
|
console.log('Auto-fetch configuration failed: ' + error.message);
|
|
});
|
|
}, 1000);
|
|
|
|
console.log('Login successful. Auto-fetching configuration...');
|
|
|
|
} else if (error) {
|
|
console.log(`Authentication error: ${error}`);
|
|
}
|
|
}
|
|
|
|
// Show main interface after login
|
|
function showMainInterface() {
|
|
loginSection.classList.add('hidden');
|
|
mainInterface.classList.remove('hidden');
|
|
userPubkeyDisplay.textContent = userPubkey;
|
|
}
|
|
|
|
// Load user profile using nostr-tools pool
|
|
async function loadUserProfile() {
|
|
if (!userPubkey) return;
|
|
|
|
console.log('Loading user profile...');
|
|
userName.textContent = 'Loading...';
|
|
userAbout.textContent = 'Loading...';
|
|
|
|
try {
|
|
// Create a SimplePool instance
|
|
relayPool = new window.NostrTools.SimplePool();
|
|
const relays = ['wss://relay.laantungir.net'];
|
|
|
|
// Get profile event (kind 0) for the user
|
|
const events = await relayPool.querySync(relays, {
|
|
kinds: [0],
|
|
authors: [userPubkey],
|
|
limit: 1
|
|
});
|
|
|
|
if (events.length > 0) {
|
|
const profile = JSON.parse(events[0].content);
|
|
displayProfile(profile);
|
|
} else {
|
|
userName.textContent = 'Anonymous User';
|
|
userAbout.textContent = 'No profile found';
|
|
}
|
|
|
|
} catch (error) {
|
|
console.log('Profile loading failed: ' + error.message);
|
|
userName.textContent = 'Error loading profile';
|
|
userAbout.textContent = error.message;
|
|
}
|
|
}
|
|
|
|
// Display profile data
|
|
function displayProfile(profile) {
|
|
const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User';
|
|
const about = profile.about || 'No description provided';
|
|
|
|
userName.textContent = name;
|
|
userAbout.textContent = about;
|
|
|
|
console.log(`Profile loaded for: ${name}`);
|
|
}
|
|
|
|
// Logout function
|
|
async function logout() {
|
|
console.log('Logging out...');
|
|
try {
|
|
// Clean up profile relay pool
|
|
if (relayPool) {
|
|
const relays = ['wss://relay.laantungir.net'];
|
|
relayPool = null;
|
|
}
|
|
|
|
// Clean up configuration WebSocket
|
|
if (configWebSocket) {
|
|
console.log('Closing configuration WebSocket...');
|
|
configWebSocket.close();
|
|
configWebSocket = null;
|
|
subscriptionId = null;
|
|
}
|
|
|
|
await nlLite.logout();
|
|
|
|
userPubkey = null;
|
|
isLoggedIn = false;
|
|
currentConfig = null;
|
|
|
|
// Reset UI
|
|
mainInterface.classList.add('hidden');
|
|
loginSection.classList.remove('hidden');
|
|
updateConfigStatus(false);
|
|
relayStatus.textContent = 'READY TO FETCH';
|
|
relayStatus.className = 'status disconnected';
|
|
|
|
console.log('Logged out successfully');
|
|
|
|
} catch (error) {
|
|
console.log('Logout failed: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function updateConfigStatus(loaded) {
|
|
if (loaded) {
|
|
configStatus.textContent = 'CONFIGURATION LOADED';
|
|
configStatus.className = 'status connected';
|
|
configDisplay.classList.remove('hidden');
|
|
} else {
|
|
configStatus.textContent = 'NO CONFIGURATION LOADED';
|
|
configStatus.className = 'status disconnected';
|
|
configDisplay.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// Global subscription state
|
|
let configWebSocket = null;
|
|
let subscriptionId = null;
|
|
|
|
// Generate random subscription ID
|
|
function generateSubId() {
|
|
return Math.random().toString(36).substring(2, 15);
|
|
}
|
|
|
|
// Configuration subscription using direct WebSocket connection
|
|
async function subscribeToConfiguration() {
|
|
try {
|
|
console.log('=== STARTING DIRECT WEBSOCKET CONFIGURATION SUBSCRIPTION ===');
|
|
|
|
if (!isLoggedIn) {
|
|
console.log('WARNING: Not logged in, but proceeding with subscription test');
|
|
}
|
|
|
|
const url = relayUrl.value.trim();
|
|
if (!url) {
|
|
console.error('Please enter a relay URL');
|
|
return false;
|
|
}
|
|
|
|
console.log(`Connecting to relay via WebSocket: ${url}`);
|
|
relayStatus.textContent = 'CONNECTING...';
|
|
relayStatus.className = 'status connected';
|
|
|
|
// Clean up existing connection
|
|
if (configWebSocket) {
|
|
console.log('Closing existing WebSocket connection');
|
|
configWebSocket.close();
|
|
configWebSocket = null;
|
|
}
|
|
|
|
// Try different URL formats if the first one fails
|
|
const urlsToTry = [url];
|
|
if (url.includes('127.0.0.1')) {
|
|
urlsToTry.push(url.replace('127.0.0.1', 'localhost'));
|
|
} else if (url.includes('localhost')) {
|
|
urlsToTry.push(url.replace('localhost', '127.0.0.1'));
|
|
}
|
|
|
|
console.log('URLs to try:', urlsToTry);
|
|
|
|
// Create WebSocket connection
|
|
configWebSocket = new WebSocket(urlsToTry[0]);
|
|
subscriptionId = generateSubId();
|
|
|
|
console.log(`Generated subscription ID: ${subscriptionId}`);
|
|
console.log('WebSocket readyState after creation:', configWebSocket.readyState);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let timeoutId = setTimeout(() => {
|
|
console.error('WebSocket connection timeout');
|
|
relayStatus.textContent = 'CONNECTION TIMEOUT';
|
|
relayStatus.className = 'status error';
|
|
if (configWebSocket) {
|
|
configWebSocket.close();
|
|
}
|
|
reject(new Error('Connection timeout'));
|
|
}, 10000); // 10 second timeout
|
|
|
|
configWebSocket.onopen = function (event) {
|
|
console.log('WebSocket connection established');
|
|
clearTimeout(timeoutId);
|
|
|
|
relayStatus.textContent = 'CONNECTED - SUBSCRIBING...';
|
|
relayStatus.className = 'status connected';
|
|
|
|
// Send REQ message to subscribe to kind 23455 events (ephemeral config events)
|
|
const reqMessage = [
|
|
"REQ",
|
|
subscriptionId,
|
|
{
|
|
"kinds": [23455],
|
|
"limit": 50
|
|
}
|
|
];
|
|
|
|
console.log('Sending REQ message:', JSON.stringify(reqMessage));
|
|
configWebSocket.send(JSON.stringify(reqMessage));
|
|
|
|
// Set up another timeout for receiving EOSE
|
|
timeoutId = setTimeout(() => {
|
|
console.log('No EOSE received within timeout, but connection is active');
|
|
relayStatus.textContent = 'SUBSCRIBED - WAITING FOR EVENTS';
|
|
relayStatus.className = 'status connected';
|
|
}, 5000);
|
|
|
|
resolve(true);
|
|
};
|
|
|
|
configWebSocket.onmessage = function (event) {
|
|
try {
|
|
const message = JSON.parse(event.data);
|
|
console.log('Received message:', message);
|
|
|
|
if (Array.isArray(message)) {
|
|
const [messageType, subId, eventData] = message;
|
|
|
|
if (messageType === "EVENT" && subId === subscriptionId) {
|
|
console.log('=== CONFIGURATION EVENT RECEIVED VIA WEBSOCKET ===');
|
|
console.log('Event data:', eventData);
|
|
console.log('Event kind:', eventData.kind);
|
|
console.log('Event tags:', eventData.tags);
|
|
console.log('Event pubkey:', eventData.pubkey);
|
|
console.log('=== END EVENT UPDATE ===');
|
|
|
|
// Display the configuration event
|
|
displayConfiguration(eventData);
|
|
|
|
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
|
|
relayStatus.className = 'status connected';
|
|
|
|
} else if (messageType === "EOSE" && subId === subscriptionId) {
|
|
clearTimeout(timeoutId);
|
|
console.log('EOSE received - End of stored events');
|
|
console.log('Current config after EOSE:', currentConfig);
|
|
|
|
if (!currentConfig) {
|
|
console.log('No configuration events were received');
|
|
configStatus.textContent = 'NO CONFIGURATION EVENTS FOUND';
|
|
configStatus.className = 'status error';
|
|
relayStatus.textContent = 'SUBSCRIBED - NO EVENTS FOUND';
|
|
relayStatus.className = 'status error';
|
|
} else {
|
|
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
|
|
relayStatus.className = 'status connected';
|
|
}
|
|
|
|
} else if (messageType === "NOTICE") {
|
|
console.log('Received NOTICE:', eventData || message[1]);
|
|
|
|
} else if (messageType === "OK") {
|
|
console.log('Received OK response:', message);
|
|
}
|
|
}
|
|
|
|
} catch (parseError) {
|
|
console.error('Error parsing message:', parseError);
|
|
console.log('Raw message data:', event.data);
|
|
}
|
|
};
|
|
|
|
configWebSocket.onerror = function (error) {
|
|
clearTimeout(timeoutId);
|
|
console.error('WebSocket error:', error);
|
|
console.error('WebSocket URL that failed:', urlsToTry[0]);
|
|
console.error('WebSocket readyState on error:', configWebSocket?.readyState);
|
|
console.error('Error event details:', {
|
|
type: error.type,
|
|
target: error.target,
|
|
timeStamp: error.timeStamp
|
|
});
|
|
relayStatus.textContent = 'WEBSOCKET ERROR - Check Console';
|
|
relayStatus.className = 'status error';
|
|
reject(new Error(`WebSocket connection error to ${urlsToTry[0]}`));
|
|
};
|
|
|
|
configWebSocket.onclose = function (event) {
|
|
clearTimeout(timeoutId);
|
|
console.log('WebSocket connection closed:', event.code, event.reason);
|
|
relayStatus.textContent = 'CONNECTION CLOSED';
|
|
relayStatus.className = 'status error';
|
|
configWebSocket = null;
|
|
subscriptionId = null;
|
|
};
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Configuration subscription failed:', error.message);
|
|
console.error('Configuration subscription failed:', error);
|
|
console.error('Error stack:', error.stack);
|
|
|
|
relayStatus.textContent = 'SUBSCRIPTION FAILED';
|
|
relayStatus.className = 'status error';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Legacy function name for compatibility - now calls subscription
|
|
async function fetchConfiguration() {
|
|
return await subscribeToConfiguration();
|
|
}
|
|
|
|
function displayConfiguration(event) {
|
|
try {
|
|
console.log('=== DISPLAYING CONFIGURATION EVENT ===');
|
|
console.log('Event received for display:', event);
|
|
|
|
currentConfig = event;
|
|
|
|
// Clear existing table
|
|
configTableBody.innerHTML = '';
|
|
|
|
// Display basic event info
|
|
const basicInfo = [
|
|
['Event ID', event.id],
|
|
['Public Key', event.pubkey],
|
|
['Created At', new Date(event.created_at * 1000).toISOString()],
|
|
['Kind', event.kind],
|
|
['Content', event.content]
|
|
];
|
|
|
|
console.log(`Adding ${basicInfo.length} basic info rows`);
|
|
basicInfo.forEach(([key, value]) => {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `<td>${key}</td><td>${value}</td><td>-</td>`;
|
|
configTableBody.appendChild(row);
|
|
});
|
|
|
|
// Display tags
|
|
console.log(`Processing ${event.tags.length} tags`);
|
|
event.tags.forEach(tag => {
|
|
if (tag.length >= 2) {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `<td>${tag[0]}</td><td>${tag[1]}</td><td>-</td>`;
|
|
configTableBody.appendChild(row);
|
|
}
|
|
});
|
|
|
|
// Display raw JSON
|
|
rawConfigJson.textContent = JSON.stringify(event, null, 2);
|
|
|
|
console.log('Configuration display completed successfully');
|
|
updateConfigStatus(true);
|
|
|
|
} catch (error) {
|
|
console.error('Error in displayConfiguration:', error.message);
|
|
console.error('Display configuration error:', error);
|
|
}
|
|
}
|
|
|
|
// Configuration editing functions
|
|
function generateConfigForm(event) {
|
|
if (!event || !event.tags) {
|
|
console.log('No configuration event to edit');
|
|
return;
|
|
}
|
|
|
|
configForm.innerHTML = '';
|
|
|
|
// Define field types and validation for different config parameters
|
|
const fieldTypes = {
|
|
'auth_enabled': 'boolean',
|
|
'nip42_auth_required_events': 'boolean',
|
|
'nip42_auth_required_subscriptions': 'boolean',
|
|
'nip40_expiration_enabled': 'boolean',
|
|
'nip40_expiration_strict': 'boolean',
|
|
'nip40_expiration_filter': 'boolean',
|
|
'relay_port': 'number',
|
|
'max_connections': 'number',
|
|
'pow_min_difficulty': 'number',
|
|
'nip42_challenge_expiration': 'number',
|
|
'nip40_expiration_grace_period': 'number',
|
|
'max_subscriptions_per_client': 'number',
|
|
'max_total_subscriptions': 'number',
|
|
'max_filters_per_subscription': 'number',
|
|
'max_event_tags': 'number',
|
|
'max_content_length': 'number',
|
|
'max_message_length': 'number',
|
|
'default_limit': 'number',
|
|
'max_limit': 'number'
|
|
};
|
|
|
|
const descriptions = {
|
|
'relay_pubkey': 'Relay Public Key (Read-only)',
|
|
'auth_enabled': 'Enable Authentication',
|
|
'nip42_auth_required_events': 'Require Auth for Events',
|
|
'nip42_auth_required_subscriptions': 'Require Auth for Subscriptions',
|
|
'nip42_auth_required_kinds': 'Auth Required Event Kinds',
|
|
'nip42_challenge_expiration': 'Auth Challenge Expiration (seconds)',
|
|
'relay_port': 'Relay Port',
|
|
'max_connections': 'Maximum Connections',
|
|
'relay_description': 'Relay Description',
|
|
'relay_contact': 'Relay Contact',
|
|
'relay_software': 'Relay Software URL',
|
|
'relay_version': 'Relay Version',
|
|
'pow_min_difficulty': 'Minimum PoW Difficulty',
|
|
'pow_mode': 'PoW Mode',
|
|
'nip40_expiration_enabled': 'Enable Event Expiration',
|
|
'nip40_expiration_strict': 'Strict Expiration Mode',
|
|
'nip40_expiration_filter': 'Filter Expired Events',
|
|
'nip40_expiration_grace_period': 'Expiration Grace Period (seconds)',
|
|
'max_subscriptions_per_client': 'Max Subscriptions per Client',
|
|
'max_total_subscriptions': 'Max Total Subscriptions',
|
|
'max_filters_per_subscription': 'Max Filters per Subscription',
|
|
'max_event_tags': 'Max Event Tags',
|
|
'max_content_length': 'Max Content Length',
|
|
'max_message_length': 'Max Message Length',
|
|
'default_limit': 'Default Query Limit',
|
|
'max_limit': 'Maximum Query Limit'
|
|
};
|
|
|
|
// Process configuration tags (no d tag filtering for ephemeral events)
|
|
const configData = {};
|
|
event.tags.forEach(tag => {
|
|
if (tag.length >= 2) {
|
|
configData[tag[0]] = tag[1];
|
|
}
|
|
});
|
|
|
|
// Create form fields for each configuration parameter
|
|
Object.entries(configData).forEach(([key, value]) => {
|
|
const fieldType = fieldTypes[key] || 'text';
|
|
const description = descriptions[key] || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
|
|
const fieldGroup = document.createElement('div');
|
|
fieldGroup.className = 'input-group';
|
|
|
|
const label = document.createElement('label');
|
|
label.textContent = description;
|
|
label.setAttribute('for', `config-${key}`);
|
|
|
|
let input;
|
|
|
|
if (fieldType === 'boolean') {
|
|
input = document.createElement('select');
|
|
input.innerHTML = `
|
|
<option value="true" ${value === 'true' ? 'selected' : ''}>true</option>
|
|
<option value="false" ${value === 'false' ? 'selected' : ''}>false</option>
|
|
`;
|
|
} else if (fieldType === 'number') {
|
|
input = document.createElement('input');
|
|
input.type = 'number';
|
|
input.value = value;
|
|
input.min = '0';
|
|
} else {
|
|
input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.value = value;
|
|
}
|
|
|
|
input.id = `config-${key}`;
|
|
input.name = key;
|
|
|
|
// Make relay_pubkey read-only
|
|
if (key === 'relay_pubkey' || key === 'd') {
|
|
input.disabled = true;
|
|
}
|
|
|
|
fieldGroup.appendChild(label);
|
|
fieldGroup.appendChild(input);
|
|
configForm.appendChild(fieldGroup);
|
|
});
|
|
|
|
console.log('Configuration form generated');
|
|
}
|
|
|
|
function enterEditMode() {
|
|
if (!currentConfig) {
|
|
console.log('No configuration loaded to edit');
|
|
return;
|
|
}
|
|
|
|
generateConfigForm(currentConfig);
|
|
configViewMode.classList.add('hidden');
|
|
configEditMode.classList.remove('hidden');
|
|
console.log('Entered edit mode');
|
|
}
|
|
|
|
function exitEditMode() {
|
|
configViewMode.classList.remove('hidden');
|
|
configEditMode.classList.add('hidden');
|
|
configForm.innerHTML = '';
|
|
console.log('Exited edit mode');
|
|
}
|
|
|
|
|
|
async function saveConfiguration() {
|
|
if (!isLoggedIn || !userPubkey) {
|
|
console.log('Must be logged in to save configuration');
|
|
return;
|
|
}
|
|
|
|
if (!currentConfig) {
|
|
console.log('No current configuration to update');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('Building new configuration event...');
|
|
|
|
// Collect form data
|
|
const formData = new FormData();
|
|
const formInputs = configForm.querySelectorAll('input, select');
|
|
const newTags = [];
|
|
|
|
// Add updated configuration tags (no d tag needed for ephemeral events)
|
|
formInputs.forEach(input => {
|
|
if (!input.disabled && input.name) {
|
|
newTags.push([input.name, input.value]);
|
|
}
|
|
});
|
|
|
|
// Create new kind 23455 event (ephemeral configuration event)
|
|
const newEvent = {
|
|
kind: 23455,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: newTags,
|
|
content: JSON.stringify({
|
|
action: 'update_config',
|
|
config_data: Object.fromEntries(newTags.filter(tag => tag[0] !== 'd'))
|
|
})
|
|
};
|
|
|
|
console.log('Signing event with window.nostr.signEvent()...');
|
|
|
|
// Sign the event using the standard NIP-07 interface
|
|
const signedEvent = await window.nostr.signEvent(newEvent);
|
|
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed - no signature returned');
|
|
}
|
|
|
|
console.log('Event signed successfully');
|
|
console.log('Signed event:', signedEvent);
|
|
|
|
// Publish the event to the relay
|
|
await publishConfigurationEvent(signedEvent);
|
|
|
|
} catch (error) {
|
|
console.log('Configuration save failed: ' + error.message);
|
|
console.error('Save configuration error:', error);
|
|
}
|
|
}
|
|
|
|
async function publishConfigurationEvent(signedEvent) {
|
|
try {
|
|
const url = relayUrl.value.trim();
|
|
if (!url) {
|
|
throw new Error('No relay URL specified');
|
|
}
|
|
|
|
console.log('Publishing configuration event via WebSocket...');
|
|
|
|
// Create a new WebSocket connection for publishing
|
|
const publishWs = new WebSocket(url);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let timeoutId = setTimeout(() => {
|
|
console.error('Publish timeout');
|
|
publishWs.close();
|
|
reject(new Error('Publish timeout'));
|
|
}, 10000);
|
|
|
|
publishWs.onopen = function () {
|
|
console.log('Publish WebSocket connected, sending event...');
|
|
|
|
// Send EVENT message
|
|
const eventMessage = ["EVENT", signedEvent];
|
|
console.log('Sending EVENT message:', JSON.stringify(eventMessage));
|
|
publishWs.send(JSON.stringify(eventMessage));
|
|
};
|
|
|
|
publishWs.onmessage = function (event) {
|
|
try {
|
|
const message = JSON.parse(event.data);
|
|
console.log('Publish response:', message);
|
|
|
|
if (Array.isArray(message)) {
|
|
const [messageType, eventId, success, errorMsg] = message;
|
|
|
|
if (messageType === "OK") {
|
|
clearTimeout(timeoutId);
|
|
publishWs.close();
|
|
|
|
if (success) {
|
|
console.log('Configuration published successfully!');
|
|
console.log('The updated configuration should appear automatically via subscription');
|
|
|
|
// Exit edit mode
|
|
exitEditMode();
|
|
resolve(true);
|
|
} else {
|
|
console.error('Publish rejected:', errorMsg);
|
|
reject(new Error(`Publish rejected: ${errorMsg}`));
|
|
}
|
|
}
|
|
}
|
|
} catch (parseError) {
|
|
console.error('Error parsing publish response:', parseError);
|
|
}
|
|
};
|
|
|
|
publishWs.onerror = function (error) {
|
|
clearTimeout(timeoutId);
|
|
console.error('Publish WebSocket error:', error);
|
|
publishWs.close();
|
|
reject(new Error('Publish WebSocket error'));
|
|
};
|
|
|
|
publishWs.onclose = function () {
|
|
clearTimeout(timeoutId);
|
|
};
|
|
});
|
|
|
|
} catch (error) {
|
|
console.log('Configuration publish failed: ' + error.message);
|
|
console.error('Publish error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function updateEventPreview() {
|
|
const type = commandType.value;
|
|
const payload = commandPayload.value.trim();
|
|
|
|
if (!type || !userPubkey) {
|
|
eventPreview.textContent = 'No event constructed';
|
|
return;
|
|
}
|
|
|
|
const event = {
|
|
kind: 1,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
['p', 'RELAY_PUBLIC_KEY'],
|
|
['command', type]
|
|
],
|
|
content: payload || '{}',
|
|
id: 'EVENT_ID_PLACEHOLDER',
|
|
sig: 'SIGNATURE_PLACEHOLDER'
|
|
};
|
|
|
|
eventPreview.textContent = JSON.stringify(event, null, 2);
|
|
}
|
|
|
|
// Event handlers
|
|
logoutBtn.addEventListener('click', logout);
|
|
fetchConfigBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
fetchConfiguration().catch(error => {
|
|
console.log('Manual fetch configuration failed: ' + error.message);
|
|
});
|
|
});
|
|
|
|
copyConfigBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (currentConfig) {
|
|
navigator.clipboard.writeText(JSON.stringify(currentConfig, null, 2))
|
|
.then(() => console.log('Configuration copied to clipboard'))
|
|
.catch(err => console.log('Failed to copy: ' + err.message));
|
|
}
|
|
});
|
|
|
|
editConfigBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
enterEditMode();
|
|
});
|
|
|
|
saveConfigBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
saveConfiguration().catch(error => {
|
|
console.log('Save configuration failed: ' + error.message);
|
|
});
|
|
});
|
|
|
|
cancelEditBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
exitEditMode();
|
|
});
|
|
|
|
commandType.addEventListener('change', updateEventPreview);
|
|
commandPayload.addEventListener('input', updateEventPreview);
|
|
|
|
sendCommandBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const type = commandType.value;
|
|
if (!type) {
|
|
console.log('Please select a command type');
|
|
return;
|
|
}
|
|
|
|
console.log(`Command sending not yet implemented: ${type}`);
|
|
});
|
|
|
|
clearLogBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
logPanel.innerHTML = '<div class="log-entry"><span class="log-timestamp">SYSTEM:</span> Log cleared.</div>';
|
|
});
|
|
|
|
// ================================
|
|
// 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 = `<td colspan="6" style="text-align: center; font-style: italic;">No auth rules configured</td>`;
|
|
authRulesTableBody.appendChild(row);
|
|
return;
|
|
}
|
|
|
|
rules.forEach((rule, index) => {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>${rule.rule_type}</td>
|
|
<td>${rule.pattern_type || rule.operation || '-'}</td>
|
|
<td style="font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all; max-width: 200px;">${rule.pattern_value || rule.rule_target || '-'}</td>
|
|
<td>${rule.action || 'allow'}</td>
|
|
<td>${rule.enabled !== false ? 'Active' : 'Inactive'}</td>
|
|
<td>
|
|
<div class="inline-buttons">
|
|
<button onclick="editAuthRule(${index})" style="margin: 2px; padding: 4px 8px; font-size: 12px;">EDIT</button>
|
|
<button onclick="deleteAuthRule(${index})" style="margin: 2px; padding: 4px 8px; font-size: 12px;">DELETE</button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
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 23456 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 23456 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 23456 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;
|
|
}
|
|
|
|
statusDiv.className = 'rule-status';
|
|
statusDiv.textContent = 'Adding to whitelist...';
|
|
|
|
// 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 23456 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');
|
|
|
|
// Map client-side rule types to database schema values
|
|
let dbRuleType, dbPatternType, dbAction;
|
|
|
|
switch (ruleData.rule_type) {
|
|
case 'pubkey_blacklist':
|
|
dbRuleType = 'blacklist';
|
|
dbPatternType = 'pubkey';
|
|
dbAction = 'deny';
|
|
break;
|
|
case 'pubkey_whitelist':
|
|
dbRuleType = 'whitelist';
|
|
dbPatternType = 'pubkey';
|
|
dbAction = 'allow';
|
|
break;
|
|
case 'hash_blacklist':
|
|
dbRuleType = 'blacklist';
|
|
dbPatternType = 'pubkey'; // Schema supports: pubkey, kind, ip, global - using pubkey for hash for now
|
|
dbAction = 'deny';
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown rule type: ${ruleData.rule_type}`);
|
|
}
|
|
|
|
// Map pattern type to database schema values
|
|
if (ruleData.pattern_type === 'Global') {
|
|
dbPatternType = 'global';
|
|
} else if (ruleData.pattern_type === 'pubkey') {
|
|
dbPatternType = 'pubkey';
|
|
}
|
|
|
|
// Create kind 23456 auth rule event (ephemeral auth management)
|
|
const authEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
[dbRuleType, dbPatternType, ruleData.pattern_value]
|
|
],
|
|
content: JSON.stringify({
|
|
action: 'add',
|
|
rule_type: dbRuleType,
|
|
pattern_type: dbPatternType,
|
|
pattern_value: ruleData.pattern_value,
|
|
rule_action: dbAction
|
|
})
|
|
};
|
|
|
|
// DEBUG: Log the complete event structure being sent
|
|
console.log('=== AUTH RULE EVENT DEBUG ===');
|
|
console.log('Original Rule Data:', ruleData);
|
|
console.log('Mapped DB Values:', { dbRuleType, dbPatternType, dbAction });
|
|
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 using the standard NIP-07 interface
|
|
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');
|
|
setTimeout(initializeApp, 100);
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |