- Added new components: Header, Sidebar, ExportView, ImportView, EventsView, ComposeView, RecoveryView, SprocketView, and SearchResultsView to enhance the application's functionality and user experience. - Updated App.svelte to integrate the new views and improve the overall layout. - Refactored existing components for better organization and maintainability. - Adjusted CSS styles for improved visual consistency across the application. - Incremented version number to v0.19.3 to reflect the latest changes and additions.
1236 lines
38 KiB
Svelte
1236 lines
38 KiB
Svelte
<script>
|
|
import { onMount } from "svelte";
|
|
|
|
// Props
|
|
export let userSigner;
|
|
export let userPubkey;
|
|
|
|
// State management
|
|
let activeTab = "pubkeys";
|
|
let isLoading = false;
|
|
let message = "";
|
|
let messageType = "info";
|
|
|
|
// Relay configuration state
|
|
let relayName = "";
|
|
let relayDescription = "";
|
|
let relayIcon = "";
|
|
|
|
// Banned pubkeys
|
|
let bannedPubkeys = [];
|
|
let newBannedPubkey = "";
|
|
let newBannedPubkeyReason = "";
|
|
|
|
// Allowed pubkeys
|
|
let allowedPubkeys = [];
|
|
let newAllowedPubkey = "";
|
|
let newAllowedPubkeyReason = "";
|
|
|
|
// Banned events
|
|
let bannedEvents = [];
|
|
let newBannedEvent = "";
|
|
let newBannedEventReason = "";
|
|
|
|
// Allowed events
|
|
let allowedEvents = [];
|
|
let newAllowedEvent = "";
|
|
let newAllowedEventReason = "";
|
|
|
|
// Blocked IPs
|
|
let blockedIPs = [];
|
|
let newBlockedIP = "";
|
|
let newBlockedIPReason = "";
|
|
|
|
// Allowed kinds
|
|
let allowedKinds = [];
|
|
let newAllowedKind = "";
|
|
|
|
// Events needing moderation
|
|
let eventsNeedingModeration = [];
|
|
|
|
// Relay config
|
|
let relayConfig = {
|
|
relay_name: "",
|
|
relay_description: "",
|
|
relay_icon: "",
|
|
};
|
|
|
|
// Supported methods
|
|
const supportedMethods = [
|
|
"supportedmethods",
|
|
"banpubkey",
|
|
"listbannedpubkeys",
|
|
"allowpubkey",
|
|
"listallowedpubkeys",
|
|
"listeventsneedingmoderation",
|
|
"allowevent",
|
|
"banevent",
|
|
"listbannedevents",
|
|
"changerelayname",
|
|
"changerelaydescription",
|
|
"changerelayicon",
|
|
"allowkind",
|
|
"disallowkind",
|
|
"listallowedkinds",
|
|
"blockip",
|
|
"unblockip",
|
|
"listblockedips",
|
|
];
|
|
|
|
// Load relay info on component mount
|
|
onMount(() => {
|
|
// Small delay to ensure component is fully rendered
|
|
setTimeout(() => {
|
|
fetchRelayInfo();
|
|
}, 100);
|
|
});
|
|
|
|
// Reactive statement to ensure form updates when relayConfig changes
|
|
$: console.log("relayConfig changed:", relayConfig);
|
|
|
|
// Fetch current relay information
|
|
async function fetchRelayInfo() {
|
|
try {
|
|
isLoading = true;
|
|
console.log("Fetching relay info from /");
|
|
const response = await fetch(window.location.origin + "/", {
|
|
headers: {
|
|
Accept: "application/nostr+json",
|
|
},
|
|
});
|
|
console.log("Response status:", response.status);
|
|
console.log("Response headers:", response.headers);
|
|
|
|
if (response.ok) {
|
|
const relayInfo = await response.json();
|
|
console.log("Raw relay info:", relayInfo);
|
|
|
|
// Reassign the entire object to trigger Svelte reactivity
|
|
relayConfig = {
|
|
relay_name: relayInfo.name || "",
|
|
relay_description: relayInfo.description || "",
|
|
relay_icon: relayInfo.icon || "",
|
|
};
|
|
|
|
console.log("Updated relayConfig:", relayConfig);
|
|
console.log("Loaded relay info:", relayInfo);
|
|
|
|
message = "Relay configuration loaded successfully";
|
|
messageType = "success";
|
|
} else {
|
|
console.error(
|
|
"Failed to fetch relay info, status:",
|
|
response.status,
|
|
);
|
|
message = `Failed to fetch relay info: ${response.status}`;
|
|
messageType = "error";
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch relay info:", error);
|
|
message = `Failed to fetch relay info: ${error.message}`;
|
|
messageType = "error";
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
// Create NIP-98 authentication event for HTTP requests
|
|
async function createNIP98AuthEvent(method, url) {
|
|
if (!userSigner) {
|
|
throw new Error(
|
|
"No signer available for authentication. Please log in with a Nostr extension.",
|
|
);
|
|
}
|
|
|
|
if (!userPubkey) {
|
|
throw new Error("No user pubkey available for authentication.");
|
|
}
|
|
|
|
// Get the full URL
|
|
const fullUrl = window.location.origin + url;
|
|
|
|
// Create NIP-98 authentication event
|
|
const authEvent = {
|
|
kind: 27235, // HTTPAuth kind
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["u", fullUrl],
|
|
["method", method],
|
|
],
|
|
content: "",
|
|
pubkey: userPubkey,
|
|
};
|
|
|
|
// Sign the authentication event
|
|
const signedAuthEvent = await userSigner.signEvent(authEvent);
|
|
|
|
// Encode the signed event as base64
|
|
const eventJson = JSON.stringify(signedAuthEvent);
|
|
const eventBase64 = btoa(eventJson);
|
|
|
|
return `Nostr ${eventBase64}`;
|
|
}
|
|
|
|
// Make NIP-86 API call with NIP-98 authentication
|
|
async function callNIP86API(method, params = []) {
|
|
try {
|
|
isLoading = true;
|
|
message = "";
|
|
|
|
const request = {
|
|
method: method,
|
|
params: params,
|
|
};
|
|
|
|
// Create NIP-98 authentication header
|
|
const authHeader = await createNIP98AuthEvent("POST", "/api/nip86");
|
|
|
|
const response = await fetch("/api/nip86", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/nostr+json+rpc",
|
|
Authorization: authHeader,
|
|
},
|
|
body: JSON.stringify(request),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`HTTP ${response.status}: ${response.statusText}`,
|
|
);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.error) {
|
|
throw new Error(result.error);
|
|
}
|
|
|
|
return result.result;
|
|
} catch (error) {
|
|
console.error("NIP-86 API error:", error);
|
|
message = error.message;
|
|
messageType = "error";
|
|
throw error;
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
// Load data functions
|
|
async function loadBannedPubkeys() {
|
|
try {
|
|
bannedPubkeys = await callNIP86API("listbannedpubkeys");
|
|
} catch (error) {
|
|
console.error("Failed to load banned pubkeys:", error);
|
|
}
|
|
}
|
|
|
|
async function loadAllowedPubkeys() {
|
|
try {
|
|
allowedPubkeys = await callNIP86API("listallowedpubkeys");
|
|
} catch (error) {
|
|
console.error("Failed to load allowed pubkeys:", error);
|
|
}
|
|
}
|
|
|
|
async function loadBannedEvents() {
|
|
try {
|
|
bannedEvents = await callNIP86API("listbannedevents");
|
|
} catch (error) {
|
|
console.error("Failed to load banned events:", error);
|
|
}
|
|
}
|
|
|
|
// Removed loadAllowedEvents - method doesn't exist in NIP-86 API
|
|
|
|
async function loadBlockedIPs() {
|
|
try {
|
|
blockedIPs = await callNIP86API("listblockedips");
|
|
} catch (error) {
|
|
console.error("Failed to load blocked IPs:", error);
|
|
}
|
|
}
|
|
|
|
async function loadAllowedKinds() {
|
|
try {
|
|
allowedKinds = await callNIP86API("listallowedkinds");
|
|
} catch (error) {
|
|
console.error("Failed to load allowed kinds:", error);
|
|
}
|
|
}
|
|
|
|
async function loadEventsNeedingModeration() {
|
|
try {
|
|
isLoading = true;
|
|
eventsNeedingModeration = await callNIP86API(
|
|
"listeventsneedingmoderation",
|
|
);
|
|
console.log(
|
|
"Loaded events needing moderation:",
|
|
eventsNeedingModeration,
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to load events needing moderation:", error);
|
|
message = `Failed to load moderation events: ${error.message}`;
|
|
messageType = "error";
|
|
// Set empty array to prevent further issues
|
|
eventsNeedingModeration = [];
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
// Action functions
|
|
async function banPubkey() {
|
|
if (!newBannedPubkey) return;
|
|
|
|
try {
|
|
await callNIP86API("banpubkey", [
|
|
newBannedPubkey,
|
|
newBannedPubkeyReason,
|
|
]);
|
|
message = "Pubkey banned successfully";
|
|
messageType = "success";
|
|
newBannedPubkey = "";
|
|
newBannedPubkeyReason = "";
|
|
await loadBannedPubkeys();
|
|
} catch (error) {
|
|
console.error("Failed to ban pubkey:", error);
|
|
}
|
|
}
|
|
|
|
async function allowPubkey() {
|
|
if (!newAllowedPubkey) return;
|
|
|
|
try {
|
|
await callNIP86API("allowpubkey", [
|
|
newAllowedPubkey,
|
|
newAllowedPubkeyReason,
|
|
]);
|
|
message = "Pubkey allowed successfully";
|
|
messageType = "success";
|
|
newAllowedPubkey = "";
|
|
newAllowedPubkeyReason = "";
|
|
await loadAllowedPubkeys();
|
|
} catch (error) {
|
|
console.error("Failed to allow pubkey:", error);
|
|
}
|
|
}
|
|
|
|
async function banEvent() {
|
|
if (!newBannedEvent) return;
|
|
|
|
try {
|
|
await callNIP86API("banevent", [
|
|
newBannedEvent,
|
|
newBannedEventReason,
|
|
]);
|
|
message = "Event banned successfully";
|
|
messageType = "success";
|
|
newBannedEvent = "";
|
|
newBannedEventReason = "";
|
|
await loadBannedEvents();
|
|
} catch (error) {
|
|
console.error("Failed to ban event:", error);
|
|
}
|
|
}
|
|
|
|
async function allowEvent() {
|
|
if (!newAllowedEvent) return;
|
|
|
|
try {
|
|
await callNIP86API("allowevent", [
|
|
newAllowedEvent,
|
|
newAllowedEventReason,
|
|
]);
|
|
message = "Event allowed successfully";
|
|
messageType = "success";
|
|
newAllowedEvent = "";
|
|
newAllowedEventReason = "";
|
|
// Note: No need to reload allowed events list as method doesn't exist
|
|
} catch (error) {
|
|
console.error("Failed to allow event:", error);
|
|
}
|
|
}
|
|
|
|
async function blockIP() {
|
|
if (!newBlockedIP) return;
|
|
|
|
try {
|
|
await callNIP86API("blockip", [newBlockedIP, newBlockedIPReason]);
|
|
message = "IP blocked successfully";
|
|
messageType = "success";
|
|
newBlockedIP = "";
|
|
newBlockedIPReason = "";
|
|
await loadBlockedIPs();
|
|
} catch (error) {
|
|
console.error("Failed to block IP:", error);
|
|
}
|
|
}
|
|
|
|
async function allowKind() {
|
|
if (!newAllowedKind) return;
|
|
|
|
const kindNum = parseInt(newAllowedKind);
|
|
if (isNaN(kindNum)) {
|
|
message = "Invalid kind number";
|
|
messageType = "error";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await callNIP86API("allowkind", [kindNum]);
|
|
message = "Kind allowed successfully";
|
|
messageType = "success";
|
|
newAllowedKind = "";
|
|
await loadAllowedKinds();
|
|
} catch (error) {
|
|
console.error("Failed to allow kind:", error);
|
|
}
|
|
}
|
|
|
|
async function disallowKind(kind) {
|
|
try {
|
|
await callNIP86API("disallowkind", [kind]);
|
|
message = "Kind disallowed successfully";
|
|
messageType = "success";
|
|
await loadAllowedKinds();
|
|
} catch (error) {
|
|
console.error("Failed to disallow kind:", error);
|
|
}
|
|
}
|
|
|
|
async function updateRelayName() {
|
|
if (!relayConfig.relay_name) return;
|
|
|
|
try {
|
|
await callNIP86API("changerelayname", [relayConfig.relay_name]);
|
|
message = "Relay name updated successfully";
|
|
messageType = "success";
|
|
// Refresh relay info to show updated values
|
|
await fetchRelayInfo();
|
|
} catch (error) {
|
|
console.error("Failed to update relay name:", error);
|
|
}
|
|
}
|
|
|
|
async function updateRelayDescription() {
|
|
if (!relayConfig.relay_description) return;
|
|
|
|
try {
|
|
await callNIP86API("changerelaydescription", [
|
|
relayConfig.relay_description,
|
|
]);
|
|
message = "Relay description updated successfully";
|
|
messageType = "success";
|
|
// Refresh relay info to show updated values
|
|
await fetchRelayInfo();
|
|
} catch (error) {
|
|
console.error("Failed to update relay description:", error);
|
|
}
|
|
}
|
|
|
|
async function updateRelayIcon() {
|
|
if (!relayConfig.relay_icon) return;
|
|
|
|
try {
|
|
await callNIP86API("changerelayicon", [relayConfig.relay_icon]);
|
|
message = "Relay icon updated successfully";
|
|
messageType = "success";
|
|
// Refresh relay info to show updated values
|
|
await fetchRelayInfo();
|
|
} catch (error) {
|
|
console.error("Failed to update relay icon:", error);
|
|
}
|
|
}
|
|
|
|
// Update all relay configuration at once
|
|
async function updateRelayConfiguration() {
|
|
try {
|
|
isLoading = true;
|
|
message = "";
|
|
|
|
const updates = [];
|
|
|
|
// Update relay name if provided
|
|
if (relayConfig.relay_name) {
|
|
updates.push(
|
|
callNIP86API("changerelayname", [relayConfig.relay_name]),
|
|
);
|
|
}
|
|
|
|
// Update relay description if provided
|
|
if (relayConfig.relay_description) {
|
|
updates.push(
|
|
callNIP86API("changerelaydescription", [
|
|
relayConfig.relay_description,
|
|
]),
|
|
);
|
|
}
|
|
|
|
// Update relay icon if provided
|
|
if (relayConfig.relay_icon) {
|
|
updates.push(
|
|
callNIP86API("changerelayicon", [relayConfig.relay_icon]),
|
|
);
|
|
}
|
|
|
|
if (updates.length === 0) {
|
|
message = "No changes to update";
|
|
messageType = "info";
|
|
return;
|
|
}
|
|
|
|
// Execute all updates in parallel
|
|
await Promise.all(updates);
|
|
|
|
message = "Relay configuration updated successfully";
|
|
messageType = "success";
|
|
|
|
// Refresh relay info to show updated values
|
|
await fetchRelayInfo();
|
|
} catch (error) {
|
|
console.error("Failed to update relay configuration:", error);
|
|
message = `Failed to update relay configuration: ${error.message}`;
|
|
messageType = "error";
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
async function allowEventFromModeration(eventId) {
|
|
try {
|
|
await callNIP86API("allowevent", [
|
|
eventId,
|
|
"Approved from moderation queue",
|
|
]);
|
|
message = "Event allowed successfully";
|
|
messageType = "success";
|
|
await loadEventsNeedingModeration();
|
|
} catch (error) {
|
|
console.error("Failed to allow event from moderation:", error);
|
|
}
|
|
}
|
|
|
|
async function banEventFromModeration(eventId) {
|
|
try {
|
|
await callNIP86API("banevent", [
|
|
eventId,
|
|
"Banned from moderation queue",
|
|
]);
|
|
message = "Event banned successfully";
|
|
messageType = "success";
|
|
await loadEventsNeedingModeration();
|
|
} catch (error) {
|
|
console.error("Failed to ban event from moderation:", error);
|
|
}
|
|
}
|
|
|
|
// Load data when component mounts
|
|
async function loadAllData() {
|
|
await Promise.all([
|
|
loadBannedPubkeys(),
|
|
loadAllowedPubkeys(),
|
|
loadBannedEvents(),
|
|
// loadAllowedEvents(), // Removed - method doesn't exist
|
|
loadBlockedIPs(),
|
|
loadAllowedKinds(),
|
|
// Note: loadEventsNeedingModeration() removed to prevent freezing
|
|
]);
|
|
}
|
|
|
|
// Initialize - only load basic data, not moderation
|
|
loadAllData();
|
|
</script>
|
|
|
|
<div>
|
|
<div class="header">
|
|
<h2>Managed ACL Configuration</h2>
|
|
<p>Configure access control using NIP-86 management API</p>
|
|
<div class="owner-only-notice">
|
|
<strong>Owner Only:</strong> This interface is restricted to relay owners
|
|
only.
|
|
</div>
|
|
</div>
|
|
|
|
{#if message}
|
|
<div class="message {messageType}">
|
|
{message}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="tabs">
|
|
<button
|
|
class="tab {activeTab === 'pubkeys' ? 'active' : ''}"
|
|
on:click={() => (activeTab = "pubkeys")}
|
|
>
|
|
Pubkeys
|
|
</button>
|
|
<button
|
|
class="tab {activeTab === 'events' ? 'active' : ''}"
|
|
on:click={() => (activeTab = "events")}
|
|
>
|
|
Events
|
|
</button>
|
|
<button
|
|
class="tab {activeTab === 'ips' ? 'active' : ''}"
|
|
on:click={() => (activeTab = "ips")}
|
|
>
|
|
IPs
|
|
</button>
|
|
<button
|
|
class="tab {activeTab === 'kinds' ? 'active' : ''}"
|
|
on:click={() => (activeTab = "kinds")}
|
|
>
|
|
Kinds
|
|
</button>
|
|
<button
|
|
class="tab {activeTab === 'moderation' ? 'active' : ''}"
|
|
on:click={() => {
|
|
activeTab = "moderation";
|
|
// Load moderation data only when tab is opened
|
|
if (
|
|
!eventsNeedingModeration ||
|
|
eventsNeedingModeration.length === 0
|
|
) {
|
|
loadEventsNeedingModeration();
|
|
}
|
|
}}
|
|
>
|
|
Moderation
|
|
</button>
|
|
<button
|
|
class="tab {activeTab === 'relay' ? 'active' : ''}"
|
|
on:click={() => (activeTab = "relay")}
|
|
>
|
|
Relay Config
|
|
</button>
|
|
</div>
|
|
|
|
<div class="tab-content">
|
|
{#if activeTab === "pubkeys"}
|
|
<div class="pubkeys-section">
|
|
<div class="section">
|
|
<h3>Banned Pubkeys</h3>
|
|
<div class="add-form">
|
|
<input
|
|
type="text"
|
|
placeholder="Pubkey (64 hex chars)"
|
|
bind:value={newBannedPubkey}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Reason (optional)"
|
|
bind:value={newBannedPubkeyReason}
|
|
/>
|
|
<button on:click={banPubkey} disabled={isLoading}
|
|
>Ban Pubkey</button
|
|
>
|
|
</div>
|
|
<div class="list">
|
|
{#if bannedPubkeys && bannedPubkeys.length > 0}
|
|
{#each bannedPubkeys as item}
|
|
<div class="list-item">
|
|
<span class="pubkey">{item.pubkey}</span>
|
|
{#if item.reason}
|
|
<span class="reason">{item.reason}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="no-items">
|
|
<p>No banned pubkeys configured.</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>Allowed Pubkeys</h3>
|
|
<div class="add-form">
|
|
<input
|
|
type="text"
|
|
placeholder="Pubkey (64 hex chars)"
|
|
bind:value={newAllowedPubkey}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Reason (optional)"
|
|
bind:value={newAllowedPubkeyReason}
|
|
/>
|
|
<button on:click={allowPubkey} disabled={isLoading}
|
|
>Allow Pubkey</button
|
|
>
|
|
</div>
|
|
<div class="list">
|
|
{#if allowedPubkeys && allowedPubkeys.length > 0}
|
|
{#each allowedPubkeys as item}
|
|
<div class="list-item">
|
|
<span class="pubkey">{item.pubkey}</span>
|
|
{#if item.reason}
|
|
<span class="reason">{item.reason}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="no-items">
|
|
<p>No allowed pubkeys configured.</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if activeTab === "events"}
|
|
<div class="events-section">
|
|
<div class="section">
|
|
<h3>Banned Events</h3>
|
|
<div class="add-form">
|
|
<input
|
|
type="text"
|
|
placeholder="Event ID (64 hex chars)"
|
|
bind:value={newBannedEvent}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Reason (optional)"
|
|
bind:value={newBannedEventReason}
|
|
/>
|
|
<button on:click={banEvent} disabled={isLoading}
|
|
>Ban Event</button
|
|
>
|
|
</div>
|
|
<div class="list">
|
|
{#if bannedEvents && bannedEvents.length > 0}
|
|
{#each bannedEvents as item}
|
|
<div class="list-item">
|
|
<span class="event-id">{item.id}</span>
|
|
{#if item.reason}
|
|
<span class="reason">{item.reason}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="no-items">
|
|
<p>No banned events configured.</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>Allowed Events</h3>
|
|
<div class="add-form">
|
|
<input
|
|
type="text"
|
|
placeholder="Event ID (64 hex chars)"
|
|
bind:value={newAllowedEvent}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Reason (optional)"
|
|
bind:value={newAllowedEventReason}
|
|
/>
|
|
<button on:click={allowEvent} disabled={isLoading}
|
|
>Allow Event</button
|
|
>
|
|
</div>
|
|
<div class="list">
|
|
{#if allowedEvents && allowedEvents.length > 0}
|
|
{#each allowedEvents as item}
|
|
<div class="list-item">
|
|
<span class="event-id">{item.id}</span>
|
|
{#if item.reason}
|
|
<span class="reason">{item.reason}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="no-items">
|
|
<p>No allowed events configured.</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if activeTab === "ips"}
|
|
<div class="ips-section">
|
|
<div class="section">
|
|
<h3>Blocked IPs</h3>
|
|
<div class="add-form">
|
|
<input
|
|
type="text"
|
|
placeholder="IP Address"
|
|
bind:value={newBlockedIP}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Reason (optional)"
|
|
bind:value={newBlockedIPReason}
|
|
/>
|
|
<button on:click={blockIP} disabled={isLoading}
|
|
>Block IP</button
|
|
>
|
|
</div>
|
|
<div class="list">
|
|
{#if blockedIPs && blockedIPs.length > 0}
|
|
{#each blockedIPs as item}
|
|
<div class="list-item">
|
|
<span class="ip">{item.ip}</span>
|
|
{#if item.reason}
|
|
<span class="reason">{item.reason}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="no-items">
|
|
<p>No blocked IPs configured.</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if activeTab === "kinds"}
|
|
<div class="kinds-section">
|
|
<div class="section">
|
|
<h3>Allowed Event Kinds</h3>
|
|
<div class="add-form">
|
|
<input
|
|
type="number"
|
|
placeholder="Kind number"
|
|
bind:value={newAllowedKind}
|
|
/>
|
|
<button on:click={allowKind} disabled={isLoading}
|
|
>Allow Kind</button
|
|
>
|
|
</div>
|
|
<div class="list">
|
|
{#if allowedKinds && allowedKinds.length > 0}
|
|
{#each allowedKinds as kind}
|
|
<div class="list-item">
|
|
<span class="kind">Kind {kind}</span>
|
|
<button
|
|
class="remove-btn"
|
|
on:click={() => disallowKind(kind)}
|
|
>Remove</button
|
|
>
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="no-items">
|
|
<p>
|
|
No allowed kinds configured. All kinds are
|
|
allowed by default.
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if activeTab === "moderation"}
|
|
<div class="moderation-section">
|
|
<div class="section">
|
|
<h3>Events Needing Moderation</h3>
|
|
<button
|
|
on:click={loadEventsNeedingModeration}
|
|
disabled={isLoading}>Refresh</button
|
|
>
|
|
<div class="list">
|
|
{#if eventsNeedingModeration && eventsNeedingModeration.length > 0}
|
|
{#each eventsNeedingModeration as item}
|
|
<div class="list-item">
|
|
<span class="event-id">{item.id}</span>
|
|
{#if item.reason}
|
|
<span class="reason">{item.reason}</span
|
|
>
|
|
{/if}
|
|
<div class="actions">
|
|
<button
|
|
on:click={() =>
|
|
allowEventFromModeration(
|
|
item.id,
|
|
)}>Allow</button
|
|
>
|
|
<button
|
|
on:click={() =>
|
|
banEventFromModeration(item.id)}
|
|
>Ban</button
|
|
>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="no-items">
|
|
<p>No events need moderation at this time.</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if activeTab === "relay"}
|
|
<div class="relay-section">
|
|
<div class="section">
|
|
<h3>Relay Configuration</h3>
|
|
<div class="config-actions">
|
|
<button
|
|
on:click={fetchRelayInfo}
|
|
disabled={isLoading}
|
|
class="refresh-btn"
|
|
>
|
|
🔄 Refresh from Relay Info
|
|
</button>
|
|
</div>
|
|
<div class="config-form">
|
|
<div class="form-group">
|
|
<label for="relay-name">Relay Name</label>
|
|
<input
|
|
id="relay-name"
|
|
type="text"
|
|
bind:value={relayConfig.relay_name}
|
|
placeholder="Enter relay name"
|
|
/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="relay-description"
|
|
>Relay Description</label
|
|
>
|
|
<textarea
|
|
id="relay-description"
|
|
bind:value={relayConfig.relay_description}
|
|
placeholder="Enter relay description"
|
|
></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="relay-icon">Relay Icon URL</label>
|
|
<input
|
|
id="relay-icon"
|
|
type="url"
|
|
bind:value={relayConfig.relay_icon}
|
|
placeholder="Enter icon URL"
|
|
/>
|
|
</div>
|
|
<div class="config-update-section">
|
|
<button
|
|
on:click={updateRelayConfiguration}
|
|
disabled={isLoading}
|
|
class="update-all-btn"
|
|
>
|
|
{#if isLoading}
|
|
⏳ Updating...
|
|
{:else}
|
|
💾 Update Configuration
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.header {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.header h2 {
|
|
margin: 0 0 10px 0;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.header p {
|
|
margin: 0;
|
|
color: var(--text-color);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.owner-only-notice {
|
|
margin-top: 10px;
|
|
padding: 8px 12px;
|
|
background-color: var(--warning-bg);
|
|
border: 1px solid var(--warning);
|
|
border-radius: 4px;
|
|
color: var(--text-color);
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.message {
|
|
padding: 10px 15px;
|
|
border-radius: 4px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.message.success {
|
|
background-color: var(--success-bg);
|
|
color: var(--success-text);
|
|
border: 1px solid var(--success);
|
|
}
|
|
|
|
.message.error {
|
|
background-color: var(--error-bg);
|
|
color: var(--error-text);
|
|
border: 1px solid var(--danger);
|
|
}
|
|
|
|
.message.info {
|
|
background-color: var(--primary-bg);
|
|
color: var(--text-color);
|
|
border: 1px solid var(--info);
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
border-bottom: 1px solid var(--border-color);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.tab {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
background: none;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
transition: all 0.2s;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.tab:hover {
|
|
background-color: var(--button-hover-bg);
|
|
}
|
|
|
|
.tab.active {
|
|
border-bottom-color: var(--accent-color);
|
|
color: var(--accent-color);
|
|
}
|
|
|
|
.tab-content {
|
|
min-height: 400px;
|
|
}
|
|
|
|
.section {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.section h3 {
|
|
margin: 0 0 15px 0;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.add-form {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.add-form input {
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--input-border);
|
|
border-radius: 4px;
|
|
background: var(--bg-color);
|
|
color: var(--text-color);
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.add-form button {
|
|
padding: 8px 16px;
|
|
background-color: var(--accent-color);
|
|
color: var(--text-color);
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.add-form button:disabled {
|
|
background-color: var(--secondary);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.list {
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
background: var(--bg-color);
|
|
}
|
|
|
|
.list-item {
|
|
padding: 10px 15px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.list-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.pubkey,
|
|
.event-id,
|
|
.ip,
|
|
.kind {
|
|
font-family: monospace;
|
|
font-size: 0.9em;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.reason {
|
|
color: var(--text-color);
|
|
opacity: 0.7;
|
|
font-style: italic;
|
|
}
|
|
|
|
.remove-btn {
|
|
padding: 4px 8px;
|
|
background-color: var(--danger);
|
|
color: var(--text-color);
|
|
border: none;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
font-size: 0.8em;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
gap: 5px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.actions button {
|
|
padding: 4px 8px;
|
|
border: none;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
font-size: 0.8em;
|
|
}
|
|
|
|
.actions button:first-child {
|
|
background-color: var(--success);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.actions button:last-child {
|
|
background-color: var(--danger);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.config-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.form-group label {
|
|
font-weight: bold;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea {
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--input-border);
|
|
border-radius: 4px;
|
|
background: var(--bg-color);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.form-group textarea {
|
|
min-height: 80px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.config-actions {
|
|
margin-bottom: 20px;
|
|
padding: 10px;
|
|
background-color: var(--button-bg);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.refresh-btn {
|
|
padding: 8px 16px;
|
|
background-color: var(--success);
|
|
color: var(--text-color);
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.refresh-btn:hover:not(:disabled) {
|
|
background-color: var(--success);
|
|
filter: brightness(0.9);
|
|
}
|
|
|
|
.refresh-btn:disabled {
|
|
background-color: var(--secondary);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.config-update-section {
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
background-color: var(--button-bg);
|
|
border-radius: 6px;
|
|
text-align: center;
|
|
}
|
|
|
|
.update-all-btn {
|
|
padding: 12px 24px;
|
|
background-color: var(--success);
|
|
color: var(--text-color);
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 1em;
|
|
font-weight: 600;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.update-all-btn:hover:not(:disabled) {
|
|
background-color: var(--success);
|
|
filter: brightness(0.9);
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.update-all-btn:disabled {
|
|
background-color: var(--secondary);
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.no-items {
|
|
padding: 20px;
|
|
text-align: center;
|
|
color: var(--text-color);
|
|
opacity: 0.7;
|
|
font-style: italic;
|
|
}
|
|
</style>
|