3761 lines
153 KiB
HTML
3761 lines
153 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>
|
|
:root {
|
|
/* Core Variables (6) */
|
|
--primary-color: #000000;
|
|
--secondary-color: #ffffff;
|
|
--accent-color: #ff0000;
|
|
--muted-color: #666666;
|
|
--font-family: "Courier New", Courier, monospace;
|
|
--border-radius: 15px;
|
|
--border-width: 3px;
|
|
|
|
/* Floating Tab Variables (8) */
|
|
--tab-bg-logged-out: #ffffff;
|
|
--tab-bg-logged-in: #ffffff;
|
|
--tab-bg-opacity-logged-out: 0.9;
|
|
--tab-bg-opacity-logged-in: 0.2;
|
|
--tab-color-logged-out: #000000;
|
|
--tab-color-logged-in: #ffffff;
|
|
--tab-border-logged-out: #000000;
|
|
--tab-border-logged-in: #ff0000;
|
|
--tab-border-opacity-logged-out: 1.0;
|
|
--tab-border-opacity-logged-in: 0.1;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: var(--font-family);
|
|
background-color: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
line-height: 1.4;
|
|
padding: 20px;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
border-bottom: var(--border-width) solid var(--primary-color);
|
|
padding-bottom: 10px;
|
|
margin-bottom: 30px;
|
|
font-weight: normal;
|
|
font-size: 24px;
|
|
font-family: var(--font-family);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
h2 {
|
|
font-weight: normal;
|
|
padding-left: 10px;
|
|
font-size: 16px;
|
|
font-family: var(--font-family);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.section {
|
|
background: var(--secondary-color);
|
|
border: var(--border-width) solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.input-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
font-family: var(--font-family);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
input,
|
|
textarea,
|
|
select {
|
|
width: 100%;
|
|
padding: 8px;
|
|
background: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
border: var(--border-width) solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
font-family: var(--font-family);
|
|
font-size: 14px;
|
|
box-sizing: border-box;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
input:focus,
|
|
textarea:focus,
|
|
select:focus {
|
|
border-color: var(--accent-color);
|
|
outline: none;
|
|
}
|
|
|
|
button {
|
|
width: 100%;
|
|
padding: 8px;
|
|
background: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
border: var(--border-width) solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
font-family: var(--font-family);
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
margin: 5px 0;
|
|
font-weight: bold;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
button:hover {
|
|
border-color: var(--accent-color);
|
|
}
|
|
|
|
button:active {
|
|
background: var(--accent-color);
|
|
color: var(--secondary-color);
|
|
}
|
|
|
|
button:disabled {
|
|
background-color: #ccc;
|
|
color: var(--muted-color);
|
|
cursor: not-allowed;
|
|
border-color: #ccc;
|
|
}
|
|
|
|
.status {
|
|
padding: 10px;
|
|
margin: 10px 0;
|
|
border: var(--border-width) solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
font-weight: bold;
|
|
font-family: var(--font-family);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.status.connected {
|
|
background-color: var(--primary-color);
|
|
color: var(--secondary-color);
|
|
}
|
|
|
|
.status.disconnected {
|
|
background-color: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.status.authenticated {
|
|
background-color: var(--primary-color);
|
|
color: var(--secondary-color);
|
|
}
|
|
|
|
.status.error {
|
|
background-color: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
border-color: var(--accent-color);
|
|
}
|
|
|
|
.config-table {
|
|
border: 1px solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
margin: 10px 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.config-table th,
|
|
.config-table td {
|
|
border: 1px solid var(--primary-color);
|
|
padding: 8px;
|
|
text-align: left;
|
|
font-family: var(--font-family);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.config-table-container {
|
|
overflow-x: auto;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.config-table th {
|
|
background-color: var(--primary-color);
|
|
color: var(--secondary-color);
|
|
font-weight: bold;
|
|
}
|
|
|
|
.json-display {
|
|
background-color: var(--secondary-color);
|
|
border: var(--border-width) solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
padding: 10px;
|
|
font-family: var(--font-family);
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.log-panel {
|
|
height: 200px;
|
|
overflow-y: auto;
|
|
border: var(--border-width) solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
padding: 10px;
|
|
font-size: 12px;
|
|
background-color: var(--secondary-color);
|
|
font-family: var(--font-family);
|
|
}
|
|
|
|
.log-entry {
|
|
margin-bottom: 5px;
|
|
border-bottom: 1px solid var(--muted-color);
|
|
padding-bottom: 5px;
|
|
}
|
|
|
|
.log-timestamp {
|
|
font-weight: bold;
|
|
font-family: var(--font-family);
|
|
}
|
|
|
|
.inline-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.inline-buttons button {
|
|
flex: 1;
|
|
}
|
|
|
|
.user-info {
|
|
padding: 10px;
|
|
border: var(--border-width) solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
margin: 10px 0;
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
.user-info-container {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 20px;
|
|
}
|
|
|
|
.user-details {
|
|
flex: 1;
|
|
}
|
|
|
|
.login-logout-btn {
|
|
width: auto;
|
|
min-width: 120px;
|
|
padding: 12px 16px;
|
|
background: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
border: var(--border-width) solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
font-family: var(--font-family);
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
margin: 0;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.login-logout-btn:hover {
|
|
border-color: var(--accent-color);
|
|
}
|
|
|
|
.login-logout-btn:active {
|
|
background: var(--accent-color);
|
|
color: var(--secondary-color);
|
|
}
|
|
|
|
.login-logout-btn.logout-state {
|
|
background: var(--accent-color);
|
|
color: var(--secondary-color);
|
|
border-color: var(--accent-color);
|
|
}
|
|
|
|
.login-logout-btn.logout-state:hover {
|
|
background: var(--primary-color);
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.user-pubkey {
|
|
font-family: var(--font-family);
|
|
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: var(--border-width) solid var(--primary-color);
|
|
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: var(--border-width) solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
padding: 15px;
|
|
margin: 15px 0;
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
.auth-rule-section h3 {
|
|
margin: 0 0 10px 0;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
border-left: 4px solid var(--primary-color);
|
|
padding-left: 8px;
|
|
font-family: var(--font-family);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.auth-rule-section p {
|
|
margin: 0 0 15px 0;
|
|
font-size: 13px;
|
|
color: var(--muted-color);
|
|
font-family: var(--font-family);
|
|
}
|
|
|
|
.rule-status {
|
|
margin-top: 10px;
|
|
padding: 8px;
|
|
border: var(--border-width) solid var(--muted-color);
|
|
border-radius: var(--border-radius);
|
|
font-size: 12px;
|
|
min-height: 20px;
|
|
background-color: var(--secondary-color);
|
|
font-family: var(--font-family);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.rule-status.success {
|
|
border-color: #4CAF50;
|
|
background-color: #E8F5E8;
|
|
color: #2E7D32;
|
|
}
|
|
|
|
.rule-status.error {
|
|
border-color: var(--accent-color);
|
|
background-color: #FFEBEE;
|
|
color: #C62828;
|
|
}
|
|
|
|
.rule-status.warning {
|
|
border-color: #FF9800;
|
|
background-color: #FFF3E0;
|
|
color: #E65100;
|
|
}
|
|
|
|
.warning-box {
|
|
border: var(--border-width) solid #FF9800;
|
|
border-radius: var(--border-radius);
|
|
background-color: #FFF3E0;
|
|
padding: 10px;
|
|
margin: 10px 0;
|
|
font-size: 13px;
|
|
color: #E65100;
|
|
font-family: var(--font-family);
|
|
}
|
|
|
|
.warning-box strong {
|
|
color: #D84315;
|
|
}
|
|
|
|
#login-section {
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
/* Floating tab styles */
|
|
.floating-tab {
|
|
font-family: var(--font-family);
|
|
border-radius: var(--border-radius);
|
|
border: var(--border-width) solid;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.floating-tab--logged-out {
|
|
background: rgba(255, 255, 255, var(--tab-bg-opacity-logged-out));
|
|
color: var(--tab-color-logged-out);
|
|
border-color: rgba(0, 0, 0, var(--tab-border-opacity-logged-out));
|
|
}
|
|
|
|
.floating-tab--logged-in {
|
|
background: rgba(0, 0, 0, var(--tab-bg-opacity-logged-in));
|
|
color: var(--tab-color-logged-in);
|
|
border-color: rgba(255, 0, 0, var(--tab-border-opacity-logged-in));
|
|
}
|
|
|
|
.transition {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
/* Main Sections Wrapper */
|
|
.main-sections-wrapper {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--border-width);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.flex-section {
|
|
flex: 1;
|
|
min-width: 300px;
|
|
}
|
|
|
|
@media (max-width: 700px) {
|
|
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>
|
|
|
|
<!-- Main Sections Wrapper -->
|
|
<div class="main-sections-wrapper">
|
|
|
|
<!-- Persistent Authentication Header - Always Visible -->
|
|
<div id="persistent-auth-container" class="section flex-section">
|
|
<div class="user-info-container">
|
|
<button type="button" id="login-logout-btn" class="login-logout-btn">LOGIN</button>
|
|
<div class="user-details" id="persistent-user-details" style="display: none;">
|
|
<div><strong>Name:</strong> <span id="persistent-user-name">Loading...</span></div>
|
|
<div><strong>Public Key:</strong>
|
|
<div class="user-pubkey" id="persistent-user-pubkey">Loading...</div>
|
|
</div>
|
|
<div><strong>About:</strong> <span id="persistent-user-about">Loading...</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Login Section -->
|
|
<div id="login-section" class="flex-section">
|
|
<div class="section">
|
|
<h2>NOSTR AUTHENTICATION</h2>
|
|
<p id="login-instructions">Please login with your Nostr identity to access the admin interface.</p>
|
|
<!-- nostr-lite login UI will be injected here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Relay Connection Section -->
|
|
<div id="relay-connection-section" class="flex-section">
|
|
<div class="section">
|
|
<h2>RELAY CONNECTION</h2>
|
|
|
|
<div class="input-group">
|
|
<label for="relay-connection-url">Relay URL:</label>
|
|
<input type="text" id="relay-connection-url" value="ws://localhost:8888"
|
|
placeholder="ws://localhost:8888 or wss://relay.example.com">
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<label for="relay-pubkey-manual">Relay Pubkey (if not available via NIP-11):</label>
|
|
<input type="text" id="relay-pubkey-manual" placeholder="64-character hex pubkey"
|
|
pattern="[0-9a-fA-F]{64}" title="64-character hexadecimal public key">
|
|
|
|
</div>
|
|
|
|
<div class="inline-buttons">
|
|
<button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
|
|
<button type="button" id="disconnect-relay-btn" disabled>DISCONNECT</button>
|
|
<button type="button" id="test-websocket-btn" disabled>TEST WEBSOCKET</button>
|
|
</div>
|
|
|
|
<div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div>
|
|
|
|
<!-- Relay Information Display -->
|
|
<div id="relay-info-display" class="hidden">
|
|
<h3>Relay Information (NIP-11)</h3>
|
|
<table class="config-table" id="relay-info-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Property</th>
|
|
<th>Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="relay-info-table-body">
|
|
</tbody>
|
|
</table>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div> <!-- End Main Sections Wrapper -->
|
|
|
|
|
|
|
|
|
|
<!-- Testing Section -->
|
|
<div id="div_config" class="section flex-section" style="display: none;">
|
|
<h2>RELAY CONFIGURATION</h2>
|
|
<div id="config-display" class="hidden">
|
|
<div id="config-view-mode">
|
|
<div class="config-table-container">
|
|
<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>
|
|
|
|
<div class="inline-buttons">
|
|
|
|
<button type="button" id="edit-config-btn">EDIT CONFIGURATION</button>
|
|
<button type="button" id="copy-config-btn">COPY CONFIGURATION</button>
|
|
<button type="button" id="fetch-config-btn">REFRESH</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>
|
|
</div>
|
|
|
|
<!-- Auth Rules Management - Moved after configuration -->
|
|
<div class="section flex-section" id="authRulesSection" style="display: none;">
|
|
<div class="section-header">
|
|
<h2>AUTH RULES MANAGEMENT</h2>
|
|
</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>
|
|
|
|
<!-- Simplified Auth Rule Input Section -->
|
|
<div id="authRuleInputSections" style="display: block;">
|
|
|
|
<!-- Combined Pubkey Auth Rule Section -->
|
|
|
|
|
|
<div class="input-group">
|
|
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
|
|
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
|
|
|
|
</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>
|
|
<div class="inline-buttons">
|
|
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
|
|
WHITELIST</button>
|
|
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
|
|
BLACKLIST</button>
|
|
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- TESTS Section -->
|
|
<div class="section">
|
|
<h2>ADMIN API TESTS</h2>
|
|
<p>Test the admin API functionality with real-time event logging. Login required for authenticated tests.</p>
|
|
|
|
<!-- Event Log Display -->
|
|
<div class="input-group">
|
|
<label for="test-event-log">Event Log (Sent/Received):</label>
|
|
<div class="log-panel" id="test-event-log" style="height: 300px;">
|
|
<div class="log-entry">
|
|
<span class="log-timestamp">SYSTEM:</span> Test interface ready. Click buttons below to test admin
|
|
API functions.
|
|
</div>
|
|
</div>
|
|
<button type="button" id="clear-test-log-btn">CLEAR TEST LOG</button>
|
|
</div>
|
|
|
|
<!-- Test Buttons -->
|
|
<div class="input-group">
|
|
<label>Admin API Tests:</label>
|
|
<div class="inline-buttons">
|
|
<button type="button" id="test-get-auth-rules-btn">GET AUTH RULES</button>
|
|
<button type="button" id="test-clear-auth-rules-btn">CLEAR AUTH RULES</button>
|
|
</div>
|
|
<div class="inline-buttons">
|
|
<button type="button" id="test-add-blacklist-btn">ADD BLACKLIST</button>
|
|
<button type="button" id="test-add-whitelist-btn">ADD WHITELIST</button>
|
|
</div>
|
|
<div class="inline-buttons">
|
|
<button type="button" id="test-config-query-btn">CONFIG QUERY</button>
|
|
<button type="button" id="test-post-event-btn">POST EVENT</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test Input Fields -->
|
|
<div id="test-input-section" style="display: none;">
|
|
<div class="input-group">
|
|
<label for="test-pubkey-input">Test Pubkey (for blacklist/whitelist):</label>
|
|
<div style="display: flex; gap: 10px; align-items: flex-start;">
|
|
<input type="text" id="test-pubkey-input" placeholder="Enter pubkey or nsec1... for testing"
|
|
style="flex: 1;">
|
|
<button type="button" id="generate-test-key-btn"
|
|
style="width: auto; padding: 8px 16px; white-space: nowrap;">GENERATE KEY</button>
|
|
</div>
|
|
<small>This pubkey will be used for blacklist/whitelist tests</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test Status -->
|
|
<div class="status disconnected" id="test-status">READY TO TEST</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;
|
|
// Global subscription state
|
|
let relayPool = null;
|
|
let subscriptionId = null;
|
|
// Relay connection state
|
|
let relayInfo = null;
|
|
let isRelayConnected = false;
|
|
let relayPubkey = null;
|
|
|
|
// DOM elements
|
|
const loginSection = document.getElementById('login-section');
|
|
// const mainInterface = document.getElementById('main-interface');
|
|
const persistentUserName = document.getElementById('persistent-user-name');
|
|
const persistentUserPubkey = document.getElementById('persistent-user-pubkey');
|
|
const persistentUserAbout = document.getElementById('persistent-user-about');
|
|
const persistentUserDetails = document.getElementById('persistent-user-details');
|
|
const fetchConfigBtn = document.getElementById('fetch-config-btn');
|
|
// Relay connection elements
|
|
const relayConnectionUrl = document.getElementById('relay-connection-url');
|
|
const relayPubkeyManual = document.getElementById('relay-pubkey-manual');
|
|
const relayConnectionStatus = document.getElementById('relay-connection-status');
|
|
const connectRelayBtn = document.getElementById('connect-relay-btn');
|
|
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
|
|
const testWebSocketBtn = document.getElementById('test-websocket-btn');
|
|
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 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');
|
|
|
|
// 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);
|
|
|
|
// UI logging removed - using console only
|
|
}
|
|
|
|
// ================================
|
|
// NIP-11 RELAY CONNECTION FUNCTIONS
|
|
// ================================
|
|
|
|
// Convert WebSocket URL to HTTP URL for NIP-11
|
|
function wsToHttpUrl(wsUrl) {
|
|
if (wsUrl.startsWith('ws://')) {
|
|
return wsUrl.replace('ws://', 'http://');
|
|
} else if (wsUrl.startsWith('wss://')) {
|
|
return wsUrl.replace('wss://', 'https://');
|
|
}
|
|
return wsUrl;
|
|
}
|
|
|
|
// Fetch relay information using NIP-11
|
|
async function fetchRelayInfo(relayUrl) {
|
|
try {
|
|
log(`Fetching NIP-11 relay info from: ${relayUrl}`, 'INFO');
|
|
|
|
// Convert WebSocket URL to HTTP URL
|
|
const httpUrl = wsToHttpUrl(relayUrl);
|
|
|
|
// Make HTTP request with NIP-11 headers
|
|
const response = await fetch(httpUrl, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/nostr+json',
|
|
'User-Agent': 'C-Relay-Admin-API/1.0'
|
|
},
|
|
timeout: 10000 // 10 second timeout
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type');
|
|
if (!contentType || !contentType.includes('application/nostr+json')) {
|
|
throw new Error(`Invalid content type: ${contentType}. Expected application/nostr+json`);
|
|
}
|
|
|
|
const relayInfo = await response.json();
|
|
|
|
// Log if relay info is empty (not configured yet) but don't throw error
|
|
if (!relayInfo || Object.keys(relayInfo).length === 0) {
|
|
log('Relay returned empty NIP-11 info - relay not configured yet, will use manual pubkey if provided', 'INFO');
|
|
// Return empty object - this is valid, caller will handle manual pubkey fallback
|
|
return {};
|
|
}
|
|
|
|
// Validate pubkey if present
|
|
if (relayInfo.pubkey && !/^[0-9a-fA-F]{64}$/.test(relayInfo.pubkey)) {
|
|
throw new Error(`Invalid relay pubkey format: ${relayInfo.pubkey}`);
|
|
}
|
|
|
|
log(`Successfully fetched relay info. Pubkey: ${relayInfo.pubkey ? relayInfo.pubkey.substring(0, 16) + '...' : 'not set'}`, 'INFO');
|
|
return relayInfo;
|
|
|
|
} catch (error) {
|
|
log(`Failed to fetch relay info: ${error.message}`, 'ERROR');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Test WebSocket connection to relay
|
|
async function testWebSocketConnection(wsUrl) {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
log(`Testing WebSocket connection to: ${wsUrl}`, 'INFO');
|
|
|
|
const ws = new WebSocket(wsUrl);
|
|
const timeout = setTimeout(() => {
|
|
ws.close();
|
|
reject(new Error('WebSocket connection timeout (10s)'));
|
|
}, 10000);
|
|
|
|
ws.onopen = () => {
|
|
clearTimeout(timeout);
|
|
log('WebSocket connection successful', 'INFO');
|
|
ws.close();
|
|
resolve(true);
|
|
};
|
|
|
|
ws.onerror = (error) => {
|
|
clearTimeout(timeout);
|
|
log(`WebSocket connection failed: ${error.message || 'Unknown error'}`, 'ERROR');
|
|
reject(new Error('WebSocket connection failed'));
|
|
};
|
|
|
|
ws.onclose = (event) => {
|
|
if (event.code !== 1000) { // 1000 = normal closure
|
|
clearTimeout(timeout);
|
|
reject(new Error(`WebSocket closed unexpectedly: ${event.code} ${event.reason}`));
|
|
}
|
|
};
|
|
|
|
} catch (error) {
|
|
log(`WebSocket test error: ${error.message}`, 'ERROR');
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Connect to relay (NIP-11 + WebSocket test)
|
|
async function connectToRelay() {
|
|
try {
|
|
const url = relayConnectionUrl.value.trim();
|
|
if (!url) {
|
|
throw new Error('Please enter a relay URL');
|
|
}
|
|
|
|
// Update UI to show connecting state
|
|
updateRelayConnectionStatus('connecting');
|
|
connectRelayBtn.disabled = true;
|
|
|
|
log(`Connecting to relay: ${url}`, 'INFO');
|
|
|
|
let fetchedRelayInfo;
|
|
|
|
try {
|
|
// Step 1: Try to fetch NIP-11 relay information
|
|
fetchedRelayInfo = await fetchRelayInfo(url);
|
|
|
|
// Check if NIP-11 response includes a pubkey
|
|
if (fetchedRelayInfo.pubkey) {
|
|
// NIP-11 provided pubkey - populate the manual input field
|
|
log(`NIP-11 provided relay pubkey: ${fetchedRelayInfo.pubkey.substring(0, 16)}...`, 'INFO');
|
|
relayPubkeyManual.value = fetchedRelayInfo.pubkey;
|
|
} else {
|
|
// NIP-11 response missing pubkey, check for manual input
|
|
log('NIP-11 response missing pubkey, checking for manual input...', 'INFO');
|
|
|
|
const manualPubkey = relayPubkeyManual.value.trim();
|
|
if (!manualPubkey) {
|
|
throw new Error('Relay NIP-11 response does not include a pubkey. Please enter the relay pubkey manually (shown during relay startup).');
|
|
}
|
|
|
|
if (!/^[0-9a-fA-F]{64}$/.test(manualPubkey)) {
|
|
throw new Error('Manual relay pubkey must be exactly 64 hexadecimal characters');
|
|
}
|
|
|
|
log(`Using manual relay pubkey: ${manualPubkey.substring(0, 16)}...`, 'INFO');
|
|
|
|
// Add manual pubkey to the fetched relay info
|
|
fetchedRelayInfo.pubkey = manualPubkey;
|
|
|
|
// If relay info was completely empty, create minimal info
|
|
if (Object.keys(fetchedRelayInfo).length === 1) {
|
|
fetchedRelayInfo = {
|
|
name: 'C-Relay (Manual Config)',
|
|
description: 'C-Relay instance - pubkey provided manually',
|
|
pubkey: manualPubkey,
|
|
contact: 'admin@manual.config.relay',
|
|
supported_nips: [1, 9, 11, 13, 15, 20, 33, 40, 42],
|
|
software: 'https://github.com/0xtrr/c-relay',
|
|
version: '1.0.0'
|
|
};
|
|
}
|
|
}
|
|
|
|
} catch (nip11Error) {
|
|
// If NIP-11 completely fails (network error, etc.), require manual pubkey
|
|
const manualPubkey = relayPubkeyManual.value.trim();
|
|
if (!manualPubkey) {
|
|
throw new Error(`NIP-11 fetch failed: ${nip11Error.message}. Please enter the relay pubkey manually if the relay hasn't been configured yet.`);
|
|
}
|
|
|
|
if (!/^[0-9a-fA-F]{64}$/.test(manualPubkey)) {
|
|
throw new Error('Manual relay pubkey must be exactly 64 hexadecimal characters');
|
|
}
|
|
|
|
log(`NIP-11 failed, using manual relay pubkey: ${manualPubkey.substring(0, 16)}...`, 'INFO');
|
|
|
|
// Create minimal relay info with manual pubkey
|
|
fetchedRelayInfo = {
|
|
name: 'C-Relay (Manual Config)',
|
|
description: 'C-Relay instance - pubkey provided manually',
|
|
pubkey: manualPubkey,
|
|
contact: 'admin@manual.config.relay',
|
|
supported_nips: [1, 9, 11, 13, 15, 20, 33, 40, 42],
|
|
software: 'https://github.com/0xtrr/c-relay',
|
|
version: '1.0.0'
|
|
};
|
|
}
|
|
|
|
// Step 2: Test WebSocket connection
|
|
await testWebSocketConnection(url);
|
|
|
|
// Step 3: Update global state
|
|
relayInfo = fetchedRelayInfo;
|
|
relayPubkey = fetchedRelayInfo.pubkey;
|
|
isRelayConnected = true;
|
|
|
|
// Step 4: Update UI
|
|
updateRelayConnectionStatus('connected');
|
|
updateAdminSectionsVisibility();
|
|
|
|
// Step 5: Relay URL updated
|
|
|
|
// Step 6: Automatically load configuration and auth rules
|
|
log('Relay connected successfully. Auto-loading configuration and auth rules...', 'INFO');
|
|
|
|
// Auto-fetch configuration
|
|
setTimeout(() => {
|
|
fetchConfiguration().catch(error => {
|
|
log('Auto-fetch configuration failed: ' + error.message, 'ERROR');
|
|
});
|
|
}, 500);
|
|
|
|
// Auto-fetch auth rules
|
|
setTimeout(() => {
|
|
loadAuthRules().catch(error => {
|
|
log('Auto-fetch auth rules failed: ' + error.message, 'ERROR');
|
|
});
|
|
}, 1000);
|
|
|
|
log(`Successfully connected to relay: ${relayInfo.name || 'Unknown'}`, 'INFO');
|
|
|
|
} catch (error) {
|
|
log(`Failed to connect to relay: ${error.message}`, 'ERROR');
|
|
updateRelayConnectionStatus('error');
|
|
|
|
// Reset state on failure
|
|
relayInfo = null;
|
|
relayPubkey = null;
|
|
isRelayConnected = false;
|
|
|
|
} finally {
|
|
connectRelayBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Disconnect from relay
|
|
function disconnectFromRelay() {
|
|
try {
|
|
log('Disconnecting from relay...', 'INFO');
|
|
|
|
// Clean up relay pool if exists
|
|
if (relayPool) {
|
|
const url = relayConnectionUrl.value.trim();
|
|
if (url) {
|
|
relayPool.close([url]);
|
|
}
|
|
relayPool = null;
|
|
subscriptionId = null;
|
|
}
|
|
|
|
// Reset state
|
|
relayInfo = null;
|
|
relayPubkey = null;
|
|
isRelayConnected = false;
|
|
|
|
// Update UI
|
|
updateRelayConnectionStatus('disconnected');
|
|
hideRelayInfo();
|
|
updateAdminSectionsVisibility();
|
|
|
|
log('Disconnected from relay', 'INFO');
|
|
|
|
} catch (error) {
|
|
log(`Error during relay disconnection: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
// Update relay connection status UI
|
|
function updateRelayConnectionStatus(status) {
|
|
if (!relayConnectionStatus) return;
|
|
|
|
switch (status) {
|
|
case 'connecting':
|
|
relayConnectionStatus.textContent = 'CONNECTING...';
|
|
relayConnectionStatus.className = 'status connected';
|
|
connectRelayBtn.disabled = true;
|
|
disconnectRelayBtn.disabled = true;
|
|
testWebSocketBtn.disabled = true;
|
|
break;
|
|
case 'connected':
|
|
relayConnectionStatus.textContent = 'CONNECTED';
|
|
relayConnectionStatus.className = 'status connected';
|
|
connectRelayBtn.disabled = true;
|
|
disconnectRelayBtn.disabled = false;
|
|
testWebSocketBtn.disabled = false;
|
|
break;
|
|
case 'disconnected':
|
|
relayConnectionStatus.textContent = 'NOT CONNECTED';
|
|
relayConnectionStatus.className = 'status disconnected';
|
|
connectRelayBtn.disabled = false;
|
|
disconnectRelayBtn.disabled = true;
|
|
testWebSocketBtn.disabled = true;
|
|
break;
|
|
case 'error':
|
|
relayConnectionStatus.textContent = 'CONNECTION FAILED';
|
|
relayConnectionStatus.className = 'status error';
|
|
connectRelayBtn.disabled = false;
|
|
disconnectRelayBtn.disabled = true;
|
|
testWebSocketBtn.disabled = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Hide relay information display (placeholder for removed functionality)
|
|
function hideRelayInfo() {
|
|
// Relay info display functionality has been removed
|
|
console.log('Relay info display functionality has been removed');
|
|
}
|
|
|
|
// 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();
|
|
updateLoginLogoutButton();
|
|
|
|
// Note: Configuration fetching now requires explicit relay connection
|
|
// User must connect to relay manually after login
|
|
console.log('✅ Authentication state restored - connect to relay to fetch configuration');
|
|
|
|
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: false
|
|
},
|
|
floatingTab: {
|
|
enabled: false,
|
|
// 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);
|
|
window.addEventListener('nlLogout', handleLogoutEvent);
|
|
|
|
} 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();
|
|
updateLoginLogoutButton();
|
|
|
|
// Note: Configuration fetching now requires explicit relay connection
|
|
// User must connect to relay manually after login
|
|
console.log('Login successful. Connect to relay to access admin functions.');
|
|
|
|
} else if (error) {
|
|
console.log(`Authentication error: ${error}`);
|
|
}
|
|
}
|
|
|
|
// Handle logout events
|
|
function handleLogoutEvent() {
|
|
console.log('Logout event received');
|
|
|
|
userPubkey = null;
|
|
isLoggedIn = false;
|
|
currentConfig = null;
|
|
|
|
// Clean up relay connection
|
|
disconnectFromRelay();
|
|
|
|
// Reset UI
|
|
// mainInterface.classList.add('hidden');
|
|
loginSection.classList.remove('hidden');
|
|
updateConfigStatus(false);
|
|
updateLoginLogoutButton();
|
|
updateAdminSectionsVisibility();
|
|
|
|
console.log('Logout event handled successfully');
|
|
}
|
|
|
|
|
|
// Update visibility of admin sections based on login and relay connection status
|
|
function updateAdminSectionsVisibility() {
|
|
const divConfig = document.getElementById('div_config');
|
|
const authRulesSection = document.getElementById('authRulesSection');
|
|
const shouldShow = isLoggedIn && isRelayConnected;
|
|
|
|
if (divConfig) divConfig.style.display = shouldShow ? 'block' : 'none';
|
|
if (authRulesSection) authRulesSection.style.display = shouldShow ? 'block' : 'none';
|
|
}
|
|
|
|
// Show main interface after login
|
|
function showMainInterface() {
|
|
loginSection.classList.add('hidden');
|
|
// mainInterface.classList.remove('hidden');
|
|
updateLoginLogoutButton();
|
|
updateAdminSectionsVisibility();
|
|
}
|
|
|
|
// Load user profile using nostr-tools pool
|
|
async function loadUserProfile() {
|
|
if (!userPubkey) return;
|
|
|
|
console.log('Loading user profile...');
|
|
persistentUserName.textContent = 'Loading...';
|
|
persistentUserAbout.textContent = 'Loading...';
|
|
|
|
try {
|
|
// Create a SimplePool instance for profile loading
|
|
const profilePool = new window.NostrTools.SimplePool();
|
|
const relays = ['wss://relay.laantungir.net'];
|
|
|
|
// Get profile event (kind 0) for the user
|
|
const events = await profilePool.querySync(relays, {
|
|
kinds: [0],
|
|
authors: [userPubkey],
|
|
limit: 1
|
|
});
|
|
|
|
if (events.length > 0) {
|
|
console.log('Profile event found:', events[0]);
|
|
const profile = JSON.parse(events[0].content);
|
|
console.log('Parsed profile:', profile);
|
|
displayProfile(profile);
|
|
} else {
|
|
console.log('No profile events found for pubkey:', userPubkey);
|
|
persistentUserName.textContent = 'Anonymous User';
|
|
persistentUserAbout.textContent = 'No profile found';
|
|
// Still show the pubkey since we have it
|
|
persistentUserPubkey.textContent = userPubkey;
|
|
}
|
|
|
|
// Close the profile pool
|
|
profilePool.close(relays);
|
|
|
|
} catch (error) {
|
|
console.log('Profile loading failed: ' + error.message);
|
|
persistentUserName.textContent = 'Error loading profile';
|
|
persistentUserAbout.textContent = error.message;
|
|
// Still show the pubkey since we have it
|
|
persistentUserPubkey.textContent = userPubkey;
|
|
}
|
|
}
|
|
|
|
// Display profile data
|
|
function displayProfile(profile) {
|
|
const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User';
|
|
const about = profile.about || 'No description provided';
|
|
|
|
// Update persistent user details
|
|
persistentUserName.textContent = name;
|
|
persistentUserPubkey.textContent = userPubkey;
|
|
persistentUserAbout.textContent = about;
|
|
|
|
console.log(`Profile loaded for: ${name} with pubkey: ${userPubkey}`);
|
|
}
|
|
|
|
// Logout function
|
|
async function logout() {
|
|
console.log('Logging out...');
|
|
try {
|
|
// Clean up relay connection
|
|
disconnectFromRelay();
|
|
|
|
// Clean up configuration pool
|
|
if (relayPool) {
|
|
console.log('Closing configuration pool...');
|
|
const url = relayConnectionUrl.value.trim();
|
|
if (url) {
|
|
relayPool.close([url]);
|
|
}
|
|
relayPool = null;
|
|
subscriptionId = null;
|
|
}
|
|
|
|
await nlLite.logout();
|
|
|
|
userPubkey = null;
|
|
isLoggedIn = false;
|
|
currentConfig = null;
|
|
|
|
// Reset UI - keep persistent auth container visible
|
|
// mainInterface.classList.add('hidden');
|
|
loginSection.classList.remove('hidden');
|
|
updateConfigStatus(false);
|
|
updateLoginLogoutButton();
|
|
|
|
console.log('Logged out successfully');
|
|
|
|
} catch (error) {
|
|
console.log('Logout failed: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function updateConfigStatus(loaded) {
|
|
if (loaded) {
|
|
configDisplay.classList.remove('hidden');
|
|
} else {
|
|
configDisplay.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Generate random subscription ID
|
|
function generateSubId() {
|
|
return Math.random().toString(36).substring(2, 15);
|
|
}
|
|
|
|
// Configuration subscription using nostr-tools SimplePool
|
|
async function subscribeToConfiguration() {
|
|
try {
|
|
console.log('=== STARTING SIMPLEPOOL CONFIGURATION SUBSCRIPTION ===');
|
|
|
|
if (!isLoggedIn) {
|
|
console.log('WARNING: Not logged in, but proceeding with subscription test');
|
|
}
|
|
|
|
const url = relayConnectionUrl.value.trim();
|
|
if (!url) {
|
|
console.error('Please enter a relay URL');
|
|
return false;
|
|
}
|
|
|
|
console.log(`Connecting to relay via SimplePool: ${url}`);
|
|
|
|
// Clean up existing pool
|
|
if (relayPool) {
|
|
console.log('Closing existing pool connection');
|
|
relayPool.close([url]);
|
|
relayPool = null;
|
|
subscriptionId = null;
|
|
}
|
|
|
|
// Create new SimplePool instance
|
|
relayPool = new window.NostrTools.SimplePool();
|
|
subscriptionId = generateSubId();
|
|
|
|
console.log(`Generated subscription ID: ${subscriptionId}`);
|
|
|
|
// Subscribe to kind 23457 events (admin response events)
|
|
const subscription = relayPool.subscribeMany([url], [{
|
|
since: Math.floor(Date.now() / 1000),
|
|
kinds: [23457],
|
|
authors: [getRelayPubkey()], // Only listen to responses from the relay
|
|
"#p": [userPubkey], // Only responses directed to this user
|
|
limit: 50
|
|
}], {
|
|
onevent(event) {
|
|
console.log('=== ADMIN RESPONSE EVENT RECEIVED VIA SIMPLEPOOL ===');
|
|
console.log('Event data:', event);
|
|
console.log('Event kind:', event.kind);
|
|
console.log('Event tags:', event.tags);
|
|
console.log('Event pubkey:', event.pubkey);
|
|
console.log('=== END ADMIN RESPONSE ===');
|
|
|
|
// Log all received messages for testing
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('RECV', `Admin response event: ${JSON.stringify(event)}`, 'EVENT');
|
|
}
|
|
|
|
// Process admin response event
|
|
processAdminResponse(event);
|
|
},
|
|
oneose() {
|
|
console.log('EOSE received - End of stored events');
|
|
console.log('Current config after EOSE:', currentConfig);
|
|
|
|
if (!currentConfig) {
|
|
console.log('No configuration events were received');
|
|
}
|
|
},
|
|
onclose(reason) {
|
|
console.log('Subscription closed:', reason);
|
|
updateConfigStatus(false);
|
|
}
|
|
});
|
|
|
|
// Store subscription for cleanup
|
|
relayPool.currentSubscription = subscription;
|
|
|
|
console.log('SimplePool subscription established');
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('Configuration subscription failed:', error.message);
|
|
console.error('Configuration subscription failed:', error);
|
|
console.error('Error stack:', error.stack);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Process admin response events (kind 23457)
|
|
async function processAdminResponse(event) {
|
|
try {
|
|
console.log('=== PROCESSING ADMIN RESPONSE ===');
|
|
console.log('Response event:', event);
|
|
|
|
// Verify this is a kind 23457 admin response event
|
|
if (event.kind !== 23457) {
|
|
console.log('Ignoring non-admin response event, kind:', event.kind);
|
|
return;
|
|
}
|
|
|
|
// Verify the event is from the relay
|
|
const expectedRelayPubkey = getRelayPubkey();
|
|
if (event.pubkey !== expectedRelayPubkey) {
|
|
console.log('Ignoring response from unknown pubkey:', event.pubkey);
|
|
return;
|
|
}
|
|
|
|
// Decrypt the NIP-44 encrypted content
|
|
const decryptedContent = await decryptFromRelay(event.content);
|
|
if (!decryptedContent) {
|
|
throw new Error('Failed to decrypt admin response content');
|
|
}
|
|
|
|
console.log('Decrypted admin response:', decryptedContent);
|
|
|
|
// Parse the decrypted JSON response
|
|
const responseData = JSON.parse(decryptedContent);
|
|
console.log('Parsed response data:', responseData);
|
|
|
|
// Log the response for testing
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('RECV', `Decrypted response: ${JSON.stringify(responseData)}`, 'RESPONSE');
|
|
}
|
|
|
|
// Handle different types of admin responses
|
|
handleAdminResponseData(responseData);
|
|
|
|
} catch (error) {
|
|
console.error('Error processing admin response:', error);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('ERROR', `Failed to process admin response: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle different types of admin response data
|
|
function handleAdminResponseData(responseData) {
|
|
try {
|
|
console.log('=== HANDLING ADMIN RESPONSE DATA ===');
|
|
console.log('Response data:', responseData);
|
|
console.log('Response query_type:', responseData.query_type);
|
|
|
|
// Handle auth query responses - updated to match backend response types
|
|
if (responseData.query_type &&
|
|
(responseData.query_type.includes('auth_rules') ||
|
|
responseData.query_type.includes('auth'))) {
|
|
console.log('Routing to auth query handler');
|
|
handleAuthQueryResponse(responseData);
|
|
return;
|
|
}
|
|
|
|
// Handle config update responses specifically
|
|
if (responseData.query_type === 'config_update') {
|
|
console.log('Routing to config update handler');
|
|
handleConfigUpdateResponse(responseData);
|
|
return;
|
|
}
|
|
|
|
// Handle config query responses - updated to match backend response types
|
|
if (responseData.query_type &&
|
|
(responseData.query_type.includes('config') ||
|
|
responseData.query_type.startsWith('config_'))) {
|
|
console.log('Routing to config query handler');
|
|
handleConfigQueryResponse(responseData);
|
|
return;
|
|
}
|
|
|
|
// Handle system command responses
|
|
if (responseData.command) {
|
|
console.log('Routing to system command handler');
|
|
handleSystemCommandResponse(responseData);
|
|
return;
|
|
}
|
|
|
|
// Handle auth rule modification responses
|
|
if (responseData.operation || responseData.rules_processed !== undefined) {
|
|
console.log('Routing to auth rule modification handler');
|
|
handleAuthRuleResponse(responseData);
|
|
return;
|
|
}
|
|
|
|
// Generic response handling
|
|
console.log('Using generic response handler');
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('RECV', `Generic admin response: ${JSON.stringify(responseData)}`, 'RESPONSE');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error handling admin response data:', error);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('ERROR', `Failed to handle response data: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle config query responses
|
|
function handleConfigQueryResponse(responseData) {
|
|
console.log('=== CONFIG QUERY RESPONSE ===');
|
|
console.log('Query type:', responseData.query_type);
|
|
console.log('Total results:', responseData.total_results);
|
|
console.log('Data:', responseData.data);
|
|
|
|
// Convert the config response data to the format expected by displayConfiguration
|
|
if (responseData.data && responseData.data.length > 0) {
|
|
console.log('Converting config response to display format...');
|
|
|
|
// Create a synthetic event structure for displayConfiguration
|
|
const syntheticEvent = {
|
|
id: 'config_response_' + Date.now(),
|
|
pubkey: getRelayPubkey(),
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
kind: 'config_response',
|
|
content: 'Configuration from admin API',
|
|
tags: []
|
|
};
|
|
|
|
// Convert config data to tags format
|
|
responseData.data.forEach(config => {
|
|
const key = config.key || config.config_key;
|
|
const value = config.value || config.config_value;
|
|
if (key && value !== undefined) {
|
|
syntheticEvent.tags.push([key, value]);
|
|
}
|
|
});
|
|
|
|
console.log('Synthetic event created:', syntheticEvent);
|
|
console.log('Calling displayConfiguration with synthetic event...');
|
|
|
|
// Display the configuration using the original display function
|
|
displayConfiguration(syntheticEvent);
|
|
|
|
log(`Configuration loaded: ${responseData.total_results} parameters`, 'INFO');
|
|
} else {
|
|
console.log('No configuration data received');
|
|
updateConfigStatus(false);
|
|
}
|
|
|
|
// Also log to test interface for debugging
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('RECV', `Config query response: ${responseData.query_type}, ${responseData.total_results} results`, 'CONFIG_QUERY');
|
|
|
|
if (responseData.data && responseData.data.length > 0) {
|
|
logTestEvent('RECV', '=== CONFIGURATION VALUES ===', 'CONFIG');
|
|
responseData.data.forEach((config, index) => {
|
|
const key = config.key || config.config_key || `config_${index}`;
|
|
const value = config.value || config.config_value || 'undefined';
|
|
const category = config.category || 'general';
|
|
const dataType = config.data_type || 'string';
|
|
|
|
logTestEvent('RECV', `${key}: ${value} (${dataType}, ${category})`, 'CONFIG');
|
|
});
|
|
logTestEvent('RECV', '=== END CONFIGURATION VALUES ===', 'CONFIG');
|
|
} else {
|
|
logTestEvent('RECV', 'No configuration values found', 'CONFIG_QUERY');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle config update responses
|
|
function handleConfigUpdateResponse(responseData) {
|
|
console.log('=== CONFIG UPDATE RESPONSE ===');
|
|
console.log('Query type:', responseData.query_type);
|
|
console.log('Status:', responseData.status);
|
|
console.log('Data:', responseData.data);
|
|
|
|
if (responseData.status === 'success') {
|
|
const updatesApplied = responseData.updates_applied || 0;
|
|
log(`Configuration updated successfully: ${updatesApplied} parameters changed`, 'INFO');
|
|
|
|
// Show success message with details
|
|
if (responseData.data && Array.isArray(responseData.data)) {
|
|
responseData.data.forEach((config, index) => {
|
|
if (config.status === 'success') {
|
|
log(`✓ ${config.key}: ${config.value} (${config.data_type})`, 'INFO');
|
|
} else {
|
|
log(`✗ ${config.key}: ${config.error || 'Failed to update'}`, 'ERROR');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Automatically refresh configuration display after successful update
|
|
setTimeout(() => {
|
|
fetchConfiguration().catch(error => {
|
|
console.log('Auto-refresh configuration failed after update: ' + error.message);
|
|
});
|
|
}, 1000);
|
|
|
|
} else {
|
|
const errorMessage = responseData.message || responseData.error || 'Unknown error';
|
|
log(`Configuration update failed: ${errorMessage}`, 'ERROR');
|
|
|
|
// Show detailed error information if available
|
|
if (responseData.data && Array.isArray(responseData.data)) {
|
|
responseData.data.forEach((config, index) => {
|
|
if (config.status === 'error') {
|
|
log(`✗ ${config.key}: ${config.error || 'Failed to update'}`, 'ERROR');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Log to test interface for debugging
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('RECV', `Config update response: ${responseData.status}`, 'CONFIG_UPDATE');
|
|
|
|
if (responseData.data && responseData.data.length > 0) {
|
|
responseData.data.forEach((config, index) => {
|
|
const status = config.status === 'success' ? '✓' : '✗';
|
|
const message = config.status === 'success' ?
|
|
`${config.key} = ${config.value}` :
|
|
`${config.key}: ${config.error || 'Failed'}`;
|
|
logTestEvent('RECV', `${status} ${message}`, 'CONFIG_UPDATE');
|
|
});
|
|
} else {
|
|
logTestEvent('RECV', 'No configuration update details received', 'CONFIG_UPDATE');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle auth query responses
|
|
function handleAuthQueryResponse(responseData) {
|
|
console.log('=== AUTH QUERY RESPONSE ===');
|
|
console.log('Query type:', responseData.query_type);
|
|
console.log('Total results:', responseData.total_results);
|
|
console.log('Data:', responseData.data);
|
|
|
|
// Update the current auth rules with the response data
|
|
if (responseData.data && Array.isArray(responseData.data)) {
|
|
currentAuthRules = responseData.data;
|
|
console.log('Updated currentAuthRules with', currentAuthRules.length, 'rules');
|
|
|
|
// Always show the auth rules table when we receive data (no VIEW RULES button anymore)
|
|
console.log('Auto-showing auth rules table since we received data...');
|
|
showAuthRulesTable();
|
|
|
|
updateAuthRulesStatus('loaded');
|
|
log(`Loaded ${responseData.total_results} auth rules from relay`, 'INFO');
|
|
} else {
|
|
currentAuthRules = [];
|
|
console.log('No auth rules data received, cleared currentAuthRules');
|
|
|
|
// Show empty table (no VIEW RULES button anymore)
|
|
console.log('Auto-showing auth rules table with empty data...');
|
|
showAuthRulesTable();
|
|
|
|
updateAuthRulesStatus('loaded');
|
|
log('No auth rules found on relay', 'INFO');
|
|
}
|
|
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('RECV', `Auth query response: ${responseData.query_type}, ${responseData.total_results} results`, 'AUTH_QUERY');
|
|
|
|
if (responseData.data && responseData.data.length > 0) {
|
|
responseData.data.forEach((rule, index) => {
|
|
logTestEvent('RECV', `Rule ${index + 1}: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'AUTH_RULE');
|
|
});
|
|
} else {
|
|
logTestEvent('RECV', 'No auth rules found', 'AUTH_QUERY');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle system command responses
|
|
function handleSystemCommandResponse(responseData) {
|
|
console.log('=== SYSTEM COMMAND RESPONSE ===');
|
|
console.log('Command:', responseData.command);
|
|
console.log('Status:', responseData.status);
|
|
|
|
// Handle delete auth rule responses
|
|
if (responseData.command === 'delete_auth_rule') {
|
|
if (responseData.status === 'success') {
|
|
log('Auth rule deleted successfully', 'INFO');
|
|
// Refresh the auth rules display
|
|
loadAuthRules();
|
|
} else {
|
|
log(`Failed to delete auth rule: ${responseData.message || 'Unknown error'}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
// Handle clear all auth rules responses
|
|
if (responseData.command === 'clear_all_auth_rules') {
|
|
if (responseData.status === 'success') {
|
|
const rulesCleared = responseData.rules_cleared || 0;
|
|
log(`Successfully cleared ${rulesCleared} auth rules`, 'INFO');
|
|
// Clear local auth rules and refresh display
|
|
currentAuthRules = [];
|
|
displayAuthRules(currentAuthRules);
|
|
} else {
|
|
log(`Failed to clear auth rules: ${responseData.message || 'Unknown error'}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('RECV', `System command response: ${responseData.command} - ${responseData.status}`, 'SYSTEM_CMD');
|
|
}
|
|
}
|
|
|
|
// Handle auth rule modification responses
|
|
function handleAuthRuleResponse(responseData) {
|
|
console.log('=== AUTH RULE MODIFICATION RESPONSE ===');
|
|
console.log('Operation:', responseData.operation);
|
|
console.log('Status:', responseData.status);
|
|
|
|
// Handle auth rule addition/modification responses
|
|
if (responseData.status === 'success') {
|
|
const rulesProcessed = responseData.rules_processed || 0;
|
|
log(`Successfully processed ${rulesProcessed} auth rule modifications`, 'INFO');
|
|
|
|
// Refresh the auth rules display to show the new rules
|
|
if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') {
|
|
loadAuthRules();
|
|
}
|
|
} else {
|
|
log(`Failed to process auth rule modifications: ${responseData.message || 'Unknown error'}`, 'ERROR');
|
|
}
|
|
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('RECV', `Auth rule response: ${responseData.operation} - ${responseData.status}`, 'AUTH_RULE');
|
|
|
|
if (responseData.processed_rules) {
|
|
responseData.processed_rules.forEach((rule, index) => {
|
|
logTestEvent('RECV', `Processed rule ${index + 1}: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'AUTH_RULE');
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to decrypt content from relay using NIP-44
|
|
async function decryptFromRelay(encryptedContent) {
|
|
try {
|
|
console.log('Decrypting content from relay...');
|
|
|
|
// Get the relay public key for decryption
|
|
const relayPubkey = getRelayPubkey();
|
|
|
|
// Use NIP-07 extension's NIP-44 decrypt method
|
|
if (!window.nostr || !window.nostr.nip44) {
|
|
throw new Error('NIP-44 decryption not available via NIP-07 extension');
|
|
}
|
|
|
|
const decryptedContent = await window.nostr.nip44.decrypt(relayPubkey, encryptedContent);
|
|
|
|
if (!decryptedContent) {
|
|
throw new Error('NIP-44 decryption returned empty result');
|
|
}
|
|
|
|
console.log('Successfully decrypted content from relay');
|
|
return decryptedContent;
|
|
|
|
} catch (error) {
|
|
console.error('NIP-44 decryption failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Fetch configuration using admin API
|
|
async function fetchConfiguration() {
|
|
try {
|
|
console.log('=== FETCHING CONFIGURATION VIA ADMIN API ===');
|
|
|
|
// Require both login and relay connection
|
|
if (!isLoggedIn || !userPubkey) {
|
|
throw new Error('Must be logged in to fetch configuration');
|
|
}
|
|
|
|
if (!isRelayConnected || !relayPubkey) {
|
|
throw new Error('Must be connected to relay to fetch configuration. Please use the Relay Connection section first.');
|
|
}
|
|
|
|
// First establish subscription to receive responses
|
|
const subscriptionResult = await subscribeToConfiguration();
|
|
if (!subscriptionResult) {
|
|
throw new Error('Failed to establish admin response subscription');
|
|
}
|
|
|
|
// Wait a moment for subscription to be established
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
// Send config query command if logged in
|
|
if (isLoggedIn && userPubkey && relayPool) {
|
|
console.log('Sending config query command...');
|
|
|
|
// Create command array for getting configuration
|
|
const command_array = '["config_query", "all"]';
|
|
|
|
// Encrypt the command content using NIP-44
|
|
const encrypted_content = await encryptForRelay(command_array);
|
|
if (!encrypted_content) {
|
|
throw new Error('Failed to encrypt config query command');
|
|
}
|
|
|
|
// Create kind 23456 admin event
|
|
const configEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["p", getRelayPubkey()]
|
|
],
|
|
content: encrypted_content
|
|
};
|
|
|
|
// Sign the event
|
|
const signedEvent = await window.nostr.signEvent(configEvent);
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed');
|
|
}
|
|
|
|
console.log('Config query event signed, publishing...');
|
|
|
|
// Publish via SimplePool with detailed error diagnostics
|
|
const url = relayConnectionUrl.value.trim();
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
console.log(`✅ Relay ${index} (${url}): Event published successfully`);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('INFO', `Relay ${index} publish success`, 'PUBLISH');
|
|
}
|
|
} else {
|
|
console.error(`❌ Relay ${index} (${url}): Publish failed:`, result.reason);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('ERROR', `Relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected the event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
console.log('Config query command sent successfully - waiting for response...');
|
|
|
|
} else {
|
|
console.log('Not logged in - only subscription established for testing');
|
|
}
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('Failed to fetch configuration:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
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 (aligned with README.md)
|
|
const fieldTypes = {
|
|
'auth_enabled': 'boolean',
|
|
'nip42_auth_required': 'boolean',
|
|
'nip40_expiration_enabled': 'boolean',
|
|
'max_connections': 'number',
|
|
'pow_min_difficulty': 'number',
|
|
'nip42_challenge_timeout': 'number',
|
|
'max_subscriptions_per_client': 'number',
|
|
'max_event_tags': 'number',
|
|
'max_content_length': 'number'
|
|
};
|
|
|
|
const descriptions = {
|
|
'relay_pubkey': 'Relay Public Key (Read-only)',
|
|
'auth_enabled': 'Enable Authentication',
|
|
'nip42_auth_required': 'Enable NIP-42 Cryptographic Authentication',
|
|
'nip42_auth_required_kinds': 'Event Kinds Requiring NIP-42 Auth',
|
|
'nip42_challenge_timeout': 'NIP-42 Challenge Expiration Seconds',
|
|
'max_connections': 'Maximum Connections',
|
|
'relay_description': 'Relay Description',
|
|
'relay_contact': 'Relay Contact',
|
|
'pow_min_difficulty': 'Minimum Proof-of-Work Difficulty',
|
|
'nip40_expiration_enabled': 'Enable Event Expiration',
|
|
'max_subscriptions_per_client': 'Max Subscriptions per Client',
|
|
'max_event_tags': 'Maximum Tags per Event',
|
|
'max_content_length': 'Maximum Event Content Length'
|
|
};
|
|
|
|
// 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 configuration update command...');
|
|
|
|
// Collect form data
|
|
const formInputs = configForm.querySelectorAll('input, select');
|
|
const configObjects = [];
|
|
|
|
// Process each form input as a config object
|
|
formInputs.forEach(input => {
|
|
if (!input.disabled && input.name && input.name !== 'd' && input.name !== 'relay_pubkey') {
|
|
// Determine data type based on input type
|
|
let dataType = 'string';
|
|
if (input.type === 'number') {
|
|
dataType = 'integer';
|
|
} else if (input.tagName === 'SELECT' && (input.value === 'true' || input.value === 'false')) {
|
|
dataType = 'boolean';
|
|
}
|
|
|
|
// Determine category based on key name
|
|
let category = 'general';
|
|
const key = input.name;
|
|
if (key.startsWith('relay_')) {
|
|
category = 'relay';
|
|
} else if (key.startsWith('nip40_')) {
|
|
category = 'expiration';
|
|
} else if (key.startsWith('nip42_') || key.startsWith('auth_')) {
|
|
category = 'authentication';
|
|
} else if (key.startsWith('pow_')) {
|
|
category = 'proof_of_work';
|
|
} else if (key.startsWith('max_')) {
|
|
category = 'limits';
|
|
}
|
|
|
|
configObjects.push({
|
|
key: key,
|
|
value: input.value,
|
|
data_type: dataType,
|
|
category: category
|
|
});
|
|
}
|
|
});
|
|
|
|
if (configObjects.length === 0) {
|
|
console.log('No configuration changes to save');
|
|
return;
|
|
}
|
|
|
|
console.log(`Sending config_update command with ${configObjects.length} configuration objects...`);
|
|
|
|
// Send single config_update command with all config objects
|
|
await sendConfigUpdateCommand(configObjects);
|
|
|
|
console.log('Configuration update command sent successfully');
|
|
|
|
// Exit edit mode
|
|
exitEditMode();
|
|
|
|
} catch (error) {
|
|
console.log('Configuration save failed: ' + error.message);
|
|
console.error('Save configuration error:', error);
|
|
}
|
|
}
|
|
|
|
// Send config update command using kind 23456 with new config_update format
|
|
async function sendConfigUpdateCommand(configObjects) {
|
|
try {
|
|
if (!relayPool) {
|
|
throw new Error('SimplePool connection not available');
|
|
}
|
|
|
|
console.log(`Sending config_update command with ${configObjects.length} configuration objects`);
|
|
|
|
// Create command array for config update (per README.md spec)
|
|
// Format: ["config_update", [config_objects_array]]
|
|
const command_array = JSON.stringify(["config_update", configObjects]);
|
|
|
|
// Encrypt using NIP-44
|
|
const encrypted_content = await encryptForRelay(command_array);
|
|
if (!encrypted_content) {
|
|
throw new Error('Failed to encrypt config_update command');
|
|
}
|
|
|
|
// Create kind 23456 admin event (unified admin API)
|
|
const configEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [["p", getRelayPubkey()]], // Per README.md spec
|
|
content: encrypted_content
|
|
};
|
|
|
|
// Sign the event
|
|
const signedEvent = await window.nostr.signEvent(configEvent);
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed');
|
|
}
|
|
|
|
console.log(`Config update event signed with ${configObjects.length} objects`);
|
|
|
|
// Publish via SimplePool with detailed error diagnostics
|
|
const url = relayConnectionUrl.value.trim();
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
console.log(`✅ Config Update Relay ${index} (${url}): Event published successfully`);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('INFO', `Config update relay ${index} publish success`, 'PUBLISH');
|
|
}
|
|
} else {
|
|
console.error(`❌ Config Update Relay ${index} (${url}): Publish failed:`, result.reason);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('ERROR', `Config update relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected config update event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
console.log(`Config update command sent successfully with ${configObjects.length} configuration objects`);
|
|
|
|
// Log for testing
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('SENT', `Config update command: ${configObjects.length} objects`, 'CONFIG_UPDATE');
|
|
configObjects.forEach((config, index) => {
|
|
logTestEvent('SENT', `Config ${index + 1}: ${config.key} = ${config.value} (${config.data_type})`, 'CONFIG');
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`Failed to send config_update command:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Login/Logout button functionality
|
|
function updateLoginLogoutButton() {
|
|
const loginLogoutBtn = document.getElementById('login-logout-btn');
|
|
if (!loginLogoutBtn) return;
|
|
|
|
if (isLoggedIn) {
|
|
loginLogoutBtn.textContent = 'LOGOUT';
|
|
loginLogoutBtn.className = 'login-logout-btn logout-state';
|
|
loginLogoutBtn.onclick = logout;
|
|
// Show user details when logged in
|
|
if (persistentUserDetails) {
|
|
persistentUserDetails.style.display = 'block';
|
|
}
|
|
} else {
|
|
loginLogoutBtn.textContent = 'ADMIN NOSTR LOGIN';
|
|
loginLogoutBtn.className = 'login-logout-btn';
|
|
loginLogoutBtn.onclick = () => {
|
|
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.launch) {
|
|
window.NOSTR_LOGIN_LITE.launch('login');
|
|
} else {
|
|
console.log('NOSTR_LOGIN_LITE not available');
|
|
}
|
|
};
|
|
// Hide user details when logged out
|
|
if (persistentUserDetails) {
|
|
persistentUserDetails.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Event handlers
|
|
// Initialize login/logout button
|
|
updateLoginLogoutButton();
|
|
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();
|
|
});
|
|
|
|
// Relay connection event handlers
|
|
connectRelayBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
connectToRelay().catch(error => {
|
|
console.log('Relay connection failed: ' + error.message);
|
|
});
|
|
});
|
|
|
|
disconnectRelayBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
disconnectFromRelay();
|
|
});
|
|
|
|
testWebSocketBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const url = relayConnectionUrl.value.trim();
|
|
if (!url) {
|
|
log('Please enter a relay URL first', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
testWebSocketConnection(url)
|
|
.then(() => {
|
|
log('WebSocket test successful', 'INFO');
|
|
})
|
|
.catch(error => {
|
|
log(`WebSocket test failed: ${error.message}`, 'ERROR');
|
|
});
|
|
});
|
|
|
|
// ================================
|
|
// AUTH RULES MANAGEMENT FUNCTIONS
|
|
// ================================
|
|
|
|
// Global auth rules state
|
|
let currentAuthRules = [];
|
|
let editingAuthRule = null;
|
|
|
|
// DOM elements for auth rules
|
|
const authRulesSection = document.getElementById('authRulesSection');
|
|
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');
|
|
|
|
// 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';
|
|
|
|
// Add null checks for all elements
|
|
if (authRulesTableContainer) {
|
|
authRulesTableContainer.style.display = 'none';
|
|
}
|
|
if (authRuleFormContainer) {
|
|
authRuleFormContainer.style.display = 'none';
|
|
}
|
|
|
|
currentAuthRules = [];
|
|
editingAuthRule = null;
|
|
log('Auth rules section hidden', 'INFO');
|
|
}
|
|
}
|
|
|
|
// Update auth rules status indicator (removed - no status element)
|
|
function updateAuthRulesStatus(status) {
|
|
// Status element removed - no-op
|
|
}
|
|
|
|
// Load auth rules from relay using admin API
|
|
async function loadAuthRules() {
|
|
try {
|
|
log('Loading auth rules via admin API...', 'INFO');
|
|
updateAuthRulesStatus('loading');
|
|
|
|
if (!isLoggedIn || !userPubkey) {
|
|
throw new Error('Must be logged in to load auth rules');
|
|
}
|
|
|
|
if (!relayPool) {
|
|
throw new Error('SimplePool connection not available');
|
|
}
|
|
|
|
// Create command array for getting all auth rules
|
|
const command_array = '["auth_query", "all"]';
|
|
|
|
// Encrypt the command content using NIP-44
|
|
const encrypted_content = await encryptForRelay(command_array);
|
|
if (!encrypted_content) {
|
|
throw new Error('Failed to encrypt auth query command');
|
|
}
|
|
|
|
// Create kind 23456 admin event
|
|
const authEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["p", getRelayPubkey()]
|
|
],
|
|
content: encrypted_content
|
|
};
|
|
|
|
// Sign the event
|
|
const signedEvent = await window.nostr.signEvent(authEvent);
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed');
|
|
}
|
|
|
|
log('Sending auth rules query to relay...', 'INFO');
|
|
|
|
// Publish via SimplePool with detailed error diagnostics
|
|
const url = relayConnectionUrl.value.trim();
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
console.log(`✅ Auth Rules Query Relay ${index} (${url}): Event published successfully`);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('INFO', `Auth rules query relay ${index} publish success`, 'PUBLISH');
|
|
}
|
|
} else {
|
|
console.error(`❌ Auth Rules Query Relay ${index} (${url}): Publish failed:`, result.reason);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('ERROR', `Auth rules query relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected auth rules query event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
log('Auth rules query sent successfully - waiting for response...', 'INFO');
|
|
updateAuthRulesStatus('loaded');
|
|
|
|
} catch (error) {
|
|
log(`Failed to load auth rules: ${error.message}`, 'ERROR');
|
|
updateAuthRulesStatus('error');
|
|
currentAuthRules = [];
|
|
displayAuthRules(currentAuthRules);
|
|
}
|
|
}
|
|
|
|
// Display auth rules in the table
|
|
function displayAuthRules(rules) {
|
|
console.log('=== DISPLAY AUTH RULES DEBUG ===');
|
|
console.log('authRulesTableBody element:', authRulesTableBody);
|
|
console.log('Rules to display:', rules);
|
|
console.log('Rules length:', rules ? rules.length : 'undefined');
|
|
console.log('authRulesTableContainer display:', authRulesTableContainer ? authRulesTableContainer.style.display : 'element not found');
|
|
|
|
if (!authRulesTableBody) {
|
|
console.log('ERROR: authRulesTableBody element not found');
|
|
return;
|
|
}
|
|
|
|
authRulesTableBody.innerHTML = '';
|
|
console.log('Cleared existing table content');
|
|
|
|
if (!rules || rules.length === 0) {
|
|
console.log('No rules to display, showing empty message');
|
|
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);
|
|
console.log('Added empty rules message row');
|
|
return;
|
|
}
|
|
|
|
console.log(`Displaying ${rules.length} auth rules`);
|
|
rules.forEach((rule, index) => {
|
|
console.log(`Adding rule ${index + 1}:`, rule);
|
|
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
|
|
console.log(`Total Rules: ${rules.length}, Active Rules: ${rules.filter(r => r.enabled !== false).length}`);
|
|
|
|
console.log('=== END DISPLAY AUTH RULES DEBUG ===');
|
|
}
|
|
|
|
// Show auth rules table (automatically called when auth rules are loaded)
|
|
function showAuthRulesTable() {
|
|
console.log('=== SHOW AUTH RULES TABLE DEBUG ===');
|
|
console.log('authRulesTableContainer element:', authRulesTableContainer);
|
|
console.log('Current display style:', authRulesTableContainer ? authRulesTableContainer.style.display : 'element not found');
|
|
|
|
if (authRulesTableContainer) {
|
|
authRulesTableContainer.style.display = 'block';
|
|
console.log('Set authRulesTableContainer display to block');
|
|
|
|
// If we already have cached auth rules, display them immediately
|
|
if (currentAuthRules && currentAuthRules.length >= 0) {
|
|
console.log('Displaying cached auth rules:', currentAuthRules.length, 'rules');
|
|
displayAuthRules(currentAuthRules);
|
|
updateAuthRulesStatus('loaded');
|
|
log(`Auth rules table displayed with ${currentAuthRules.length} cached rules`, 'INFO');
|
|
} else {
|
|
// No cached rules, load from relay
|
|
console.log('No cached auth rules, loading from relay...');
|
|
loadAuthRules();
|
|
log('Auth rules table displayed - loading from relay', 'INFO');
|
|
}
|
|
} else {
|
|
console.log('ERROR: authRulesTableContainer element not found');
|
|
}
|
|
console.log('=== END SHOW AUTH RULES TABLE DEBUG ===');
|
|
}
|
|
|
|
// 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 using admin API
|
|
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');
|
|
|
|
if (!isLoggedIn || !userPubkey) {
|
|
throw new Error('Must be logged in to delete auth rules');
|
|
}
|
|
|
|
if (!relayPool) {
|
|
throw new Error('SimplePool connection not available');
|
|
}
|
|
|
|
// Create command array for deleting auth rule
|
|
// Format: ["system_command", "delete_auth_rule", rule_type, pattern_type, pattern_value]
|
|
const rule_type = rule.rule_type;
|
|
const pattern_type = rule.pattern_type || 'pubkey';
|
|
const pattern_value = rule.pattern_value || rule.rule_target;
|
|
|
|
const command_array = `["system_command", "delete_auth_rule", "${rule_type}", "${pattern_type}", "${pattern_value}"]`;
|
|
|
|
// Encrypt the command content using NIP-44
|
|
const encrypted_content = await encryptForRelay(command_array);
|
|
if (!encrypted_content) {
|
|
throw new Error('Failed to encrypt delete auth rule command');
|
|
}
|
|
|
|
// Create kind 23456 admin event
|
|
const authEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["p", getRelayPubkey()]
|
|
],
|
|
content: encrypted_content
|
|
};
|
|
|
|
// Sign the event
|
|
const signedEvent = await window.nostr.signEvent(authEvent);
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed');
|
|
}
|
|
|
|
log('Sending delete auth rule command to relay...', 'INFO');
|
|
|
|
// Publish via SimplePool with detailed error diagnostics
|
|
const url = relayConnectionUrl.value.trim();
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
console.log(`✅ Delete Auth Rule Relay ${index} (${url}): Event published successfully`);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('INFO', `Delete auth rule relay ${index} publish success`, 'PUBLISH');
|
|
}
|
|
} else {
|
|
console.error(`❌ Delete Auth Rule Relay ${index} (${url}): Publish failed:`, result.reason);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('ERROR', `Delete auth rule relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected delete auth rule event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
log('Delete auth rule command sent successfully - waiting for response...', 'INFO');
|
|
|
|
// Remove from local array immediately for UI responsiveness
|
|
currentAuthRules.splice(index, 1);
|
|
displayAuthRules(currentAuthRules);
|
|
|
|
} 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 (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 (updated to use combined input)
|
|
function addBlacklistRule() {
|
|
const input = document.getElementById('authRulePubkey');
|
|
|
|
if (!input) return;
|
|
|
|
const inputValue = input.value.trim();
|
|
if (!inputValue) {
|
|
log('Please enter a pubkey or nsec', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
// Convert nsec to hex if needed
|
|
const hexPubkey = nsecToHex(inputValue);
|
|
if (!hexPubkey) {
|
|
log('Invalid pubkey format. Please enter nsec1... or 64-character hex', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
// Validate hex length
|
|
if (hexPubkey.length !== 64) {
|
|
log('Invalid pubkey length. Must be exactly 64 characters', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
log('Adding to blacklist...', 'INFO');
|
|
|
|
// 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(() => {
|
|
log(`Pubkey ${hexPubkey.substring(0, 16)}... added to blacklist`, 'INFO');
|
|
input.value = '';
|
|
|
|
// Refresh auth rules display if visible
|
|
if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') {
|
|
loadAuthRules();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
log(`Failed to add rule: ${error.message}`, 'ERROR');
|
|
});
|
|
}
|
|
|
|
// Add whitelist rule (updated to use combined input)
|
|
function addWhitelistRule() {
|
|
const input = document.getElementById('authRulePubkey');
|
|
const warningDiv = document.getElementById('whitelistWarning');
|
|
|
|
if (!input) return;
|
|
|
|
const inputValue = input.value.trim();
|
|
if (!inputValue) {
|
|
log('Please enter a pubkey or nsec', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
// Convert nsec to hex if needed
|
|
const hexPubkey = nsecToHex(inputValue);
|
|
if (!hexPubkey) {
|
|
log('Invalid pubkey format. Please enter nsec1... or 64-character hex', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
// Validate hex length
|
|
if (hexPubkey.length !== 64) {
|
|
log('Invalid pubkey length. Must be exactly 64 characters', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
// Show whitelist warning
|
|
if (warningDiv) {
|
|
warningDiv.style.display = 'block';
|
|
}
|
|
|
|
log('Adding to whitelist...', 'INFO');
|
|
|
|
// 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(() => {
|
|
log(`Pubkey ${hexPubkey.substring(0, 16)}... added to whitelist`, 'INFO');
|
|
input.value = '';
|
|
|
|
// Refresh auth rules display if visible
|
|
if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') {
|
|
loadAuthRules();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
log(`Failed to add rule: ${error.message}`, 'ERROR');
|
|
});
|
|
}
|
|
|
|
// Add auth rule via SimplePool (kind 23456 event) - FIXED to match working test pattern
|
|
async function addAuthRuleViaWebSocket(ruleData) {
|
|
if (!isLoggedIn || !userPubkey) {
|
|
throw new Error('Must be logged in to add auth rules');
|
|
}
|
|
|
|
if (!relayPool) {
|
|
throw new Error('SimplePool connection not available');
|
|
}
|
|
|
|
try {
|
|
log(`Adding auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value.substring(0, 16)}...`, 'INFO');
|
|
|
|
// Map client-side rule types to command array format (matching working tests)
|
|
let commandRuleType, commandPatternType;
|
|
|
|
switch (ruleData.rule_type) {
|
|
case 'pubkey_blacklist':
|
|
commandRuleType = 'blacklist';
|
|
commandPatternType = 'pubkey';
|
|
break;
|
|
case 'pubkey_whitelist':
|
|
commandRuleType = 'whitelist';
|
|
commandPatternType = 'pubkey';
|
|
break;
|
|
case 'hash_blacklist':
|
|
commandRuleType = 'blacklist';
|
|
commandPatternType = 'hash';
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown rule type: ${ruleData.rule_type}`);
|
|
}
|
|
|
|
// Create command array in the same format as working tests
|
|
// Format: ["blacklist", "pubkey", "abc123..."] or ["whitelist", "pubkey", "def456..."]
|
|
const command_array = `["${commandRuleType}", "${commandPatternType}", "${ruleData.pattern_value}"]`;
|
|
|
|
// Encrypt the command content using NIP-44 (same as working tests)
|
|
const encrypted_content = await encryptForRelay(command_array);
|
|
if (!encrypted_content) {
|
|
throw new Error('Failed to encrypt auth rule command');
|
|
}
|
|
|
|
// Create kind 23456 admin event with encrypted content (same as working tests)
|
|
const authEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["p", getRelayPubkey()]
|
|
],
|
|
content: encrypted_content
|
|
};
|
|
|
|
// DEBUG: Log the complete event structure being sent
|
|
console.log('=== AUTH RULE EVENT DEBUG (FIXED FORMAT) ===');
|
|
console.log('Original Rule Data:', ruleData);
|
|
console.log('Command Array:', command_array);
|
|
console.log('Encrypted Content:', encrypted_content.substring(0, 50) + '...');
|
|
console.log('Auth Event (before signing):', JSON.stringify(authEvent, null, 2));
|
|
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');
|
|
}
|
|
|
|
// Publish via SimplePool with detailed error diagnostics
|
|
const url = relayConnectionUrl.value.trim();
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
console.log(`✅ Add Auth Rule Relay ${index} (${url}): Event published successfully`);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('INFO', `Add auth rule relay ${index} publish success`, 'PUBLISH');
|
|
}
|
|
} else {
|
|
console.error(`❌ Add Auth Rule Relay ${index} (${url}): Publish failed:`, result.reason);
|
|
if (typeof logTestEvent === 'function') {
|
|
logTestEvent('ERROR', `Add auth rule relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected add auth rule event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
log('Auth rule added successfully', 'INFO');
|
|
|
|
} catch (error) {
|
|
log(`Failed to add auth rule: ${error.message}`, 'ERROR');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ================================
|
|
// TEST FUNCTIONS FOR ADMIN API
|
|
// ================================
|
|
|
|
// Test event logging function
|
|
function logTestEvent(direction, message, type = 'INFO') {
|
|
const testLog = document.getElementById('test-event-log');
|
|
if (!testLog) return;
|
|
|
|
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
|
|
const logEntry = document.createElement('div');
|
|
logEntry.className = 'log-entry';
|
|
|
|
const directionColor = direction === 'SENT' ? '#007bff' : '#28a745';
|
|
logEntry.innerHTML = `
|
|
<span class="log-timestamp">${timestamp}</span>
|
|
<span style="color: ${directionColor}; font-weight: bold;">[${direction}]</span>
|
|
<span style="color: #666;">[${type}]</span>
|
|
${message}
|
|
`;
|
|
|
|
testLog.appendChild(logEntry);
|
|
testLog.scrollTop = testLog.scrollHeight;
|
|
}
|
|
|
|
// Test: Get Auth Rules
|
|
async function testGetAuthRules() {
|
|
if (!isLoggedIn || !userPubkey) {
|
|
logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
if (!relayPool) {
|
|
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logTestEvent('INFO', 'Testing Get Auth Rules command...', 'TEST');
|
|
|
|
// Create command array for getting auth rules
|
|
const command_array = '["auth_query", "all"]';
|
|
|
|
// Encrypt the command content using NIP-44
|
|
const encrypted_content = await encryptForRelay(command_array);
|
|
if (!encrypted_content) {
|
|
throw new Error('Failed to encrypt auth query command');
|
|
}
|
|
|
|
// Create kind 23456 admin event
|
|
const authEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["p", getRelayPubkey()]
|
|
],
|
|
content: encrypted_content
|
|
};
|
|
|
|
// Sign the event
|
|
const signedEvent = await window.nostr.signEvent(authEvent);
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed');
|
|
}
|
|
|
|
logTestEvent('SENT', `Get Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
|
|
|
// Publish via SimplePool with detailed error diagnostics
|
|
const url = relayConnectionUrl.value.trim();
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
logTestEvent('INFO', `Test Add Blacklist relay ${index} publish success`, 'PUBLISH');
|
|
} else {
|
|
logTestEvent('ERROR', `Test Add Blacklist relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected test add blacklist event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
logTestEvent('INFO', 'Get Auth Rules command sent successfully', 'SUCCESS');
|
|
|
|
} catch (error) {
|
|
logTestEvent('ERROR', `Get Auth Rules test failed: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
// Test: Clear All Auth Rules
|
|
async function testClearAuthRules() {
|
|
if (!isLoggedIn || !userPubkey) {
|
|
logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
if (!relayPool) {
|
|
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logTestEvent('INFO', 'Testing Clear All Auth Rules command...', 'TEST');
|
|
|
|
// Create command array for clearing auth rules
|
|
const command_array = '["system_command", "clear_all_auth_rules"]';
|
|
|
|
// Encrypt the command content using NIP-44
|
|
const encrypted_content = await encryptForRelay(command_array);
|
|
if (!encrypted_content) {
|
|
throw new Error('Failed to encrypt clear auth rules command');
|
|
}
|
|
|
|
// Create kind 23456 admin event
|
|
const authEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["p", getRelayPubkey()]
|
|
],
|
|
content: encrypted_content
|
|
};
|
|
|
|
// Sign the event
|
|
const signedEvent = await window.nostr.signEvent(authEvent);
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed');
|
|
}
|
|
|
|
logTestEvent('SENT', `Clear Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
|
|
|
// Publish via SimplePool with detailed error diagnostics
|
|
const url = relayConnectionUrl.value.trim();
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
logTestEvent('INFO', `Test Add Whitelist relay ${index} publish success`, 'PUBLISH');
|
|
} else {
|
|
logTestEvent('ERROR', `Test Add Whitelist relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected test add whitelist event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
logTestEvent('INFO', 'Clear Auth Rules command sent successfully', 'SUCCESS');
|
|
|
|
} catch (error) {
|
|
logTestEvent('ERROR', `Clear Auth Rules test failed: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
// Test: Add Blacklist
|
|
async function testAddBlacklist() {
|
|
const testPubkeyInput = document.getElementById('test-pubkey-input');
|
|
let testPubkey = testPubkeyInput ? testPubkeyInput.value.trim() : '';
|
|
|
|
// Use a default test pubkey if none provided
|
|
if (!testPubkey) {
|
|
testPubkey = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
|
|
logTestEvent('INFO', `Using default test pubkey: ${testPubkey}`, 'INFO');
|
|
}
|
|
|
|
if (!isLoggedIn || !userPubkey) {
|
|
logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
if (!relayPool) {
|
|
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logTestEvent('INFO', `Testing Add Blacklist for pubkey: ${testPubkey.substring(0, 16)}...`, 'TEST');
|
|
|
|
// Create command array for adding blacklist rule
|
|
const command_array = `["blacklist", "pubkey", "${testPubkey}"]`;
|
|
|
|
// Encrypt the command content using NIP-44
|
|
const encrypted_content = await encryptForRelay(command_array);
|
|
if (!encrypted_content) {
|
|
throw new Error('Failed to encrypt blacklist command');
|
|
}
|
|
|
|
// Create kind 23456 admin event
|
|
const authEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["p", getRelayPubkey()]
|
|
],
|
|
content: encrypted_content
|
|
};
|
|
|
|
// Sign the event
|
|
const signedEvent = await window.nostr.signEvent(authEvent);
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed');
|
|
}
|
|
|
|
logTestEvent('SENT', `Add Blacklist event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
|
|
|
// Publish via SimplePool with detailed error diagnostics
|
|
const url = relayConnectionUrl.value.trim();
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
logTestEvent('INFO', `Test Config Query relay ${index} publish success`, 'PUBLISH');
|
|
} else {
|
|
logTestEvent('ERROR', `Test Config Query relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected test config query event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
logTestEvent('INFO', 'Add Blacklist command sent successfully', 'SUCCESS');
|
|
|
|
} catch (error) {
|
|
logTestEvent('ERROR', `Add Blacklist test failed: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
// Test: Add Whitelist
|
|
async function testAddWhitelist() {
|
|
const testPubkeyInput = document.getElementById('test-pubkey-input');
|
|
let testPubkey = testPubkeyInput ? testPubkeyInput.value.trim() : '';
|
|
|
|
// Use a default test pubkey if none provided
|
|
if (!testPubkey) {
|
|
testPubkey = 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890';
|
|
logTestEvent('INFO', `Using default test pubkey: ${testPubkey}`, 'INFO');
|
|
}
|
|
|
|
if (!isLoggedIn || !userPubkey) {
|
|
logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
if (!relayPool) {
|
|
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logTestEvent('INFO', `Testing Add Whitelist for pubkey: ${testPubkey.substring(0, 16)}...`, 'TEST');
|
|
|
|
// Create command array for adding whitelist rule
|
|
const command_array = `["whitelist", "pubkey", "${testPubkey}"]`;
|
|
|
|
// Encrypt the command content using NIP-44
|
|
const encrypted_content = await encryptForRelay(command_array);
|
|
if (!encrypted_content) {
|
|
throw new Error('Failed to encrypt whitelist command');
|
|
}
|
|
|
|
// Create kind 23456 admin event
|
|
const authEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["p", getRelayPubkey()]
|
|
],
|
|
content: encrypted_content
|
|
};
|
|
|
|
// Sign the event
|
|
const signedEvent = await window.nostr.signEvent(authEvent);
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed');
|
|
}
|
|
|
|
logTestEvent('SENT', `Add Whitelist event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
|
|
|
// Publish via SimplePool
|
|
const url = relayConnectionUrl.value.trim();
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
logTestEvent('INFO', `Test Post Event relay ${index} publish success`, 'PUBLISH');
|
|
} else {
|
|
logTestEvent('ERROR', `Test Post Event relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected test post event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
logTestEvent('INFO', 'Add Whitelist command sent successfully', 'SUCCESS');
|
|
|
|
} catch (error) {
|
|
logTestEvent('ERROR', `Add Whitelist test failed: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
// Test: Config Query
|
|
async function testConfigQuery() {
|
|
if (!isLoggedIn || !userPubkey) {
|
|
logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
if (!relayPool) {
|
|
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logTestEvent('INFO', 'Testing Config Query command...', 'TEST');
|
|
|
|
// Create command array for getting configuration
|
|
const command_array = '["config_query", "all"]';
|
|
|
|
// Encrypt the command content using NIP-44
|
|
const encrypted_content = await encryptForRelay(command_array);
|
|
if (!encrypted_content) {
|
|
throw new Error('Failed to encrypt config query command');
|
|
}
|
|
|
|
// Create kind 23456 admin event
|
|
const configEvent = {
|
|
kind: 23456,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["p", getRelayPubkey()]
|
|
],
|
|
content: encrypted_content
|
|
};
|
|
|
|
// Sign the event
|
|
const signedEvent = await window.nostr.signEvent(configEvent);
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed');
|
|
}
|
|
|
|
logTestEvent('SENT', `Config Query event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
|
|
|
// Publish via SimplePool with detailed error diagnostics
|
|
const url = relayUrl.value.trim();
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
logTestEvent('INFO', `Test Config Query relay ${index} publish success`, 'PUBLISH');
|
|
} else {
|
|
logTestEvent('ERROR', `Test Config Query relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected test config query event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
logTestEvent('INFO', 'Config Query command sent successfully', 'SUCCESS');
|
|
|
|
} catch (error) {
|
|
logTestEvent('ERROR', `Config Query test failed: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
// Test: Post Basic Event
|
|
async function testPostEvent() {
|
|
if (!isLoggedIn || !userPubkey) {
|
|
logTestEvent('ERROR', 'Must be logged in to test event posting', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
if (!relayPool) {
|
|
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logTestEvent('INFO', 'Testing basic event posting...', 'TEST');
|
|
|
|
// Create a simple kind 1 text note event
|
|
const testEvent = {
|
|
kind: 1,
|
|
pubkey: userPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["t", "test"],
|
|
["client", "c-relay-admin-api"]
|
|
],
|
|
content: `Test event from C-Relay Admin API at ${new Date().toISOString()}`
|
|
};
|
|
|
|
logTestEvent('SENT', `Test event (before signing): ${JSON.stringify(testEvent)}`, 'EVENT');
|
|
|
|
// Sign the event using NIP-07
|
|
const signedEvent = await window.nostr.signEvent(testEvent);
|
|
if (!signedEvent || !signedEvent.sig) {
|
|
throw new Error('Event signing failed');
|
|
}
|
|
|
|
logTestEvent('SENT', `Signed test event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
|
|
|
// Publish via SimplePool to the same relay with detailed error diagnostics
|
|
const url = relayConnectionUrl.value.trim();
|
|
logTestEvent('INFO', `Publishing to relay: ${url}`, 'INFO');
|
|
|
|
const publishPromises = relayPool.publish([url], signedEvent);
|
|
|
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
|
const results = await Promise.allSettled(publishPromises);
|
|
|
|
// Log detailed publish results for diagnostics
|
|
let successCount = 0;
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
successCount++;
|
|
logTestEvent('INFO', `Test Post Event relay ${index} publish success`, 'PUBLISH');
|
|
} else {
|
|
logTestEvent('ERROR', `Test Post Event relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH');
|
|
}
|
|
});
|
|
|
|
// Throw error if all relays failed
|
|
if (successCount === 0) {
|
|
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
throw new Error(`All relays rejected test post event. Details: ${errorDetails}`);
|
|
}
|
|
|
|
logTestEvent('INFO', 'Test event published successfully!', 'SUCCESS');
|
|
logTestEvent('INFO', 'Check if the event appears in the subscription above...', 'INFO');
|
|
|
|
} catch (error) {
|
|
logTestEvent('ERROR', `Post Event test failed: ${error.message}`, 'ERROR');
|
|
console.error('Post Event test error:', error);
|
|
}
|
|
}
|
|
|
|
// Helper function to encrypt content for relay using NIP-44
|
|
async function encryptForRelay(content) {
|
|
try {
|
|
logTestEvent('INFO', `Encrypting content: ${content}`, 'DEBUG');
|
|
|
|
// Get the relay public key for encryption
|
|
const relayPubkey = getRelayPubkey();
|
|
|
|
// Check if we have access to NIP-44 encryption via nostr-tools
|
|
if (!window.NostrTools || !window.NostrTools.nip44) {
|
|
throw new Error('NIP-44 encryption not available - nostr-tools library missing');
|
|
}
|
|
|
|
// Get user's private key for encryption
|
|
// We need to use the NIP-07 extension to get the private key
|
|
if (!window.nostr || !window.nostr.nip44) {
|
|
throw new Error('NIP-44 encryption not available via NIP-07 extension');
|
|
}
|
|
|
|
// Use NIP-07 extension's NIP-44 encrypt method
|
|
const encrypted_content = await window.nostr.nip44.encrypt(relayPubkey, content);
|
|
|
|
if (!encrypted_content) {
|
|
throw new Error('NIP-44 encryption returned empty result');
|
|
}
|
|
|
|
logTestEvent('INFO', `Successfully encrypted content using NIP-44`, 'DEBUG');
|
|
logTestEvent('INFO', `Encrypted content: ${encrypted_content.substring(0, 50)}...`, 'DEBUG');
|
|
|
|
return encrypted_content;
|
|
} catch (error) {
|
|
logTestEvent('ERROR', `NIP-44 encryption failed: ${error.message}`, 'ERROR');
|
|
|
|
// Fallback: Try using nostr-tools directly if NIP-07 fails
|
|
try {
|
|
logTestEvent('INFO', 'Attempting fallback encryption with nostr-tools...', 'DEBUG');
|
|
|
|
if (!window.NostrTools || !window.NostrTools.nip44) {
|
|
throw new Error('nostr-tools NIP-44 not available');
|
|
}
|
|
|
|
// We need the user's private key, but we can't get it directly
|
|
// This is a security limitation - we should use NIP-07
|
|
throw new Error('Cannot access private key for direct encryption - use NIP-07 extension');
|
|
|
|
} catch (fallbackError) {
|
|
logTestEvent('ERROR', `Fallback encryption failed: ${fallbackError.message}`, 'ERROR');
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to get relay pubkey
|
|
function getRelayPubkey() {
|
|
// Use the dynamically fetched relay pubkey if available
|
|
if (relayPubkey && isRelayConnected) {
|
|
return relayPubkey;
|
|
}
|
|
|
|
// Fallback to hardcoded value for testing/development
|
|
log('Warning: Using hardcoded relay pubkey. Please connect to relay first.', 'WARNING');
|
|
return '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
|
|
}
|
|
|
|
// Enhanced SimplePool message handler to capture test responses
|
|
function enhancePoolForTesting() {
|
|
// SimplePool handles message parsing automatically, so we just need to
|
|
// ensure our event handlers log appropriately. This is already done
|
|
// in the subscription onevent callback.
|
|
console.log('SimplePool enhanced for testing - automatic message handling enabled');
|
|
}
|
|
|
|
// Generate random test pubkey function
|
|
function generateRandomTestKey() {
|
|
// Generate 32 random bytes (64 hex characters) for a valid pubkey
|
|
const randomBytes = new Uint8Array(32);
|
|
crypto.getRandomValues(randomBytes);
|
|
|
|
// Convert to hex string
|
|
const hexPubkey = Array.from(randomBytes)
|
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
|
|
// Set the generated key in the input field
|
|
const testPubkeyInput = document.getElementById('test-pubkey-input');
|
|
if (testPubkeyInput) {
|
|
testPubkeyInput.value = hexPubkey;
|
|
logTestEvent('INFO', `Generated random test pubkey: ${hexPubkey.substring(0, 16)}...`, 'KEYGEN');
|
|
}
|
|
|
|
return hexPubkey;
|
|
}
|
|
|
|
// Event handlers for test buttons
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Test button event handlers
|
|
const testGetAuthRulesBtn = document.getElementById('test-get-auth-rules-btn');
|
|
const testClearAuthRulesBtn = document.getElementById('test-clear-auth-rules-btn');
|
|
const testAddBlacklistBtn = document.getElementById('test-add-blacklist-btn');
|
|
const testAddWhitelistBtn = document.getElementById('test-add-whitelist-btn');
|
|
const testConfigQueryBtn = document.getElementById('test-config-query-btn');
|
|
const testPostEventBtn = document.getElementById('test-post-event-btn');
|
|
const clearTestLogBtn = document.getElementById('clear-test-log-btn');
|
|
const generateTestKeyBtn = document.getElementById('generate-test-key-btn');
|
|
|
|
if (testGetAuthRulesBtn) {
|
|
testGetAuthRulesBtn.addEventListener('click', testGetAuthRules);
|
|
}
|
|
|
|
if (testClearAuthRulesBtn) {
|
|
testClearAuthRulesBtn.addEventListener('click', testClearAuthRules);
|
|
}
|
|
|
|
if (testAddBlacklistBtn) {
|
|
testAddBlacklistBtn.addEventListener('click', testAddBlacklist);
|
|
}
|
|
|
|
if (testAddWhitelistBtn) {
|
|
testAddWhitelistBtn.addEventListener('click', testAddWhitelist);
|
|
}
|
|
|
|
if (testConfigQueryBtn) {
|
|
testConfigQueryBtn.addEventListener('click', testConfigQuery);
|
|
}
|
|
|
|
if (testPostEventBtn) {
|
|
testPostEventBtn.addEventListener('click', testPostEvent);
|
|
}
|
|
|
|
if (clearTestLogBtn) {
|
|
clearTestLogBtn.addEventListener('click', () => {
|
|
const testLog = document.getElementById('test-event-log');
|
|
if (testLog) {
|
|
testLog.innerHTML = '<div class="log-entry"><span class="log-timestamp">SYSTEM:</span> Test log cleared.</div>';
|
|
}
|
|
});
|
|
}
|
|
|
|
if (generateTestKeyBtn) {
|
|
generateTestKeyBtn.addEventListener('click', generateRandomTestKey);
|
|
}
|
|
|
|
// Show test input section when needed
|
|
const testInputSection = document.getElementById('test-input-section');
|
|
if (testInputSection) {
|
|
testInputSection.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
// Initialize the app
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
console.log('C-Relay Admin API interface loaded');
|
|
|
|
// Initialize login/logout button state
|
|
updateLoginLogoutButton();
|
|
|
|
setTimeout(() => {
|
|
initializeApp();
|
|
// Enhance SimplePool for testing after initialization
|
|
setTimeout(enhancePoolForTesting, 2000);
|
|
}, 100);
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |