enki ae9557bacf
Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
docs and about updates
2025-08-18 00:50:56 -07:00

1670 lines
83 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BitTorrent Gateway</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<header>
<h1>⚡ BitTorrent Gateway</h1>
<nav>
<a href="#about" onclick="showAbout(); return false;">About</a>
<a href="#services" onclick="showServices(); return false;">Server Stats</a>
<a href="#upload" onclick="showUpload(); return false;" id="upload-link" style="display: none;">Upload</a>
<a href="#files" onclick="showFiles(); return false;" id="files-link" style="display: none;">My Files</a>
<a href="/admin" id="admin-link" style="display: none;">Admin</a>
<div id="auth-status" class="auth-status">
<button id="login-btn" onclick="showLogin()">Login</button>
<div id="user-info" style="display: none;">
<span id="user-pubkey-short"></span>
<button id="logout-btn" onclick="logout()">Logout</button>
</div>
</div>
</nav>
</header>
<main>
<!-- Upload Section -->
<div id="upload-section" class="section">
<div class="upload-area" id="upload-area">
<div class="upload-icon">📁</div>
<h3>Drag & drop files here</h3>
<p>or click to select files</p>
<input type="file" id="file-input" multiple>
</div>
<div id="upload-progress" class="upload-progress hidden">
<div class="progress-header">
<h4 id="upload-filename">Uploading...</h4>
<span id="upload-cancel" onclick="cancelUpload()"></span>
</div>
<div class="progress-bar">
<div id="progress-fill" class="progress-fill"></div>
</div>
<div class="progress-info">
<span id="progress-percent">0%</span>
<span id="progress-speed">0 MB/s</span>
<span id="progress-eta">--:--</span>
</div>
</div>
<div class="upload-options">
<label>
<input type="checkbox" id="announce-dht" checked>
Announce to DHT
</label>
<label>
<input type="checkbox" id="store-blossom" checked>
Store in Blossom
</label>
</div>
<div id="recent-uploads" class="recent-uploads">
<h3>Recent Uploads</h3>
<div id="uploads-list" class="uploads-list">
<p class="empty-state">No recent uploads</p>
</div>
</div>
</div>
<!-- Services Section -->
<div id="services-section" class="section">
<h2>Server Stats</h2>
<div class="service-grid">
<div class="service-card">
<div class="service-header">
<h3>🚀 Gateway API</h3>
<span id="gateway-status" class="status-indicator"></span>
</div>
<div class="service-info">
<p><strong>Total Files:</strong> <span id="gateway-uploads">0</span></p>
<p><strong>Storage Used:</strong> <span id="gateway-storage">0 MB</span></p>
<p><strong>Downloads Served:</strong> <span id="gateway-downloads">0</span></p>
</div>
</div>
<div class="service-card">
<div class="service-header">
<h3>🌸 Blossom Server</h3>
<span id="blossom-status" class="status-indicator"></span>
</div>
<div class="service-info">
<p><strong>Blobs Stored:</strong> <span id="blossom-blobs">0</span></p>
<p><strong>Blob Storage:</strong> <span id="blossom-storage">0 MB</span></p>
<p><strong>Requests Today:</strong> <span id="blossom-requests">0</span></p>
</div>
</div>
<div class="service-card">
<div class="service-header">
<h3>🕸️ DHT Node</h3>
<span id="dht-status" class="status-indicator"></span>
</div>
<div class="service-info">
<p><strong>Network Peers:</strong> <span id="dht-peers">0</span></p>
<p><strong>Torrents Seeding:</strong> <span id="dht-torrents">0</span></p>
<p><strong>Announces Today:</strong> <span id="dht-announces">0</span></p>
</div>
</div>
<div class="service-card">
<div class="service-header">
<h3>📡 BitTorrent Tracker</h3>
<span id="tracker-status" class="status-indicator"></span>
</div>
<div class="service-info">
<p><strong>Active Torrents:</strong> <span id="tracker-torrents">0</span></p>
<p><strong>Connected Peers:</strong> <span id="tracker-peers">0</span></p>
<p><strong>Seeders/Leechers:</strong> <span id="tracker-seeders">0</span>/<span id="tracker-leechers">0</span></p>
</div>
</div>
</div>
<div class="system-info">
<h3>System Information</h3>
<div class="info-grid">
<div class="info-item">
<label>Mode:</label>
<span id="system-mode">unified</span>
</div>
<div class="info-item">
<label>Uptime:</label>
<span id="system-uptime">--</span>
</div>
<div class="info-item">
<label>Total Storage:</label>
<span id="system-storage">0 MB</span>
</div>
<div class="info-item">
<label>Active Connections:</label>
<span id="system-connections">0</span>
</div>
</div>
</div>
</div>
<!-- User Files Section -->
<div id="files-section" class="section">
<div class="dashboard-header">
<h2>My Files Dashboard</h2>
<div class="dashboard-controls">
<div class="view-toggle">
<button id="grid-view-btn" class="view-btn active" onclick="setViewMode('grid')"></button>
<button id="list-view-btn" class="view-btn" onclick="setViewMode('list')"></button>
</div>
<button id="refresh-files" class="action-btn">↻ Refresh</button>
</div>
</div>
<div id="user-stats" class="user-stats">
<div class="stat-card">
<div class="stat-number" id="user-file-count">0</div>
<div class="stat-label">Files</div>
</div>
<div class="stat-card">
<div class="stat-number" id="user-storage-used">0 MB</div>
<div class="stat-label">Storage Used</div>
</div>
<div class="stat-card">
<div class="stat-placeholder">
<div class="stat-number">Coming Soon</div>
<div class="stat-note">Account limits & quotas</div>
</div>
<div class="stat-label">Storage Limit</div>
</div>
<div class="stat-card">
<div class="stat-number" id="user-last-login">Never</div>
<div class="stat-label">Last Login</div>
</div>
</div>
<div class="file-filters">
<button class="filter-btn active" onclick="filterFiles('all')">All Files</button>
<button class="filter-btn" onclick="filterFiles('blob')">Blobs</button>
<button class="filter-btn" onclick="filterFiles('torrent')">Torrents</button>
<button class="filter-btn" onclick="filterFiles('private')">Private</button>
</div>
<div id="file-container" class="file-container">
<div id="file-list" class="file-grid">
<p class="empty-state">No files uploaded yet.</p>
</div>
</div>
<div id="loading-files" class="loading-state" style="display: none;">
<div class="spinner"></div>
<p>Loading files...</p>
</div>
</div>
<!-- About Section -->
<div id="about-section" class="section active">
<div class="about-header">
<h2>⚡ BitTorrent Gateway</h2>
<p class="about-subtitle">Decentralized Content Distribution with Nostr Integration</p>
</div>
<div class="about-content">
<div class="intro-section">
<h3>What This Platform Does</h3>
<p>The BitTorrent Gateway is a next-generation content distribution system that combines the reliability of traditional web hosting with the power of peer-to-peer networks and decentralized social discovery through Nostr. It automatically chooses the best way to store and distribute your files:</p>
<div class="storage-flow">
<div class="flow-item">
<div class="flow-icon">📄</div>
<div class="flow-content">
<strong>Small Files (&lt;100MB)</strong>
<p>Stored as blobs for instant access and immediate availability</p>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-item">
<div class="flow-icon">🧩</div>
<div class="flow-content">
<strong>Large Files (≥100MB)</strong>
<p>Automatically chunked into 2MB pieces with torrent + DHT distribution</p>
</div>
</div>
</div>
<div class="key-benefits">
<h4>Why This Approach Works</h4>
<div class="benefit-list">
<div class="benefit-item">
<span class="benefit-icon">🚀</span>
<div class="benefit-content">
<strong>Always Available:</strong> Files are instantly accessible via WebSeed, with P2P peers providing additional bandwidth
</div>
</div>
<div class="benefit-item">
<span class="benefit-icon">🌐</span>
<div class="benefit-content">
<strong>Scales Automatically:</strong> Popular files get more peers, reducing server load naturally
</div>
</div>
<div class="benefit-item">
<span class="benefit-icon">🔒</span>
<div class="benefit-content">
<strong>Censorship Resistant:</strong> Content announced on Nostr can't be taken down by any single party
</div>
</div>
<div class="benefit-item">
<span class="benefit-icon">💰</span>
<div class="benefit-content">
<strong>Cost Effective:</strong> P2P distribution reduces bandwidth costs for large files
</div>
</div>
</div>
</div>
</div>
<div class="nostr-integration">
<h3>🌟 How Nostr Integration Works</h3>
<p class="nostr-intro">Nostr (Notes and Other Stuff Transmitted by Relays) is a decentralized protocol that enables censorship-resistant social networking. Here's how we integrate it for content distribution:</p>
<div class="nostr-flow">
<div class="nostr-step">
<div class="step-number">1</div>
<div class="step-content">
<h5>Content Upload</h5>
<p>You upload a file using your Nostr identity (no email or passwords needed)</p>
</div>
</div>
<div class="flow-arrow"></div>
<div class="nostr-step">
<div class="step-number">2</div>
<div class="step-content">
<h5>Automatic Processing</h5>
<p>System creates torrents, generates magnet links, and sets up WebSeed URLs</p>
</div>
</div>
<div class="flow-arrow"></div>
<div class="nostr-step">
<div class="step-number">3</div>
<div class="step-content">
<h5>Nostr Announcement</h5>
<p>File metadata is broadcast to Nostr relays as a structured event</p>
</div>
</div>
<div class="flow-arrow"></div>
<div class="nostr-step">
<div class="step-number">4</div>
<div class="step-content">
<h5>Social Discovery</h5>
<p>Other users can discover, comment on, and share your content through Nostr clients</p>
</div>
</div>
</div>
<div class="nostr-benefits">
<h4>Benefits of Nostr Integration:</h4>
<ul>
<li><strong>No Central Control:</strong> Content discovery happens through the distributed Nostr relay network</li>
<li><strong>Social Features:</strong> Users can comment, react, and share content using any Nostr client</li>
<li><strong>Network Effects:</strong> Content spreads naturally through social connections</li>
<li><strong>Privacy:</strong> You control your identity and data through cryptographic keys</li>
<li><strong>Interoperability:</strong> Works with existing Nostr apps and clients</li>
</ul>
</div>
<div class="nostr-example">
<h4>Example Nostr Event:</h4>
<div class="code-block">
<pre>{
"kind": 1063,
"content": "New file: example-video.mp4",
"tags": [
["magnet", "magnet:?xt=urn:btih:..."],
["size", "157286400"],
["name", "example-video.mp4"],
["webseed", "https://gateway.example.com/api/webseed/..."],
["mimetype", "video/mp4"]
]
}</pre>
</div>
<p class="code-explanation">This event gets distributed across Nostr relays, allowing anyone subscribed to file announcements to discover new content.</p>
</div>
</div>
<div class="architecture-section">
<h3>How It Works</h3>
<div class="arch-overview">
<h4>📡 P2P Coordination</h4>
<p>The system features a sophisticated <strong>P2P coordinator</strong> that manages all networking components, providing unified peer discovery, smart peer ranking, load balancing, and health monitoring across tracker, DHT, and WebSeed sources.</p>
<h4>🎯 Built-in BitTorrent Tracker</h4>
<div class="tech-details">
<p>Includes a full BitTorrent tracker with advanced features:</p>
<ul>
<li><strong>Client Compatibility:</strong> Optimized for qBittorrent, Transmission, WebTorrent, Deluge, uTorrent</li>
<li><strong>Abuse Prevention:</strong> Advanced detection and rate limiting systems</li>
<li><strong>Geographic Optimization:</strong> Proximity-based peer selection for faster transfers</li>
</ul>
</div>
<h4>🔄 WebSeed Implementation (BEP-19)</h4>
<div class="tech-details">
<p>Advanced WebSeed features for guaranteed availability:</p>
<ul>
<li><strong>LRU Caching:</strong> Intelligent piece caching with configurable size limits</li>
<li><strong>Concurrent Optimization:</strong> Prevents duplicate loads, manages request limits</li>
<li><strong>Standards Compliant:</strong> Full BEP-19 specification support</li>
</ul>
</div>
<h4>🌊 File Processing Pipeline</h4>
<div class="pipeline-steps">
<div class="step">
<span class="step-number">1</span>
<div class="step-content">
<strong>Upload & Analysis</strong>
<p>File received, SHA-256 hash calculated, size analysis determines storage strategy</p>
</div>
</div>
<div class="step">
<span class="step-number">2</span>
<div class="step-content">
<strong>Storage & Distribution</strong>
<p>Small files stored as blobs, large files chunked and torrents created</p>
</div>
</div>
<div class="step">
<span class="step-number">3</span>
<div class="step-content">
<strong>P2P Announcement</strong>
<p>Content announced to DHT network and Nostr relays for discovery</p>
</div>
</div>
<div class="step">
<span class="step-number">4</span>
<div class="step-content">
<strong>Immediate Availability</strong>
<p>Content immediately available via WebSeed, P2P peers join over time</p>
</div>
</div>
</div>
</div>
</div>
<div class="features-section">
<h3>Key Features</h3>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-icon">🚀</div>
<div class="feature-content">
<h4>Intelligent Content Distribution</h4>
<p>Automatic selection between blob storage and BitTorrent based on file size, with WebSeed fallback ensuring 100% availability</p>
</div>
</div>
<div class="feature-card">
<div class="feature-icon">🌐</div>
<div class="feature-content">
<h4>Multi-Protocol Support</h4>
<p>Full BitTorrent protocol, WebSeed (BEP-19), DHT, and Nostr integration for comprehensive P2P networking</p>
</div>
</div>
<div class="feature-card">
<div class="feature-icon">🔒</div>
<div class="feature-content">
<h4>Production-Ready Security</h4>
<p>Multi-layer rate limiting, advanced abuse detection, input sanitization, and comprehensive security headers</p>
</div>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<div class="feature-content">
<h4>Comprehensive Monitoring</h4>
<p>Real-time P2P health monitoring with 0-100 scoring system, performance metrics, and automatic alerting</p>
</div>
</div>
<div class="feature-card">
<div class="feature-icon">🎯</div>
<div class="feature-content">
<h4>BitTorrent Client Compatibility</h4>
<p>Optimized for popular clients with client-specific adjustments and proper announce intervals</p>
</div>
</div>
<div class="feature-card">
<div class="feature-icon"></div>
<div class="feature-content">
<h4>Smart Caching & Optimization</h4>
<p>LRU caching system, concurrent processing, geographic peer selection, and connection pooling</p>
</div>
</div>
</div>
</div>
<div class="services-section">
<h3>System Architecture</h3>
<div class="component-list">
<div class="component">
<h4>🚀 Gateway Server (Port 9877)</h4>
<p>Main API server with WebSeed implementation, smart proxy for chunked content reassembly, advanced LRU caching system, and comprehensive security middleware.</p>
<div class="component-specs">
<span>WebSeed (BEP-19) • Rate Limiting • Abuse Prevention</span>
</div>
</div>
<div class="component">
<h4>🌸 Blossom Server (Port 8082)</h4>
<p>Content-addressed blob storage with Nostr protocol compatibility, SHA-256 addressing, and direct storage for files under 100MB.</p>
<div class="component-specs">
<span>Nostr Compatible • Content Addressing • Efficient Blob Storage</span>
</div>
</div>
<div class="component">
<h4>🕸️ DHT Node (Port 6883)</h4>
<p>Full Kademlia DHT implementation with bootstrap connectivity to major networks, automatic torrent announcement, and peer discovery.</p>
<div class="component-specs">
<span>Distributed Peer Discovery • Bootstrap Networks • Announce</span>
</div>
</div>
<div class="component">
<h4>📡 Built-in BitTorrent Tracker</h4>
<p>Full announce/scrape protocol support, client compatibility optimizations, abuse detection, and peer management with geographic proximity selection.</p>
<div class="component-specs">
<span>Multi-Client Support • Abuse Detection • Smart Peer Selection</span>
</div>
</div>
<div class="component">
<h4>🎯 P2P Coordinator</h4>
<p>Unified management of all P2P components, smart peer ranking algorithm, load balancing across sources, and comprehensive health monitoring.</p>
<div class="component-specs">
<span>Unified Discovery • Smart Ranking • Health Monitoring</span>
</div>
</div>
</div>
</div>
<div class="api-section">
<h3>API Reference</h3>
<div class="api-tabs">
<button class="api-tab active" onclick="showApiTab('core')">Core API</button>
<button class="api-tab" onclick="showApiTab('streaming')">Streaming</button>
<button class="api-tab" onclick="showApiTab('blossom')">Blossom</button>
<button class="api-tab" onclick="showApiTab('tracker')">Tracker</button>
</div>
<div id="api-core" class="api-content active">
<h4>Core API Endpoints</h4>
<div class="endpoint-list">
<div class="endpoint">
<span class="method post">POST</span>
<code>/api/upload</code>
<span class="desc">Upload files with intelligent storage routing</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/api/download/{hash}</code>
<span class="desc">Download files by hash (range requests supported)</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/api/torrent/{hash}</code>
<span class="desc">Get .torrent file for BitTorrent clients</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/api/stats</code>
<span class="desc">Overall system statistics</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/api/p2p/stats</code>
<span class="desc">Detailed P2P statistics</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/api/health</code>
<span class="desc">Component health status</span>
</div>
</div>
</div>
<div id="api-streaming" class="api-content">
<h4>WebSeed & P2P Endpoints</h4>
<div class="endpoint-list">
<div class="endpoint">
<span class="method get">GET</span>
<code>/api/webseed/{hash}</code>
<span class="desc">WebSeed endpoint for BitTorrent clients (BEP-19)</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/api/webseed/health</code>
<span class="desc">WebSeed health check and cache statistics</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/api/diagnostics</code>
<span class="desc">Comprehensive system diagnostics</span>
</div>
</div>
<div class="webseed-info">
<h5>WebSeed Features:</h5>
<ul>
<li>✅ BEP-19 compliant WebSeed implementation</li>
<li>✅ Advanced LRU piece caching</li>
<li>✅ Concurrent request optimization</li>
<li>✅ Client-specific optimizations</li>
<li>✅ Range request support</li>
</ul>
</div>
</div>
<div id="api-blossom" class="api-content">
<h4>Blossom Protocol Endpoints</h4>
<div class="endpoint-list">
<div class="endpoint">
<span class="method put">PUT</span>
<code>/upload</code>
<span class="desc">Upload blob to Blossom server</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/{hash}</code>
<span class="desc">Download blob by SHA-256 hash</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/info</code>
<span class="desc">Server information and capabilities</span>
</div>
</div>
</div>
<div id="api-tracker" class="api-content">
<h4>BitTorrent Tracker Endpoints</h4>
<div class="endpoint-list">
<div class="endpoint">
<span class="method get">GET</span>
<code>/announce</code>
<span class="desc">BitTorrent announce endpoint for peer discovery</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/scrape</code>
<span class="desc">BitTorrent scrape endpoint for torrent statistics</span>
</div>
</div>
<div class="tracker-info">
<h5>Tracker Features:</h5>
<ul>
<li>✅ Built-in tracker eliminates external dependencies</li>
<li>✅ Always includes gateway as WebSeed peer</li>
<li>✅ Supports both compact and dictionary peer formats</li>
<li>✅ Automatic peer cleanup after 45 minutes</li>
<li>✅ Full BitTorrent protocol compliance</li>
</ul>
</div>
</div>
</div>
<div class="tech-section">
<h3>Technical Implementation</h3>
<div class="tech-grid">
<div class="tech-item">
<h4>Intelligent Storage Strategy</h4>
<p>Files under 100MB stored as blobs for immediate access. Larger files automatically chunked into 2MB pieces with BitTorrent-compatible structure for parallel transfers.</p>
</div>
<div class="tech-item">
<h4>Advanced P2P Coordination</h4>
<p>Sophisticated peer ranking algorithm considering geographic proximity (30%), source reliability (25%), and historical performance (20%) for optimal peer selection.</p>
</div>
<div class="tech-item">
<h4>Production-Grade Performance</h4>
<p>LRU piece caching, concurrent request handling, connection pooling, and comprehensive health monitoring with automatic alerting systems.</p>
</div>
<div class="tech-item">
<h4>Multi-Client Optimization</h4>
<p>Built-in tracker with client detection and specific optimizations for qBittorrent, Transmission, WebTorrent, and other popular BitTorrent clients.</p>
</div>
</div>
</div>
<div class="protocols-section">
<h3>Supported Protocols & Standards</h3>
<div class="protocol-badges">
<span class="protocol-badge">HTTP/1.1 Range Requests</span>
<span class="protocol-badge">BitTorrent Protocol</span>
<span class="protocol-badge">WebSeed (BEP-19)</span>
<span class="protocol-badge">HLS Streaming</span>
<span class="protocol-badge">Blossom Protocol</span>
<span class="protocol-badge">Nostr (NIP-35)</span>
<span class="protocol-badge">Kademlia DHT</span>
</div>
</div>
</div>
</div>
</main>
</div>
<div id="toast-container" class="toast-container"></div>
<!-- Login Modal -->
<div id="login-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" onclick="hideLogin()">&times;</span>
<h2>Login with Nostr</h2>
<div class="extension-info">
<h4>Need a Nostr Extension?</h4>
<div class="browser-extensions">
<div class="browser-group">
<strong>🌐 Chrome / Edge / Brave:</strong>
<ul>
<li><a href="https://getalby.com" target="_blank">Alby</a> - Most popular, supports Lightning</li>
<li><a href="https://github.com/fiatjaf/nos2x" target="_blank">nos2x</a> - Simple and lightweight</li>
</ul>
</div>
<div class="browser-group">
<strong>🦊 Firefox:</strong>
<ul>
<li><a href="https://github.com/fiatjaf/nos2x-fox" target="_blank">nos2x-fox</a> - Firefox version of nos2x</li>
<li><a href="https://getalby.com" target="_blank">Alby</a> - Also available for Firefox</li>
</ul>
</div>
</div>
</div>
<div class="login-methods">
<button id="nip07-login" class="login-btn">
Login with Browser Extension (NIP-07)
</button>
<button id="create-account-btn" class="login-btn" style="background-color: var(--bg-tertiary); color: var(--text-primary); margin-top: 10px;">
Don't have an account? Create one
</button>
</div>
<div class="bunker-section">
<h4>Remote Signer (NIP-46)</h4>
<input type="text" id="bunker-url" class="bunker-input"
placeholder="bunker://pubkey?relay=wss://relay.domain.com&secret=...">
<button id="nip46-login" class="login-btn">Connect Bunker</button>
</div>
<div id="login-status" class="status" style="display: none;"></div>
</div>
</div>
<!-- Share Modal -->
<div id="share-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" onclick="hideShareModal()">&times;</span>
<h2>Share File</h2>
<h3 id="share-file-name"></h3>
<div id="share-links" class="share-links">
<!-- Dynamic content -->
</div>
</div>
</div>
<!-- File Details Modal -->
<div id="file-details-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" onclick="hideFileDetailsModal()">&times;</span>
<h2>File Details</h2>
<div id="file-details-content">
<div class="file-preview-large">
<div id="file-icon-large"></div>
<h3 id="file-name-large"></h3>
</div>
<div class="file-metadata">
<div class="metadata-row">
<label>File Size:</label>
<span id="file-size-detail"></span>
</div>
<div class="metadata-row">
<label>Storage Type:</label>
<span id="file-storage-type"></span>
</div>
<div class="metadata-row">
<label>Upload Date:</label>
<span id="file-upload-date"></span>
</div>
<div class="metadata-row">
<label>File Hash:</label>
<span id="file-hash-detail" class="hash-text"></span>
</div>
<div class="metadata-row">
<label>Access Level:</label>
<div class="access-controls">
<select id="file-access-level">
<option value="public">Public</option>
<option value="private">Private</option>
</select>
<button id="update-access" class="action-btn">Update</button>
</div>
</div>
</div>
<div class="file-actions-detail">
<button class="action-btn" onclick="shareFileFromModal()">🔗 Share</button>
<button class="action-btn" onclick="downloadFileFromModal()">⬇ Download</button>
<button class="action-btn danger" onclick="deleteFileFromModal()">🗑 Delete</button>
</div>
</div>
</div>
</div>
<!-- Account Creation Modal -->
<div id="create-account-modal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 600px;">
<span class="close" onclick="hideCreateAccount()">&times;</span>
<h2>Create Nostr Account</h2>
<div class="account-explanation">
<h4>What is Nostr?</h4>
<p>Nostr is a decentralized protocol that gives you control over your digital identity. Unlike traditional accounts, there's no company storing your data or controlling your access.</p>
<h4>How it works:</h4>
<ul>
<li><strong>No passwords</strong> - You get a pair of cryptographic keys instead</li>
<li><strong>You own your identity</strong> - No one can delete or suspend your account</li>
<li><strong>Privacy focused</strong> - We never see or store your private key</li>
<li><strong>Works everywhere</strong> - Same identity across all Nostr apps</li>
</ul>
<h4>Your Keys Explained:</h4>
<div class="key-explanation">
<div class="key-info">
<strong>🔓 Public Key (npub):</strong>
<p>Like your username - safe to share publicly. Others use this to find you.</p>
</div>
<div class="key-info">
<strong>🔐 Private Key (nsec):</strong>
<p>Like your password but MORE IMPORTANT - never share this! It proves you own your identity.</p>
</div>
</div>
<div class="warning-box">
<h4>⚠️ IMPORTANT SECURITY NOTICE</h4>
<p>Your private key is generated in your browser and <strong>never sent to our servers</strong>. You MUST save it securely - if you lose it, your account cannot be recovered!</p>
</div>
</div>
<div class="key-generation" id="key-generation">
<button id="generate-keys" class="login-btn">Generate My Nostr Keys</button>
</div>
<div class="key-display" id="key-display" style="display: none;">
<h4>Your New Nostr Keys</h4>
<div class="key-pair">
<div class="key-item">
<label>🔓 Public Key (share this):</label>
<div class="key-row">
<input type="text" id="generated-npub" readonly onclick="this.select()">
<button onclick="copyKey('generated-npub')">Copy</button>
</div>
</div>
<div class="key-item">
<label>🔐 Private Key (SAVE THIS SECURELY!):</label>
<div class="key-row">
<input type="password" id="generated-nsec" readonly onclick="this.select()">
<button onclick="toggleKeyVisibility('generated-nsec')">Show</button>
<button onclick="copyKey('generated-nsec')">Copy</button>
</div>
</div>
</div>
<div class="save-instructions">
<h4>Save Your Private Key Now!</h4>
<p>Write down your private key (nsec) and store it safely. Recommended methods:</p>
<ul>
<li>✅ Password manager (1Password, Bitwarden, etc.)</li>
<li>✅ Written on paper stored securely</li>
<li>✅ Encrypted text file</li>
<li>❌ Screenshot or unencrypted file</li>
<li>❌ Cloud storage without encryption</li>
</ul>
</div>
<div class="extension-reminder">
<h4>Next Step: Install a Browser Extension</h4>
<p>To use your new Nostr keys, you'll need a browser extension:</p>
<div class="browser-extensions">
<div class="browser-group">
<strong>🌐 Chrome / Edge / Brave:</strong>
<ul>
<li><a href="https://getalby.com" target="_blank">Alby</a> - Most popular, supports Lightning</li>
<li><a href="https://github.com/fiatjaf/nos2x" target="_blank">nos2x</a> - Simple and lightweight</li>
</ul>
</div>
<div class="browser-group">
<strong>🦊 Firefox:</strong>
<ul>
<li><a href="https://github.com/fiatjaf/nos2x-fox" target="_blank">nos2x-fox</a> - Firefox version of nos2x</li>
<li><a href="https://getalby.com" target="_blank">Alby</a> - Also available for Firefox</li>
</ul>
</div>
</div>
<p><strong>After installing:</strong> Import your private key (nsec) into the extension, then come back here to login!</p>
</div>
<div class="account-actions">
<button onclick="hideCreateAccount()" class="login-btn">I've Saved My Keys Securely</button>
</div>
</div>
</div>
</div>
<script src="/static/nostr-crypto.js"></script>
<script src="/static/nostr-auth.js"></script>
<script src="/static/upload.js"></script>
<script>
// Navigation functions
function showUpload() {
if (!window.nostrAuth || !window.nostrAuth.isAuthenticated()) {
showToast('Please login to upload files', 'warning');
showLogin();
return;
}
hideAllSections();
document.getElementById('upload-section').classList.add('active');
}
function showFiles() {
if (!window.nostrAuth || !window.nostrAuth.isAuthenticated()) {
showLogin();
return;
}
hideAllSections();
const filesSection = document.getElementById('files-section');
filesSection.classList.add('active');
console.log('Files section classes:', filesSection.className);
console.log('Files section display:', window.getComputedStyle(filesSection).display);
loadUserFiles();
loadUserStats();
}
function showServices() {
hideAllSections();
document.getElementById('services-section').classList.add('active');
loadServiceStats();
}
function showAbout() {
hideAllSections();
document.getElementById('about-section').classList.add('active');
}
function showApiTab(tabName) {
// Hide all API content sections
document.querySelectorAll('.api-content').forEach(content => {
content.classList.remove('active');
});
// Remove active class from all tabs
document.querySelectorAll('.api-tab').forEach(tab => {
tab.classList.remove('active');
});
// Show selected content and activate tab
document.getElementById(`api-${tabName}`).classList.add('active');
event.target.classList.add('active');
}
function hideAllSections() {
document.querySelectorAll('.section').forEach(section => {
section.classList.remove('active');
});
}
// Auth functions
function showLogin() {
document.getElementById('login-modal').style.display = 'flex';
}
function hideLogin() {
document.getElementById('login-modal').style.display = 'none';
}
async function updateAuthStatus() {
const loginBtn = document.getElementById('login-btn');
const userInfo = document.getElementById('user-info');
const userPubkeyShort = document.getElementById('user-pubkey-short');
const filesLink = document.getElementById('files-link');
const adminLink = document.getElementById('admin-link');
const uploadLink = document.getElementById('upload-link');
if (window.nostrAuth && window.nostrAuth.isAuthenticated()) {
const pubkey = window.nostrAuth.getCurrentUser();
console.log('User authenticated, updating UI...', pubkey);
loginBtn.style.display = 'none';
userInfo.style.display = 'flex';
if (filesLink) {
filesLink.style.display = 'block';
console.log('Files link shown');
}
if (uploadLink) {
uploadLink.style.display = 'block';
console.log('Upload link shown');
}
// Check if user is admin
try {
const response = await fetch('/api/users/me/admin-status', {
credentials: 'include',
headers: {
'Authorization': `Bearer ${window.nostrAuth.sessionToken}`
}
});
if (response.ok) {
const data = await response.json();
adminLink.style.display = data.is_admin ? 'block' : 'none';
} else {
if (response.status === 401 || response.status === 403) {
// Clear invalid session data and update UI
window.nostrAuth.sessionToken = null;
window.nostrAuth.pubkey = null;
localStorage.removeItem('session_token');
localStorage.removeItem('user_pubkey');
updateAuthStatus();
return; // Exit early since auth state changed
}
adminLink.style.display = 'none';
}
} catch (error) {
adminLink.style.display = 'none';
}
// Show pubkey immediately, fetch profile in background
userPubkeyShort.textContent = pubkey.substring(0, 8) + '...';
// Fetch profile in background with retries
fetchUserProfile(pubkey, userPubkeyShort);
} else {
console.log('User not authenticated, hiding auth links');
loginBtn.style.display = 'block';
userInfo.style.display = 'none';
if (filesLink) {
filesLink.style.display = 'none';
}
if (adminLink) {
adminLink.style.display = 'none';
}
if (uploadLink) {
uploadLink.style.display = 'none';
}
}
}
async function fetchUserProfile(pubkey, displayElement, retryCount = 0) {
const maxRetries = 3;
const timeout = 3000; // 3 second timeout per attempt
try {
console.log(`Fetching profile (attempt ${retryCount + 1}/${maxRetries})...`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(`/api/profile/${pubkey}`, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) {
const data = await response.json();
if (data.success && data.profile) {
const profile = data.profile;
const displayName = profile.display_name || profile.name || (pubkey.substring(0, 8) + '...');
if (profile.picture) {
displayElement.innerHTML = `
<img src="${profile.picture}" style="width: 24px; height: 24px; border-radius: 50%; margin-right: 8px; vertical-align: middle;">
${displayName}
`;
} else {
displayElement.textContent = displayName;
}
console.log('Profile loaded successfully');
return;
}
}
throw new Error('Profile not found');
} catch (error) {
console.log(`Profile fetch attempt ${retryCount + 1} failed:`, error.message);
if (retryCount < maxRetries - 1) {
// Retry with exponential backoff
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
setTimeout(() => {
fetchUserProfile(pubkey, displayElement, retryCount + 1);
}, delay);
} else {
console.log('Profile fetching failed after all retries, keeping pubkey display');
}
}
}
function showStatus(message, isError = false) {
const status = document.getElementById('login-status');
status.textContent = message;
status.className = `status ${isError ? 'error' : 'success'}`;
status.style.display = 'block';
setTimeout(() => {
status.style.display = 'none';
}, 5000);
}
async function logout() {
if (window.nostrAuth) {
await window.nostrAuth.logout();
updateAuthStatus();
showUpload();
showToast('Logged out successfully', 'success');
}
}
// Dashboard state
let currentViewMode = 'grid';
let currentFilter = 'all';
let userFiles = [];
// Dashboard functions
function setViewMode(mode) {
currentViewMode = mode;
document.getElementById('grid-view-btn').classList.toggle('active', mode === 'grid');
document.getElementById('list-view-btn').classList.toggle('active', mode === 'list');
const fileList = document.getElementById('file-list');
fileList.className = mode === 'grid' ? 'file-grid' : 'file-list-view';
renderFiles();
}
function filterFiles(filter) {
currentFilter = filter;
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
renderFiles();
}
// User file management with debugging
async function loadUserStats() {
try {
console.log('Loading user stats...');
const stats = await window.nostrAuth.getUserStats();
console.log('User stats loaded:', stats);
document.getElementById('user-file-count').textContent = stats.file_count;
document.getElementById('user-storage-used').textContent = formatBytes(stats.storage_used);
document.getElementById('user-last-login').textContent = stats.last_login ?
new Date(stats.last_login).toLocaleDateString() : 'Never';
} catch (error) {
console.error('Failed to load user stats:', error);
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
// Update auth UI since session was cleared
updateAuthStatus();
}
}
}
async function loadUserFiles() {
const loadingEl = document.getElementById('loading-files');
const fileList = document.getElementById('file-list');
// Show loading state
loadingEl.style.display = 'block';
fileList.innerHTML = '<div class="loading-state"><div class="spinner"></div>LOADING FILES...</div>';
try {
console.log('Loading user files...');
console.log('Auth status:', window.nostrAuth.isAuthenticated());
console.log('Session token:', window.nostrAuth.sessionToken ? 'present' : 'missing');
const response = await window.nostrAuth.getUserFiles();
userFiles = response.files || [];
console.log('User files loaded:', userFiles);
if (userFiles.length === 0) {
fileList.innerHTML = '<div class="empty-state">NO FILES UPLOADED YET</div>';
} else {
renderFiles();
}
} catch (error) {
console.error('Failed to load user files:', error);
let errorMessage = 'FAILED TO LOAD FILES';
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
errorMessage = 'SESSION EXPIRED - PLEASE LOGIN AGAIN';
// Update auth UI since session was cleared
updateAuthStatus();
} else if (error.message.includes('403')) {
errorMessage = 'ACCESS DENIED';
} else if (error.message.includes('500')) {
errorMessage = 'SERVER ERROR - TRY AGAIN LATER';
} else if (error.message.includes('NetworkError') || error.message.includes('fetch')) {
errorMessage = 'CONNECTION ERROR - CHECK NETWORK';
}
fileList.innerHTML = `<div class="error-state">${errorMessage}<br><button onclick="loadUserFiles()" class="action-btn" style="margin-top: 10px;">RETRY</button></div>`;
} finally {
loadingEl.style.display = 'none';
}
}
function renderFiles() {
console.log('Rendering files:', userFiles.length, 'files');
console.log('Current filter:', currentFilter);
console.log('Current view mode:', currentViewMode);
const fileList = document.getElementById('file-list');
console.log('File list element:', fileList);
console.log('File list display:', fileList ? window.getComputedStyle(fileList).display : 'not found');
if (!fileList) {
console.error('file-list element not found!');
return;
}
// Filter files based on current filter
let filteredFiles = userFiles;
if (currentFilter !== 'all') {
filteredFiles = userFiles.filter(file => {
switch (currentFilter) {
case 'blob': return file.storage_type === 'blob';
case 'torrent': return file.storage_type === 'torrent';
case 'private': return file.access_level === 'private';
default: return true;
}
});
}
console.log('Filtered files:', filteredFiles.length, 'files');
if (filteredFiles.length === 0) {
fileList.innerHTML = '<p class="empty-state">No files found.</p>';
return;
}
if (currentViewMode === 'grid') {
console.log('Rendering grid view for', filteredFiles.length, 'files');
console.log('First file:', filteredFiles[0]);
try {
const html = filteredFiles.map(file => {
console.log('Processing file:', file.name, file.hash);
return `
<div class="file-card" data-hash="${file.hash}" onclick="showFileDetails('${file.hash}')">
<div class="file-preview">
${getFileIcon(file.name)}
</div>
<div class="file-info">
<h4 class="file-name" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</h4>
<div class="file-meta">
<span class="file-size">${formatBytes(file.size)}</span>
<span class="file-type">${file.storage_type}</span>
<span class="access-level ${file.access_level}">${file.access_level}</span>
</div>
<div class="file-date">${new Date(file.uploaded_at).toLocaleDateString()}</div>
</div>
<div class="file-actions" onclick="event.stopPropagation()">
<button class="action-btn" onclick="shareFile('${file.hash}')" title="Share">🔗</button>
<button class="action-btn" onclick="downloadFile('${file.hash}')" title="Download">⬇</button>
<button class="action-btn danger" onclick="deleteFile('${file.hash}')" title="Delete">🗑</button>
</div>
</div>
`;
}).join('');
console.log('Generated HTML length:', html.length);
fileList.innerHTML = html;
console.log('Grid HTML set successfully');
} catch (error) {
console.error('Error in grid rendering:', error);
}
} else {
fileList.innerHTML = filteredFiles.map(file => `
<div class="file-row" data-hash="${file.hash}">
<div class="file-icon">${getFileIcon(file.name)}</div>
<div class="file-details">
<div class="file-name">${escapeHtml(file.name)}</div>
<div class="file-meta">
${formatBytes(file.size)}${file.storage_type}${file.access_level}
${new Date(file.uploaded_at).toLocaleDateString()}
</div>
</div>
<div class="file-actions">
<button class="action-btn" onclick="shareFile('${file.hash}')">Share</button>
<button class="action-btn" onclick="downloadFile('${file.hash}')">Download</button>
<button class="action-btn danger" onclick="deleteFile('${file.hash}')">Delete</button>
</div>
</div>
`).join('');
}
}
function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
const iconMap = {
'mp4': '🎥', 'mkv': '🎥', 'avi': '🎥', 'mov': '🎥',
'mp3': '🎵', 'wav': '🎵', 'flac': '🎵', 'm4a': '🎵',
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'webp': '🖼️',
'pdf': '📄', 'doc': '📄', 'docx': '📄', 'txt': '📄',
'zip': '📦', 'rar': '📦', '7z': '📦', 'tar': '📦',
'exe': '⚙️', 'deb': '⚙️', 'dmg': '⚙️', 'msi': '⚙️'
};
return iconMap[ext] || '📁';
}
async function shareFile(hash) {
const file = userFiles.find(f => f.hash === hash);
if (!file) return;
const baseUrl = window.location.origin;
const links = {
direct: `${baseUrl}/api/download/${hash}`,
torrent: `${baseUrl}/api/torrent/${hash}`,
magnet: `magnet:?xt=urn:btih:${hash}&dn=${encodeURIComponent(file.name)}`,
stream: file.name.match(/\.(mp4|mkv|avi|mov)$/i) ? `${baseUrl}/player.html?hash=${hash}` : null
};
showShareModal(file, links);
}
function showShareModal(file, links) {
const modal = document.getElementById('share-modal');
const fileName = document.getElementById('share-file-name');
const linksContainer = document.getElementById('share-links');
fileName.textContent = file.name;
let linksHTML = `
<div class="share-link">
<label>Direct Download:</label>
<div class="link-row">
<input type="text" value="${links.direct}" readonly onclick="this.select()">
<button onclick="copyToClipboard('${links.direct}')">Copy</button>
</div>
</div>
<div class="share-link">
<label>Torrent File:</label>
<div class="link-row">
<input type="text" value="${links.torrent}" readonly onclick="this.select()">
<button onclick="copyToClipboard('${links.torrent}')">Copy</button>
</div>
</div>
<div class="share-link">
<label>Magnet Link:</label>
<div class="link-row">
<input type="text" value="${links.magnet}" readonly onclick="this.select()">
<button onclick="copyToClipboard('${links.magnet}')">Copy</button>
</div>
</div>
`;
if (links.stream) {
linksHTML += `
<div class="share-link">
<label>Stream Player:</label>
<div class="link-row">
<input type="text" value="${links.stream}" readonly onclick="this.select()">
<button onclick="copyToClipboard('${links.stream}')">Copy</button>
</div>
</div>
`;
}
linksContainer.innerHTML = linksHTML;
modal.style.display = 'flex';
}
function hideShareModal() {
document.getElementById('share-modal').style.display = 'none';
}
// File details modal
let currentFileDetails = null;
function showFileDetails(hash) {
const file = userFiles.find(f => f.hash === hash);
if (!file) return;
currentFileDetails = file;
document.getElementById('file-icon-large').innerHTML = getFileIcon(file.name);
document.getElementById('file-name-large').textContent = file.name;
document.getElementById('file-size-detail').textContent = formatBytes(file.size);
document.getElementById('file-storage-type').textContent = file.storage_type;
document.getElementById('file-upload-date').textContent = new Date(file.uploaded_at).toLocaleDateString();
document.getElementById('file-hash-detail').textContent = file.hash;
document.getElementById('file-access-level').value = file.access_level;
document.getElementById('file-details-modal').style.display = 'flex';
}
function hideFileDetailsModal() {
document.getElementById('file-details-modal').style.display = 'none';
currentFileDetails = null;
}
async function updateFileAccess() {
if (!currentFileDetails) return;
const newAccessLevel = document.getElementById('file-access-level').value;
if (newAccessLevel === currentFileDetails.access_level) return;
try {
const response = await window.nostrAuth.updateFileAccess(currentFileDetails.hash, newAccessLevel);
if (response.success) {
showToast('Access level updated successfully', 'success');
currentFileDetails.access_level = newAccessLevel;
// Update the file in userFiles array
const fileIndex = userFiles.findIndex(f => f.hash === currentFileDetails.hash);
if (fileIndex !== -1) {
userFiles[fileIndex].access_level = newAccessLevel;
}
renderFiles();
hideFileDetailsModal();
} else {
showToast('Failed to update access level: ' + response.message, 'error');
}
} catch (error) {
console.error('Error updating access level:', error);
showToast('Error updating access level: ' + error.message, 'error');
}
}
function shareFileFromModal() {
if (currentFileDetails) {
shareFile(currentFileDetails.hash);
hideFileDetailsModal();
}
}
function downloadFileFromModal() {
if (currentFileDetails) {
downloadFile(currentFileDetails.hash);
}
}
function deleteFileFromModal() {
if (currentFileDetails) {
hideFileDetailsModal();
deleteFile(currentFileDetails.hash);
}
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
showToast('Copied to clipboard!', 'success');
} catch (error) {
console.error('Failed to copy:', error);
showToast('Failed to copy to clipboard', 'error');
}
}
async function deleteFile(hash) {
if (!confirm('Are you sure you want to delete this file?')) return;
try {
await window.nostrAuth.deleteFile(hash);
showToast('File deleted successfully', 'success');
userFiles = userFiles.filter(f => f.hash !== hash);
renderFiles();
loadUserStats();
} catch (error) {
console.error('Failed to delete file:', error);
showToast('Failed to delete file: ' + error.message, 'error');
}
}
function downloadFile(hash) {
const a = document.createElement('a');
a.href = `/api/download/${hash}`;
a.download = '';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Service stats functions
async function loadServiceStats() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
if (data.gateway) {
document.getElementById('gateway-status').textContent = data.gateway.status === 'healthy' ? '🟢' : '🔴';
document.getElementById('gateway-uploads').textContent = data.gateway.uploads;
document.getElementById('gateway-storage').textContent = formatBytes(data.gateway.storage);
}
if (data.blossom) {
document.getElementById('blossom-status').textContent = data.blossom.status === 'healthy' ? '🟢' : '🔴';
document.getElementById('blossom-blobs').textContent = data.blossom.blobs;
document.getElementById('blossom-storage').textContent = formatBytes(data.blossom.storage);
}
if (data.dht) {
document.getElementById('dht-status').textContent = data.dht.status === 'healthy' ? '🟢' : '🔴';
document.getElementById('dht-peers').textContent = data.dht.peers;
document.getElementById('dht-torrents').textContent = data.dht.torrents;
}
if (data.tracker) {
document.getElementById('tracker-status').textContent = data.tracker.status === 'healthy' ? '🟢' : '🔴';
document.getElementById('tracker-torrents').textContent = data.tracker.torrents;
document.getElementById('tracker-peers').textContent = data.tracker.peers;
document.getElementById('tracker-seeders').textContent = data.tracker.seeders;
document.getElementById('tracker-leechers').textContent = data.tracker.leechers;
}
if (data.system) {
document.getElementById('system-mode').textContent = data.system.mode;
document.getElementById('system-uptime').textContent = data.system.uptime;
document.getElementById('system-storage').textContent = formatBytes(data.system.storage);
document.getElementById('system-connections').textContent = data.system.connections;
}
} catch (error) {
console.error('Failed to load service stats:', error);
// Set error indicators
document.querySelectorAll('.status-indicator').forEach(indicator => {
indicator.textContent = '🔴';
});
}
}
function refreshDHTStats() {
loadServiceStats();
}
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 5000);
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
updateAuthStatus();
// Login event listeners
document.getElementById('nip07-login').addEventListener('click', async () => {
const result = await window.nostrAuth.loginNIP07();
if (result.success) {
showStatus(result.message);
await updateAuthStatus();
setTimeout(() => {
hideLogin();
updateAuthStatus(); // Force another update for Firefox
showToast('Logged in successfully!', 'success');
}, 50);
} else {
showStatus(result.message, true);
}
});
document.getElementById('nip46-login').addEventListener('click', async () => {
const bunkerURL = document.getElementById('bunker-url').value.trim();
if (!bunkerURL) {
showStatus('Please enter a bunker URL', true);
return;
}
const result = await window.nostrAuth.loginNIP46(bunkerURL);
if (result.success) {
showStatus(result.message);
await updateAuthStatus();
setTimeout(() => {
hideLogin();
updateAuthStatus(); // Force another update for Firefox
showToast('Logged in successfully!', 'success');
}, 50);
} else {
showStatus(result.message, true);
}
});
document.getElementById('refresh-files').addEventListener('click', () => {
loadUserFiles();
loadUserStats();
});
// Auto-refresh services stats every 30 seconds if on services page
setInterval(() => {
if (document.getElementById('services-section').classList.contains('active')) {
loadServiceStats();
}
}, 30000);
document.getElementById('update-access').addEventListener('click', updateFileAccess);
// Account creation event listeners
document.getElementById('create-account-btn').addEventListener('click', () => {
hideLogin();
showCreateAccount();
});
document.getElementById('generate-keys').addEventListener('click', generateNostrKeys);
});
// Close modals when clicking outside
window.addEventListener('click', (event) => {
const loginModal = document.getElementById('login-modal');
const shareModal = document.getElementById('share-modal');
const fileDetailsModal = document.getElementById('file-details-modal');
if (event.target === loginModal) {
hideLogin();
}
if (event.target === shareModal) {
hideShareModal();
}
if (event.target === fileDetailsModal) {
hideFileDetailsModal();
}
});
// Account creation functions
function showCreateAccount() {
document.getElementById('create-account-modal').style.display = 'flex';
}
function hideCreateAccount() {
document.getElementById('create-account-modal').style.display = 'none';
// Reset the modal state
document.getElementById('key-generation').style.display = 'block';
document.getElementById('key-display').style.display = 'none';
}
async function generateNostrKeys() {
try {
showToast('Generating cryptographic keys...', 'info');
// Use the proper cryptographic implementation
const keyPair = await window.NostrCrypto.generateKeyPair();
// Display the keys
document.getElementById('generated-npub').value = keyPair.npub;
document.getElementById('generated-nsec').value = keyPair.nsec;
// Store for potential extension import
window.generatedKeys = keyPair;
// Show key display
document.getElementById('key-generation').style.display = 'none';
document.getElementById('key-display').style.display = 'block';
showToast('Keys generated successfully!', 'success');
} catch (error) {
console.error('Key generation failed:', error);
showToast('Failed to generate keys. Please try again.', 'error');
}
}
function copyKey(elementId) {
const element = document.getElementById(elementId);
element.select();
document.execCommand('copy');
showToast('Copied to clipboard!', 'success');
}
function toggleKeyVisibility(elementId) {
const element = document.getElementById(elementId);
const button = event.target;
if (element.type === 'password') {
element.type = 'text';
button.textContent = 'Hide';
} else {
element.type = 'password';
button.textContent = 'Show';
}
}
</script>
</body>
</html>