ManagedACL.svelte raw

   1  <script>
   2      import { onMount } from "svelte";
   3      import { getApiBase } from "./config.js";
   4  
   5      // Props
   6      export let userSigner;
   7      export let userPubkey;
   8  
   9      // State management
  10      let activeTab = "pubkeys";
  11      let isLoading = false;
  12      let message = "";
  13      let messageType = "info";
  14  
  15      // Relay configuration state
  16      let relayName = "";
  17      let relayDescription = "";
  18      let relayIcon = "";
  19  
  20      // Banned pubkeys
  21      let bannedPubkeys = [];
  22      let newBannedPubkey = "";
  23      let newBannedPubkeyReason = "";
  24  
  25      // Allowed pubkeys
  26      let allowedPubkeys = [];
  27      let newAllowedPubkey = "";
  28      let newAllowedPubkeyReason = "";
  29  
  30      // Banned events
  31      let bannedEvents = [];
  32      let newBannedEvent = "";
  33      let newBannedEventReason = "";
  34  
  35      // Allowed events
  36      let allowedEvents = [];
  37      let newAllowedEvent = "";
  38      let newAllowedEventReason = "";
  39  
  40      // Blocked IPs
  41      let blockedIPs = [];
  42      let newBlockedIP = "";
  43      let newBlockedIPReason = "";
  44  
  45      // Allowed kinds
  46      let allowedKinds = [];
  47      let newAllowedKind = "";
  48  
  49      // Events needing moderation
  50      let eventsNeedingModeration = [];
  51  
  52      // Relay config
  53      let relayConfig = {
  54          relay_name: "",
  55          relay_description: "",
  56          relay_icon: "",
  57      };
  58  
  59      // Supported methods
  60      const supportedMethods = [
  61          "supportedmethods",
  62          "banpubkey",
  63          "listbannedpubkeys",
  64          "allowpubkey",
  65          "listallowedpubkeys",
  66          "listeventsneedingmoderation",
  67          "allowevent",
  68          "banevent",
  69          "listbannedevents",
  70          "changerelayname",
  71          "changerelaydescription",
  72          "changerelayicon",
  73          "allowkind",
  74          "disallowkind",
  75          "listallowedkinds",
  76          "blockip",
  77          "unblockip",
  78          "listblockedips",
  79      ];
  80  
  81      // Load relay info on component mount
  82      onMount(() => {
  83          // Small delay to ensure component is fully rendered
  84          setTimeout(() => {
  85              fetchRelayInfo();
  86          }, 100);
  87      });
  88  
  89      // Reactive statement to ensure form updates when relayConfig changes
  90      $: console.log("relayConfig changed:", relayConfig);
  91  
  92      // Fetch current relay information
  93      async function fetchRelayInfo() {
  94          try {
  95              isLoading = true;
  96              console.log("Fetching relay info from /");
  97              const response = await fetch(getApiBase() +"/", {
  98                  headers: {
  99                      Accept: "application/nostr+json",
 100                  },
 101              });
 102              console.log("Response status:", response.status);
 103              console.log("Response headers:", response.headers);
 104  
 105              if (response.ok) {
 106                  const relayInfo = await response.json();
 107                  console.log("Raw relay info:", relayInfo);
 108  
 109                  // Reassign the entire object to trigger Svelte reactivity
 110                  relayConfig = {
 111                      relay_name: relayInfo.name || "",
 112                      relay_description: relayInfo.description || "",
 113                      relay_icon: relayInfo.icon || "",
 114                  };
 115  
 116                  console.log("Updated relayConfig:", relayConfig);
 117                  console.log("Loaded relay info:", relayInfo);
 118  
 119                  message = "Relay configuration loaded successfully";
 120                  messageType = "success";
 121              } else {
 122                  console.error(
 123                      "Failed to fetch relay info, status:",
 124                      response.status,
 125                  );
 126                  message = `Failed to fetch relay info: ${response.status}`;
 127                  messageType = "error";
 128              }
 129          } catch (error) {
 130              console.error("Failed to fetch relay info:", error);
 131              message = `Failed to fetch relay info: ${error.message}`;
 132              messageType = "error";
 133          } finally {
 134              isLoading = false;
 135          }
 136      }
 137  
 138      // Create NIP-98 authentication event for HTTP requests
 139      async function createNIP98AuthEvent(method, url) {
 140          if (!userSigner) {
 141              throw new Error(
 142                  "No signer available for authentication. Please log in with a Nostr extension.",
 143              );
 144          }
 145  
 146          if (!userPubkey) {
 147              throw new Error("No user pubkey available for authentication.");
 148          }
 149  
 150          // Get the full URL
 151          const fullUrl = getApiBase() +url;
 152  
 153          // Create NIP-98 authentication event
 154          const authEvent = {
 155              kind: 27235, // HTTPAuth kind
 156              created_at: Math.floor(Date.now() / 1000),
 157              tags: [
 158                  ["u", fullUrl],
 159                  ["method", method],
 160              ],
 161              content: "",
 162              pubkey: userPubkey,
 163          };
 164  
 165          // Sign the authentication event
 166          const signedAuthEvent = await userSigner.signEvent(authEvent);
 167  
 168          // Encode the signed event as base64
 169          const eventJson = JSON.stringify(signedAuthEvent);
 170          const eventBase64 = btoa(eventJson);
 171  
 172          return `Nostr ${eventBase64}`;
 173      }
 174  
 175      // Make NIP-86 API call with NIP-98 authentication
 176      async function callNIP86API(method, params = []) {
 177          try {
 178              isLoading = true;
 179              message = "";
 180  
 181              const request = {
 182                  method: method,
 183                  params: params,
 184              };
 185  
 186              // Create NIP-98 authentication header
 187              const authHeader = await createNIP98AuthEvent("POST", "/api/nip86");
 188  
 189              const response = await fetch("/api/nip86", {
 190                  method: "POST",
 191                  headers: {
 192                      "Content-Type": "application/nostr+json+rpc",
 193                      Authorization: authHeader,
 194                  },
 195                  body: JSON.stringify(request),
 196              });
 197  
 198              if (!response.ok) {
 199                  throw new Error(
 200                      `HTTP ${response.status}: ${response.statusText}`,
 201                  );
 202              }
 203  
 204              const result = await response.json();
 205  
 206              if (result.error) {
 207                  throw new Error(result.error);
 208              }
 209  
 210              return result.result;
 211          } catch (error) {
 212              console.error("NIP-86 API error:", error);
 213              message = error.message;
 214              messageType = "error";
 215              throw error;
 216          } finally {
 217              isLoading = false;
 218          }
 219      }
 220  
 221      // Load data functions
 222      async function loadBannedPubkeys() {
 223          try {
 224              bannedPubkeys = await callNIP86API("listbannedpubkeys");
 225          } catch (error) {
 226              console.error("Failed to load banned pubkeys:", error);
 227          }
 228      }
 229  
 230      async function loadAllowedPubkeys() {
 231          try {
 232              allowedPubkeys = await callNIP86API("listallowedpubkeys");
 233          } catch (error) {
 234              console.error("Failed to load allowed pubkeys:", error);
 235          }
 236      }
 237  
 238      async function loadBannedEvents() {
 239          try {
 240              bannedEvents = await callNIP86API("listbannedevents");
 241          } catch (error) {
 242              console.error("Failed to load banned events:", error);
 243          }
 244      }
 245  
 246      // Removed loadAllowedEvents - method doesn't exist in NIP-86 API
 247  
 248      async function loadBlockedIPs() {
 249          try {
 250              blockedIPs = await callNIP86API("listblockedips");
 251          } catch (error) {
 252              console.error("Failed to load blocked IPs:", error);
 253          }
 254      }
 255  
 256      async function loadAllowedKinds() {
 257          try {
 258              allowedKinds = await callNIP86API("listallowedkinds");
 259          } catch (error) {
 260              console.error("Failed to load allowed kinds:", error);
 261          }
 262      }
 263  
 264      async function loadEventsNeedingModeration() {
 265          try {
 266              isLoading = true;
 267              eventsNeedingModeration = await callNIP86API(
 268                  "listeventsneedingmoderation",
 269              );
 270              console.log(
 271                  "Loaded events needing moderation:",
 272                  eventsNeedingModeration,
 273              );
 274          } catch (error) {
 275              console.error("Failed to load events needing moderation:", error);
 276              message = `Failed to load moderation events: ${error.message}`;
 277              messageType = "error";
 278              // Set empty array to prevent further issues
 279              eventsNeedingModeration = [];
 280          } finally {
 281              isLoading = false;
 282          }
 283      }
 284  
 285      // Action functions
 286      async function banPubkey() {
 287          if (!newBannedPubkey) return;
 288  
 289          try {
 290              await callNIP86API("banpubkey", [
 291                  newBannedPubkey,
 292                  newBannedPubkeyReason,
 293              ]);
 294              message = "Pubkey banned successfully";
 295              messageType = "success";
 296              newBannedPubkey = "";
 297              newBannedPubkeyReason = "";
 298              await loadBannedPubkeys();
 299          } catch (error) {
 300              console.error("Failed to ban pubkey:", error);
 301          }
 302      }
 303  
 304      async function allowPubkey() {
 305          if (!newAllowedPubkey) return;
 306  
 307          try {
 308              await callNIP86API("allowpubkey", [
 309                  newAllowedPubkey,
 310                  newAllowedPubkeyReason,
 311              ]);
 312              message = "Pubkey allowed successfully";
 313              messageType = "success";
 314              newAllowedPubkey = "";
 315              newAllowedPubkeyReason = "";
 316              await loadAllowedPubkeys();
 317          } catch (error) {
 318              console.error("Failed to allow pubkey:", error);
 319          }
 320      }
 321  
 322      async function banEvent() {
 323          if (!newBannedEvent) return;
 324  
 325          try {
 326              await callNIP86API("banevent", [
 327                  newBannedEvent,
 328                  newBannedEventReason,
 329              ]);
 330              message = "Event banned successfully";
 331              messageType = "success";
 332              newBannedEvent = "";
 333              newBannedEventReason = "";
 334              await loadBannedEvents();
 335          } catch (error) {
 336              console.error("Failed to ban event:", error);
 337          }
 338      }
 339  
 340      async function allowEvent() {
 341          if (!newAllowedEvent) return;
 342  
 343          try {
 344              await callNIP86API("allowevent", [
 345                  newAllowedEvent,
 346                  newAllowedEventReason,
 347              ]);
 348              message = "Event allowed successfully";
 349              messageType = "success";
 350              newAllowedEvent = "";
 351              newAllowedEventReason = "";
 352              // Note: No need to reload allowed events list as method doesn't exist
 353          } catch (error) {
 354              console.error("Failed to allow event:", error);
 355          }
 356      }
 357  
 358      async function blockIP() {
 359          if (!newBlockedIP) return;
 360  
 361          try {
 362              await callNIP86API("blockip", [newBlockedIP, newBlockedIPReason]);
 363              message = "IP blocked successfully";
 364              messageType = "success";
 365              newBlockedIP = "";
 366              newBlockedIPReason = "";
 367              await loadBlockedIPs();
 368          } catch (error) {
 369              console.error("Failed to block IP:", error);
 370          }
 371      }
 372  
 373      async function allowKind() {
 374          if (!newAllowedKind) return;
 375  
 376          const kindNum = parseInt(newAllowedKind);
 377          if (isNaN(kindNum)) {
 378              message = "Invalid kind number";
 379              messageType = "error";
 380              return;
 381          }
 382  
 383          try {
 384              await callNIP86API("allowkind", [kindNum]);
 385              message = "Kind allowed successfully";
 386              messageType = "success";
 387              newAllowedKind = "";
 388              await loadAllowedKinds();
 389          } catch (error) {
 390              console.error("Failed to allow kind:", error);
 391          }
 392      }
 393  
 394      async function disallowKind(kind) {
 395          try {
 396              await callNIP86API("disallowkind", [kind]);
 397              message = "Kind disallowed successfully";
 398              messageType = "success";
 399              await loadAllowedKinds();
 400          } catch (error) {
 401              console.error("Failed to disallow kind:", error);
 402          }
 403      }
 404  
 405      async function updateRelayName() {
 406          if (!relayConfig.relay_name) return;
 407  
 408          try {
 409              await callNIP86API("changerelayname", [relayConfig.relay_name]);
 410              message = "Relay name updated successfully";
 411              messageType = "success";
 412              // Refresh relay info to show updated values
 413              await fetchRelayInfo();
 414          } catch (error) {
 415              console.error("Failed to update relay name:", error);
 416          }
 417      }
 418  
 419      async function updateRelayDescription() {
 420          if (!relayConfig.relay_description) return;
 421  
 422          try {
 423              await callNIP86API("changerelaydescription", [
 424                  relayConfig.relay_description,
 425              ]);
 426              message = "Relay description updated successfully";
 427              messageType = "success";
 428              // Refresh relay info to show updated values
 429              await fetchRelayInfo();
 430          } catch (error) {
 431              console.error("Failed to update relay description:", error);
 432          }
 433      }
 434  
 435      async function updateRelayIcon() {
 436          if (!relayConfig.relay_icon) return;
 437  
 438          try {
 439              await callNIP86API("changerelayicon", [relayConfig.relay_icon]);
 440              message = "Relay icon updated successfully";
 441              messageType = "success";
 442              // Refresh relay info to show updated values
 443              await fetchRelayInfo();
 444          } catch (error) {
 445              console.error("Failed to update relay icon:", error);
 446          }
 447      }
 448  
 449      // Update all relay configuration at once
 450      async function updateRelayConfiguration() {
 451          try {
 452              isLoading = true;
 453              message = "";
 454  
 455              const updates = [];
 456  
 457              // Update relay name if provided
 458              if (relayConfig.relay_name) {
 459                  updates.push(
 460                      callNIP86API("changerelayname", [relayConfig.relay_name]),
 461                  );
 462              }
 463  
 464              // Update relay description if provided
 465              if (relayConfig.relay_description) {
 466                  updates.push(
 467                      callNIP86API("changerelaydescription", [
 468                          relayConfig.relay_description,
 469                      ]),
 470                  );
 471              }
 472  
 473              // Update relay icon if provided
 474              if (relayConfig.relay_icon) {
 475                  updates.push(
 476                      callNIP86API("changerelayicon", [relayConfig.relay_icon]),
 477                  );
 478              }
 479  
 480              if (updates.length === 0) {
 481                  message = "No changes to update";
 482                  messageType = "info";
 483                  return;
 484              }
 485  
 486              // Execute all updates in parallel
 487              await Promise.all(updates);
 488  
 489              message = "Relay configuration updated successfully";
 490              messageType = "success";
 491  
 492              // Refresh relay info to show updated values
 493              await fetchRelayInfo();
 494          } catch (error) {
 495              console.error("Failed to update relay configuration:", error);
 496              message = `Failed to update relay configuration: ${error.message}`;
 497              messageType = "error";
 498          } finally {
 499              isLoading = false;
 500          }
 501      }
 502  
 503      async function allowEventFromModeration(eventId) {
 504          try {
 505              await callNIP86API("allowevent", [
 506                  eventId,
 507                  "Approved from moderation queue",
 508              ]);
 509              message = "Event allowed successfully";
 510              messageType = "success";
 511              await loadEventsNeedingModeration();
 512          } catch (error) {
 513              console.error("Failed to allow event from moderation:", error);
 514          }
 515      }
 516  
 517      async function banEventFromModeration(eventId) {
 518          try {
 519              await callNIP86API("banevent", [
 520                  eventId,
 521                  "Banned from moderation queue",
 522              ]);
 523              message = "Event banned successfully";
 524              messageType = "success";
 525              await loadEventsNeedingModeration();
 526          } catch (error) {
 527              console.error("Failed to ban event from moderation:", error);
 528          }
 529      }
 530  
 531      // Load data when component mounts
 532      async function loadAllData() {
 533          await Promise.all([
 534              loadBannedPubkeys(),
 535              loadAllowedPubkeys(),
 536              loadBannedEvents(),
 537              // loadAllowedEvents(), // Removed - method doesn't exist
 538              loadBlockedIPs(),
 539              loadAllowedKinds(),
 540              // Note: loadEventsNeedingModeration() removed to prevent freezing
 541          ]);
 542      }
 543  
 544      // Initialize - only load basic data, not moderation
 545      loadAllData();
 546  </script>
 547  
 548  <div>
 549      <div class="header">
 550          <h2>Managed ACL Configuration</h2>
 551          <p>Configure access control using NIP-86 management API</p>
 552          <div class="owner-only-notice">
 553              <strong>Owner Only:</strong> This interface is restricted to relay owners
 554              only.
 555          </div>
 556      </div>
 557  
 558      {#if message}
 559          <div class="message {messageType}">
 560              {message}
 561          </div>
 562      {/if}
 563  
 564      <div class="tabs">
 565          <button
 566              class="tab {activeTab === 'pubkeys' ? 'active' : ''}"
 567              on:click={() => (activeTab = "pubkeys")}
 568          >
 569              Pubkeys
 570          </button>
 571          <button
 572              class="tab {activeTab === 'events' ? 'active' : ''}"
 573              on:click={() => (activeTab = "events")}
 574          >
 575              Events
 576          </button>
 577          <button
 578              class="tab {activeTab === 'ips' ? 'active' : ''}"
 579              on:click={() => (activeTab = "ips")}
 580          >
 581              IPs
 582          </button>
 583          <button
 584              class="tab {activeTab === 'kinds' ? 'active' : ''}"
 585              on:click={() => (activeTab = "kinds")}
 586          >
 587              Kinds
 588          </button>
 589          <button
 590              class="tab {activeTab === 'moderation' ? 'active' : ''}"
 591              on:click={() => {
 592                  activeTab = "moderation";
 593                  // Load moderation data only when tab is opened
 594                  if (
 595                      !eventsNeedingModeration ||
 596                      eventsNeedingModeration.length === 0
 597                  ) {
 598                      loadEventsNeedingModeration();
 599                  }
 600              }}
 601          >
 602              Moderation
 603          </button>
 604          <button
 605              class="tab {activeTab === 'relay' ? 'active' : ''}"
 606              on:click={() => (activeTab = "relay")}
 607          >
 608              Relay Config
 609          </button>
 610      </div>
 611  
 612      <div class="tab-content">
 613          {#if activeTab === "pubkeys"}
 614              <div class="pubkeys-section">
 615                  <div class="section">
 616                      <h3>Banned Pubkeys</h3>
 617                      <div class="add-form">
 618                          <input
 619                              type="text"
 620                              placeholder="Pubkey (64 hex chars)"
 621                              bind:value={newBannedPubkey}
 622                          />
 623                          <input
 624                              type="text"
 625                              placeholder="Reason (optional)"
 626                              bind:value={newBannedPubkeyReason}
 627                          />
 628                          <button on:click={banPubkey} disabled={isLoading}
 629                              >Ban Pubkey</button
 630                          >
 631                      </div>
 632                      <div class="list">
 633                          {#if bannedPubkeys && bannedPubkeys.length > 0}
 634                              {#each bannedPubkeys as item}
 635                                  <div class="list-item">
 636                                      <span class="pubkey">{item.pubkey}</span>
 637                                      {#if item.reason}
 638                                          <span class="reason">{item.reason}</span
 639                                          >
 640                                      {/if}
 641                                  </div>
 642                              {/each}
 643                          {:else}
 644                              <div class="no-items">
 645                                  <p>No banned pubkeys configured.</p>
 646                              </div>
 647                          {/if}
 648                      </div>
 649                  </div>
 650  
 651                  <div class="section">
 652                      <h3>Allowed Pubkeys</h3>
 653                      <div class="add-form">
 654                          <input
 655                              type="text"
 656                              placeholder="Pubkey (64 hex chars)"
 657                              bind:value={newAllowedPubkey}
 658                          />
 659                          <input
 660                              type="text"
 661                              placeholder="Reason (optional)"
 662                              bind:value={newAllowedPubkeyReason}
 663                          />
 664                          <button on:click={allowPubkey} disabled={isLoading}
 665                              >Allow Pubkey</button
 666                          >
 667                      </div>
 668                      <div class="list">
 669                          {#if allowedPubkeys && allowedPubkeys.length > 0}
 670                              {#each allowedPubkeys as item}
 671                                  <div class="list-item">
 672                                      <span class="pubkey">{item.pubkey}</span>
 673                                      {#if item.reason}
 674                                          <span class="reason">{item.reason}</span
 675                                          >
 676                                      {/if}
 677                                  </div>
 678                              {/each}
 679                          {:else}
 680                              <div class="no-items">
 681                                  <p>No allowed pubkeys configured.</p>
 682                              </div>
 683                          {/if}
 684                      </div>
 685                  </div>
 686              </div>
 687          {/if}
 688  
 689          {#if activeTab === "events"}
 690              <div class="events-section">
 691                  <div class="section">
 692                      <h3>Banned Events</h3>
 693                      <div class="add-form">
 694                          <input
 695                              type="text"
 696                              placeholder="Event ID (64 hex chars)"
 697                              bind:value={newBannedEvent}
 698                          />
 699                          <input
 700                              type="text"
 701                              placeholder="Reason (optional)"
 702                              bind:value={newBannedEventReason}
 703                          />
 704                          <button on:click={banEvent} disabled={isLoading}
 705                              >Ban Event</button
 706                          >
 707                      </div>
 708                      <div class="list">
 709                          {#if bannedEvents && bannedEvents.length > 0}
 710                              {#each bannedEvents as item}
 711                                  <div class="list-item">
 712                                      <span class="event-id">{item.id}</span>
 713                                      {#if item.reason}
 714                                          <span class="reason">{item.reason}</span
 715                                          >
 716                                      {/if}
 717                                  </div>
 718                              {/each}
 719                          {:else}
 720                              <div class="no-items">
 721                                  <p>No banned events configured.</p>
 722                              </div>
 723                          {/if}
 724                      </div>
 725                  </div>
 726  
 727                  <div class="section">
 728                      <h3>Allowed Events</h3>
 729                      <div class="add-form">
 730                          <input
 731                              type="text"
 732                              placeholder="Event ID (64 hex chars)"
 733                              bind:value={newAllowedEvent}
 734                          />
 735                          <input
 736                              type="text"
 737                              placeholder="Reason (optional)"
 738                              bind:value={newAllowedEventReason}
 739                          />
 740                          <button on:click={allowEvent} disabled={isLoading}
 741                              >Allow Event</button
 742                          >
 743                      </div>
 744                      <div class="list">
 745                          {#if allowedEvents && allowedEvents.length > 0}
 746                              {#each allowedEvents as item}
 747                                  <div class="list-item">
 748                                      <span class="event-id">{item.id}</span>
 749                                      {#if item.reason}
 750                                          <span class="reason">{item.reason}</span
 751                                          >
 752                                      {/if}
 753                                  </div>
 754                              {/each}
 755                          {:else}
 756                              <div class="no-items">
 757                                  <p>No allowed events configured.</p>
 758                              </div>
 759                          {/if}
 760                      </div>
 761                  </div>
 762              </div>
 763          {/if}
 764  
 765          {#if activeTab === "ips"}
 766              <div class="ips-section">
 767                  <div class="section">
 768                      <h3>Blocked IPs</h3>
 769                      <div class="add-form">
 770                          <input
 771                              type="text"
 772                              placeholder="IP Address"
 773                              bind:value={newBlockedIP}
 774                          />
 775                          <input
 776                              type="text"
 777                              placeholder="Reason (optional)"
 778                              bind:value={newBlockedIPReason}
 779                          />
 780                          <button on:click={blockIP} disabled={isLoading}
 781                              >Block IP</button
 782                          >
 783                      </div>
 784                      <div class="list">
 785                          {#if blockedIPs && blockedIPs.length > 0}
 786                              {#each blockedIPs as item}
 787                                  <div class="list-item">
 788                                      <span class="ip">{item.ip}</span>
 789                                      {#if item.reason}
 790                                          <span class="reason">{item.reason}</span
 791                                          >
 792                                      {/if}
 793                                  </div>
 794                              {/each}
 795                          {:else}
 796                              <div class="no-items">
 797                                  <p>No blocked IPs configured.</p>
 798                              </div>
 799                          {/if}
 800                      </div>
 801                  </div>
 802              </div>
 803          {/if}
 804  
 805          {#if activeTab === "kinds"}
 806              <div class="kinds-section">
 807                  <div class="section">
 808                      <h3>Allowed Event Kinds</h3>
 809                      <div class="add-form">
 810                          <input
 811                              type="number"
 812                              placeholder="Kind number"
 813                              bind:value={newAllowedKind}
 814                          />
 815                          <button on:click={allowKind} disabled={isLoading}
 816                              >Allow Kind</button
 817                          >
 818                      </div>
 819                      <div class="list">
 820                          {#if allowedKinds && allowedKinds.length > 0}
 821                              {#each allowedKinds as kind}
 822                                  <div class="list-item">
 823                                      <span class="kind">Kind {kind}</span>
 824                                      <button
 825                                          class="remove-btn"
 826                                          on:click={() => disallowKind(kind)}
 827                                          >Remove</button
 828                                      >
 829                                  </div>
 830                              {/each}
 831                          {:else}
 832                              <div class="no-items">
 833                                  <p>
 834                                      No allowed kinds configured. All kinds are
 835                                      allowed by default.
 836                                  </p>
 837                              </div>
 838                          {/if}
 839                      </div>
 840                  </div>
 841              </div>
 842          {/if}
 843  
 844          {#if activeTab === "moderation"}
 845              <div class="moderation-section">
 846                  <div class="section">
 847                      <h3>Events Needing Moderation</h3>
 848                      <button
 849                          on:click={loadEventsNeedingModeration}
 850                          disabled={isLoading}>Refresh</button
 851                      >
 852                      <div class="list">
 853                          {#if eventsNeedingModeration && eventsNeedingModeration.length > 0}
 854                              {#each eventsNeedingModeration as item}
 855                                  <div class="list-item">
 856                                      <span class="event-id">{item.id}</span>
 857                                      {#if item.reason}
 858                                          <span class="reason">{item.reason}</span
 859                                          >
 860                                      {/if}
 861                                      <div class="actions">
 862                                          <button
 863                                              on:click={() =>
 864                                                  allowEventFromModeration(
 865                                                      item.id,
 866                                                  )}>Allow</button
 867                                          >
 868                                          <button
 869                                              on:click={() =>
 870                                                  banEventFromModeration(item.id)}
 871                                              >Ban</button
 872                                          >
 873                                      </div>
 874                                  </div>
 875                              {/each}
 876                          {:else}
 877                              <div class="no-items">
 878                                  <p>No events need moderation at this time.</p>
 879                              </div>
 880                          {/if}
 881                      </div>
 882                  </div>
 883              </div>
 884          {/if}
 885  
 886          {#if activeTab === "relay"}
 887              <div class="relay-section">
 888                  <div class="section">
 889                      <h3>Relay Configuration</h3>
 890                      <div class="config-actions">
 891                          <button
 892                              on:click={fetchRelayInfo}
 893                              disabled={isLoading}
 894                              class="refresh-btn"
 895                          >
 896                              🔄 Refresh from Relay Info
 897                          </button>
 898                      </div>
 899                      <div class="config-form">
 900                          <div class="form-group">
 901                              <label for="relay-name">Relay Name</label>
 902                              <input
 903                                  id="relay-name"
 904                                  type="text"
 905                                  bind:value={relayConfig.relay_name}
 906                                  placeholder="Enter relay name"
 907                              />
 908                          </div>
 909                          <div class="form-group">
 910                              <label for="relay-description"
 911                                  >Relay Description</label
 912                              >
 913                              <textarea
 914                                  id="relay-description"
 915                                  bind:value={relayConfig.relay_description}
 916                                  placeholder="Enter relay description"
 917                              ></textarea>
 918                          </div>
 919                          <div class="form-group">
 920                              <label for="relay-icon">Relay Icon URL</label>
 921                              <input
 922                                  id="relay-icon"
 923                                  type="url"
 924                                  bind:value={relayConfig.relay_icon}
 925                                  placeholder="Enter icon URL"
 926                              />
 927                          </div>
 928                          <div class="config-update-section">
 929                              <button
 930                                  on:click={updateRelayConfiguration}
 931                                  disabled={isLoading}
 932                                  class="update-all-btn"
 933                              >
 934                                  {#if isLoading}
 935                                      ⏳ Updating...
 936                                  {:else}
 937                                      💾 Update Configuration
 938                                  {/if}
 939                              </button>
 940                          </div>
 941                      </div>
 942                  </div>
 943              </div>
 944          {/if}
 945      </div>
 946  </div>
 947  
 948  <style>
 949      .header {
 950          margin-bottom: 30px;
 951      }
 952  
 953      .header h2 {
 954          margin: 0 0 10px 0;
 955          color: var(--text-color);
 956      }
 957  
 958      .header p {
 959          margin: 0;
 960          color: var(--text-color);
 961          opacity: 0.8;
 962      }
 963  
 964      .owner-only-notice {
 965          margin-top: 10px;
 966          padding: 8px 12px;
 967          background-color: var(--warning-bg);
 968          border: 1px solid var(--warning);
 969          border-radius: 4px;
 970          color: var(--text-color);
 971          font-size: 0.9em;
 972      }
 973  
 974      .message {
 975          padding: 10px 15px;
 976          border-radius: 4px;
 977          margin-bottom: 20px;
 978      }
 979  
 980      .message.success {
 981          background-color: var(--success-bg);
 982          color: var(--success-text);
 983          border: 1px solid var(--success);
 984      }
 985  
 986      .message.error {
 987          background-color: var(--error-bg);
 988          color: var(--error-text);
 989          border: 1px solid var(--danger);
 990      }
 991  
 992      .message.info {
 993          background-color: var(--primary-bg);
 994          color: var(--text-color);
 995          border: 1px solid var(--info);
 996      }
 997  
 998      .tabs {
 999          display: flex;
1000          border-bottom: 1px solid var(--border-color);
1001          margin-bottom: 20px;
1002      }
1003  
1004      .tab {
1005          padding: 10px 20px;
1006          border: none;
1007          background: none;
1008          cursor: pointer;
1009          border-bottom: 2px solid transparent;
1010          transition: all 0.2s;
1011          color: var(--text-color);
1012      }
1013  
1014      .tab:hover {
1015          background-color: var(--button-hover-bg);
1016      }
1017  
1018      .tab.active {
1019          border-bottom-color: var(--accent-color);
1020          color: var(--accent-color);
1021      }
1022  
1023      .tab-content {
1024          min-height: 400px;
1025      }
1026  
1027      .section {
1028          margin-bottom: 30px;
1029      }
1030  
1031      .section h3 {
1032          margin: 0 0 15px 0;
1033          color: var(--text-color);
1034      }
1035  
1036      .add-form {
1037          display: flex;
1038          gap: 10px;
1039          margin-bottom: 20px;
1040          flex-wrap: wrap;
1041      }
1042  
1043      .add-form input {
1044          padding: 8px 12px;
1045          border: 1px solid var(--input-border);
1046          border-radius: 4px;
1047          background: var(--bg-color);
1048          color: var(--text-color);
1049          flex: 1;
1050          min-width: 200px;
1051      }
1052  
1053      .add-form button {
1054          padding: 8px 16px;
1055          background-color: var(--accent-color);
1056          color: var(--text-color);
1057          border: none;
1058          border-radius: 4px;
1059          cursor: pointer;
1060      }
1061  
1062      .add-form button:disabled {
1063          background-color: var(--secondary);
1064          cursor: not-allowed;
1065      }
1066  
1067      .list {
1068          border: 1px solid var(--border-color);
1069          border-radius: 4px;
1070          max-height: 300px;
1071          overflow-y: auto;
1072          background: var(--bg-color);
1073      }
1074  
1075      .list-item {
1076          padding: 10px 15px;
1077          border-bottom: 1px solid var(--border-color);
1078          display: flex;
1079          align-items: center;
1080          gap: 15px;
1081          color: var(--text-color);
1082      }
1083  
1084      .list-item:last-child {
1085          border-bottom: none;
1086      }
1087  
1088      .pubkey,
1089      .event-id,
1090      .ip,
1091      .kind {
1092          font-family: monospace;
1093          font-size: 0.9em;
1094          color: var(--text-color);
1095      }
1096  
1097      .reason {
1098          color: var(--text-color);
1099          opacity: 0.7;
1100          font-style: italic;
1101      }
1102  
1103      .remove-btn {
1104          padding: 4px 8px;
1105          background-color: var(--danger);
1106          color: var(--text-color);
1107          border: none;
1108          border-radius: 3px;
1109          cursor: pointer;
1110          font-size: 0.8em;
1111      }
1112  
1113      .actions {
1114          display: flex;
1115          gap: 5px;
1116          margin-left: auto;
1117      }
1118  
1119      .actions button {
1120          padding: 4px 8px;
1121          border: none;
1122          border-radius: 3px;
1123          cursor: pointer;
1124          font-size: 0.8em;
1125      }
1126  
1127      .actions button:first-child {
1128          background-color: var(--success);
1129          color: var(--text-color);
1130      }
1131  
1132      .actions button:last-child {
1133          background-color: var(--danger);
1134          color: var(--text-color);
1135      }
1136  
1137      .config-form {
1138          display: flex;
1139          flex-direction: column;
1140          gap: 20px;
1141      }
1142  
1143      .form-group {
1144          display: flex;
1145          flex-direction: column;
1146          gap: 10px;
1147      }
1148  
1149      .form-group label {
1150          font-weight: bold;
1151          color: var(--text-color);
1152      }
1153  
1154      .form-group input,
1155      .form-group textarea {
1156          padding: 8px 12px;
1157          border: 1px solid var(--input-border);
1158          border-radius: 4px;
1159          background: var(--bg-color);
1160          color: var(--text-color);
1161      }
1162  
1163      .form-group textarea {
1164          min-height: 80px;
1165          resize: vertical;
1166      }
1167  
1168      .config-actions {
1169          margin-bottom: 20px;
1170          padding: 10px;
1171          background-color: var(--button-bg);
1172          border-radius: 4px;
1173      }
1174  
1175      .refresh-btn {
1176          padding: 8px 16px;
1177          background-color: var(--success);
1178          color: var(--text-color);
1179          border: none;
1180          border-radius: 4px;
1181          cursor: pointer;
1182          font-size: 0.9em;
1183      }
1184  
1185      .refresh-btn:hover:not(:disabled) {
1186          background-color: var(--success);
1187          filter: brightness(0.9);
1188      }
1189  
1190      .refresh-btn:disabled {
1191          background-color: var(--secondary);
1192          cursor: not-allowed;
1193      }
1194  
1195      .config-update-section {
1196          margin-top: 20px;
1197          padding: 15px;
1198          background-color: var(--button-bg);
1199          border-radius: 6px;
1200          text-align: center;
1201      }
1202  
1203      .update-all-btn {
1204          padding: 12px 24px;
1205          background-color: var(--success);
1206          color: var(--text-color);
1207          border: none;
1208          border-radius: 6px;
1209          cursor: pointer;
1210          font-size: 1em;
1211          font-weight: 600;
1212          min-width: 200px;
1213      }
1214  
1215      .update-all-btn:hover:not(:disabled) {
1216          background-color: var(--success);
1217          filter: brightness(0.9);
1218          transform: translateY(-1px);
1219          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
1220      }
1221  
1222      .update-all-btn:disabled {
1223          background-color: var(--secondary);
1224          cursor: not-allowed;
1225          transform: none;
1226          box-shadow: none;
1227      }
1228  
1229      .no-items {
1230          padding: 20px;
1231          text-align: center;
1232          color: var(--text-color);
1233          opacity: 0.7;
1234          font-style: italic;
1235      }
1236  </style>
1237