diff --git a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/overview.js b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/overview.js deleted file mode 100644 index 72ea627b..00000000 --- a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/overview.js +++ /dev/null @@ -1,1091 +0,0 @@ -'use strict'; -'require view'; -'require dom'; -'require ui'; -'require poll'; -'require exposure/api as api'; - -/** - * SecuBox Service Exposure Manager - Overview Dashboard - * Manages Tor Hidden Services and HAProxy SSL backends - * Progressive loading with debug console output - * Copyright (C) 2025 CyberMind.fr - */ - -return view.extend({ - title: _('Service Exposure Manager'), - - data: null, - pollRegistered: false, - loadStartTime: null, - DEBUG: true, - - // === Debug Logging with timestamps === - log: function(step, message, data) { - if (!this.DEBUG) return; - var elapsed = this.loadStartTime ? (Date.now() - this.loadStartTime) + 'ms' : '0ms'; - var prefix = '%c[Exposure ' + elapsed + '] %c' + step + '%c'; - if (data !== undefined) { - console.log(prefix, 'color: #64ffda; font-weight: bold;', 'color: #9b59b6; font-weight: bold;', 'color: #8892b0;', message, data); - } else { - console.log(prefix, 'color: #64ffda; font-weight: bold;', 'color: #9b59b6; font-weight: bold;', 'color: #8892b0;', message); - } - }, - - load: function() { - var self = this; - this.loadStartTime = Date.now(); - this.log('INIT', 'Starting dashboard load'); - - // Load CSS first - var cssLink = document.querySelector('link[href*="exposure/dashboard.css"]'); - if (!cssLink) { - var link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = L.resource('exposure/dashboard.css'); - document.head.appendChild(link); - this.log('CSS', 'Stylesheet injected'); - } - - // Progressive data loading with individual error handling - return this.loadDataProgressively(); - }, - - loadDataProgressively: function() { - var self = this; - - // Initialize result container - var result = { - status: { services: {}, tor: {}, ssl: {} }, - scan: { services: [] }, - conflicts: { conflicts: [] }, - tor: { services: [] }, - ssl: { backends: [] } - }; - - // Create promises with individual timing and error handling - // Priority: status > ssl > tor > scan > conflicts - - self.log('FETCH', '1/5 Status (priority: critical)'); - var p1 = api.status().then(function(data) { - result.status = data || result.status; - self.log('STATUS', 'Complete', { - total: (result.status.services || {}).total || 0, - external: (result.status.services || {}).external || 0, - tor: (result.status.tor || {}).count || 0, - ssl: (result.status.ssl || {}).count || 0 - }); - return data; - }).catch(function(err) { - self.log('ERROR', 'Status fetch failed: ' + err.message); - return result.status; - }); - - self.log('FETCH', '2/5 SSL Backends (priority: high)'); - var p2 = api.sslList().then(function(data) { - result.ssl = data || result.ssl; - var backends = result.ssl.backends || []; - self.log('SSL', 'Complete', { - count: backends.length, - domains: backends.map(function(b) { return b.domain; }) - }); - return data; - }).catch(function(err) { - self.log('ERROR', 'SSL list failed: ' + err.message); - return result.ssl; - }); - - self.log('FETCH', '3/5 Tor Services (priority: high)'); - var p3 = api.torList().then(function(data) { - result.tor = data || result.tor; - var services = result.tor.services || []; - self.log('TOR', 'Complete', { - count: services.length, - onions: services.map(function(s) { return s.service + ': ' + (s.onion || '').substring(0, 16) + '...'; }) - }); - return data; - }).catch(function(err) { - self.log('ERROR', 'Tor list failed: ' + err.message); - return result.tor; - }); - - self.log('FETCH', '4/5 Service Scan (priority: medium)'); - var p4 = api.scan().then(function(data) { - result.scan = data || result.scan; - var services = Array.isArray(data) ? data : (data.services || []); - var external = services.filter(function(s) { return s.external; }); - self.log('SCAN', 'Complete', { - total: services.length, - external: external.length, - processes: external.slice(0, 5).map(function(s) { return s.name || s.process; }) - }); - return data; - }).catch(function(err) { - self.log('ERROR', 'Scan failed: ' + err.message); - return result.scan; - }); - - self.log('FETCH', '5/5 Conflicts (priority: low)'); - var p5 = api.conflicts().then(function(data) { - result.conflicts = data || result.conflicts; - var conflicts = Array.isArray(data) ? data : (data.conflicts || []); - self.log('CONFLICTS', 'Complete', { count: conflicts.length }); - return data; - }).catch(function(err) { - self.log('ERROR', 'Conflicts check failed: ' + err.message); - return result.conflicts; - }); - - // Wait for all with graceful degradation - return Promise.all([p1, p2, p3, p4, p5]).then(function() { - var totalTime = Date.now() - self.loadStartTime; - self.log('LOADED', 'All data fetched in ' + totalTime + 'ms'); - return [result.status, result.scan, result.conflicts, result.tor, result.ssl]; - }); - }, - - render: function(data) { - var self = this; - var renderStart = Date.now(); - this.log('RENDER', 'Starting DOM construction'); - - var status = data[0] || {}; - var scanResult = data[1] || {}; - var conflictsResult = data[2] || {}; - var torResult = data[3] || {}; - var sslResult = data[4] || {}; - - // Normalize data - this.log('RENDER', 'Normalizing data structures'); - var services = Array.isArray(scanResult) ? scanResult : (scanResult.services || []); - var conflicts = Array.isArray(conflictsResult) ? conflictsResult : (conflictsResult.conflicts || []); - var torServices = Array.isArray(torResult) ? torResult : (torResult.services || []); - var sslBackends = Array.isArray(sslResult) ? sslResult : (sslResult.backends || []); - - this.log('DATA', 'Final counts', { - services: services.length, - conflicts: conflicts.length, - tor: torServices.length, - ssl: sslBackends.length - }); - - var exposableServices = services.filter(function(svc) { return svc.external; }); - - // Build exposure lookup maps - var exposedTor = {}; - var exposedSsl = {}; - torServices.forEach(function(t) { exposedTor[t.service] = t; }); - sslBackends.forEach(function(s) { exposedSsl[s.service] = s; }); - - // Find unexposed services (suggestions) - var suggestions = exposableServices.filter(function(svc) { - var name = self.getServiceName(svc); - return !exposedTor[name] && !exposedSsl[name]; - }); - - this.log('SUGGESTIONS', 'Unexposed services found', { - count: suggestions.length, - names: suggestions.slice(0, 5).map(function(s) { return s.name || s.process; }) - }); - - // Build view content progressively with timing - var content = []; - - this.log('DOM', '1/6 Page header'); - content.push(this.renderPageHeader(status)); - - if (conflicts.length > 0) { - this.log('DOM', '2/6 Conflicts banner (active)'); - content.push(this.renderConflictsBanner(conflicts)); - } else { - this.log('DOM', '2/6 Conflicts banner (skipped - none)'); - } - - this.log('DOM', '3/6 Stats grid'); - content.push(this.renderStatsGrid(status, torServices, sslBackends, exposableServices)); - - if (suggestions.length > 0) { - this.log('DOM', '4/6 Suggestions card (active)'); - content.push(this.renderSuggestionsCard(suggestions)); - } else { - this.log('DOM', '4/6 Suggestions card (skipped - none)'); - } - - this.log('DOM', '5/6 Service cards row'); - content.push(E('div', { 'class': 'exp-row' }, [ - E('div', { 'style': 'flex: 1' }, [ - this.renderTorServicesCard(torServices) - ]), - E('div', { 'style': 'flex: 1' }, [ - this.renderSslBackendsCard(sslBackends) - ]) - ])); - - this.log('DOM', '6/6 Quick actions'); - content.push(this.renderQuickActions()); - - // Filter nulls - content = content.filter(Boolean); - - // Main wrapper with animation - var view = E('div', { 'class': 'exposure-dashboard exp-fade-in' }, content); - - // Setup polling - if (!this.pollRegistered) { - this.pollRegistered = true; - this.log('POLL', 'Auto-refresh registered (30s interval)'); - poll.add(function() { - self.log('POLL', 'Refreshing dashboard...'); - return self.refreshDashboard(); - }, 30); - } - - var renderTime = Date.now() - renderStart; - var totalTime = Date.now() - this.loadStartTime; - this.log('COMPLETE', 'Dashboard ready', { - renderTime: renderTime + 'ms', - totalTime: totalTime + 'ms' - }); - - return view; - }, - - getServiceName: function(svc) { - var name = svc.name ? svc.name.toLowerCase().replace(/\s+/g, '') : svc.process; - return name.replace(/[^a-z0-9]/g, ''); - }, - - renderPageHeader: function(status) { - var servicesData = status.services || {}; - var torData = status.tor || {}; - var sslData = status.ssl || {}; - - return E('div', { 'class': 'exp-page-header' }, [ - E('div', {}, [ - E('h1', { 'class': 'exp-page-title' }, [ - E('span', { 'class': 'exp-page-title-icon' }, '\uD83D\uDD0C'), - 'Service Exposure Manager' - ]), - E('p', { 'class': 'exp-page-subtitle' }, - 'Expose local services via Tor Hidden Services (.onion) or HAProxy SSL reverse proxy') - ]), - E('div', { 'class': 'exp-header-badges' }, [ - E('div', { 'class': 'exp-header-badge' }, [ - E('span', { 'style': 'color: #64ffda;' }, String(servicesData.external || 0)), - ' Exposable' - ]), - E('div', { 'class': 'exp-header-badge' }, [ - E('span', { 'style': 'color: #9b59b6;' }, String(torData.count || 0)), - ' Tor' - ]), - E('div', { 'class': 'exp-header-badge' }, [ - E('span', { 'style': 'color: #27ae60;' }, String(sslData.count || 0)), - ' SSL' - ]) - ]) - ]); - }, - - renderConflictsBanner: function(conflicts) { - return E('div', { 'class': 'exp-card exp-warning-card' }, [ - E('div', { 'class': 'exp-card-body', 'style': 'display: flex; align-items: center; gap: 16px;' }, [ - E('span', { 'style': 'font-size: 32px;' }, '\u26A0\uFE0F'), - E('div', { 'style': 'flex: 1;' }, [ - E('div', { 'style': 'font-weight: 600; font-size: 16px; margin-bottom: 4px; color: #f39c12;' }, - conflicts.length + ' Port Conflict(s) Detected'), - E('div', { 'style': 'color: #8892b0;' }, - conflicts.map(function(c) { - return 'Port ' + c.port + ': ' + (c.services || []).join(', '); - }).join(' | ')) - ]) - ]) - ]); - }, - - renderStatsGrid: function(status, torServices, sslBackends, exposableServices) { - var servicesData = status.services || {}; - - return E('div', { 'class': 'exp-stats-grid' }, [ - E('div', { 'class': 'exp-stat-card' }, [ - E('div', { 'class': 'exp-stat-icon' }, '\uD83D\uDD0C'), - E('div', { 'class': 'exp-stat-value' }, String(servicesData.total || 0)), - E('div', { 'class': 'exp-stat-label' }, 'Total Services'), - E('div', { 'class': 'exp-stat-trend' }, (servicesData.external || 0) + ' external') - ]), - E('div', { 'class': 'exp-stat-card exp-stat-tor' }, [ - E('div', { 'class': 'exp-stat-icon' }, '\uD83E\uDDC5'), - E('div', { 'class': 'exp-stat-value' }, String(torServices.length)), - E('div', { 'class': 'exp-stat-label' }, 'Tor Hidden Services'), - E('div', { 'class': 'exp-stat-trend' }, torServices.length > 0 ? 'Active' : 'None configured') - ]), - E('div', { 'class': 'exp-stat-card exp-stat-ssl' }, [ - E('div', { 'class': 'exp-stat-icon' }, '\uD83D\uDD12'), - E('div', { 'class': 'exp-stat-value' }, String(sslBackends.length)), - E('div', { 'class': 'exp-stat-label' }, 'SSL Backends'), - E('div', { 'class': 'exp-stat-trend' }, sslBackends.length > 0 ? 'HAProxy' : 'None configured') - ]), - E('div', { 'class': 'exp-stat-card' }, [ - E('div', { 'class': 'exp-stat-icon' }, '\uD83D\uDCA1'), - E('div', { 'class': 'exp-stat-value' }, String(exposableServices.length)), - E('div', { 'class': 'exp-stat-label' }, 'Exposable'), - E('div', { 'class': 'exp-stat-trend' }, 'Ready for exposure') - ]) - ]); - }, - - renderSuggestionsCard: function(suggestions) { - var self = this; - - // Prioritize interesting services - var prioritized = suggestions.sort(function(a, b) { - var priority = { - 'streamlit': 1, 'Streamlit': 1, - 'gitea': 2, 'Gitea': 2, - 'hexo': 3, 'HexoJS': 3, - 'nextcloud': 4, - 'jupyter': 5, - 'flask': 6, - 'django': 7 - }; - var pA = priority[a.name] || priority[a.process] || 100; - var pB = priority[b.name] || priority[b.process] || 100; - return pA - pB; - }).slice(0, 6); - - return E('div', { 'class': 'exp-card exp-suggestions-card' }, [ - E('div', { 'class': 'exp-card-header' }, [ - E('div', { 'class': 'exp-card-title' }, [ - E('span', { 'class': 'exp-card-title-icon' }, '\uD83D\uDCA1'), - 'Suggested Services to Expose' - ]), - E('a', { 'href': L.url('admin/secubox/network/exposure/services'), 'class': 'exp-btn exp-btn-secondary exp-btn-sm' }, - 'View All') - ]), - E('div', { 'class': 'exp-card-body' }, [ - E('div', { 'class': 'exp-suggestions-grid' }, prioritized.map(function(svc) { - var serviceName = self.getServiceName(svc); - var icon = self.getServiceIcon(svc); - - return E('div', { 'class': 'exp-suggestion-item' }, [ - E('div', { 'class': 'exp-suggestion-icon' }, icon), - E('div', { 'class': 'exp-suggestion-info' }, [ - E('div', { 'class': 'exp-suggestion-name' }, svc.name || svc.process), - E('div', { 'class': 'exp-suggestion-port' }, 'Port ' + svc.port) - ]), - E('div', { 'class': 'exp-suggestion-actions' }, [ - E('button', { - 'class': 'exp-btn exp-btn-tor exp-btn-xs', - 'title': 'Add Tor Hidden Service', - 'click': function() { self.handleAddTor(svc, serviceName); } - }, '\uD83E\uDDC5'), - E('button', { - 'class': 'exp-btn exp-btn-ssl exp-btn-xs', - 'title': 'Add SSL Backend', - 'click': function() { self.handleAddSsl(svc, serviceName); } - }, '\uD83D\uDD12') - ]) - ]); - })) - ]) - ]); - }, - - getServiceIcon: function(svc) { - var iconMap = { - 'streamlit': '\uD83D\uDCCA', - 'Streamlit': '\uD83D\uDCCA', - 'gitea': '\uD83D\uDC19', - 'Gitea': '\uD83D\uDC19', - 'hexo': '\uD83D\uDCDD', - 'HexoJS': '\uD83D\uDCDD', - 'jupyter': '\uD83D\uDCD3', - 'flask': '\uD83C\uDF76', - 'django': '\uD83E\uDD8E', - 'nextcloud': '\u2601\uFE0F', - 'SSH': '\uD83D\uDD11', - 'HAProxy': '\u2696\uFE0F', - 'DNS': '\uD83C\uDF10', - 'LuCI': '\uD83D\uDDA5\uFE0F', - 'python': '\uD83D\uDC0D' - }; - return iconMap[svc.name] || iconMap[svc.process] || '\uD83D\uDD0C'; - }, - - renderTorServicesCard: function(torServices) { - var self = this; - - if (torServices.length === 0) { - return E('div', { 'class': 'exp-card' }, [ - E('div', { 'class': 'exp-card-header' }, [ - E('div', { 'class': 'exp-card-title' }, [ - E('span', { 'class': 'exp-card-title-icon' }, '\uD83E\uDDC5'), - 'Tor Hidden Services' - ]), - E('a', { 'href': L.url('admin/secubox/network/exposure/tor'), 'class': 'exp-btn exp-btn-tor exp-btn-sm' }, - '+ Add') - ]), - E('div', { 'class': 'exp-card-body' }, [ - E('div', { 'class': 'exp-empty' }, [ - E('div', { 'class': 'exp-empty-icon' }, '\uD83E\uDDC5'), - E('div', { 'class': 'exp-empty-text' }, 'No Tor hidden services'), - E('div', { 'class': 'exp-empty-hint' }, 'Services are accessible via .onion addresses') - ]) - ]) - ]); - } - - var items = torServices.slice(0, 5).map(function(svc) { - var onion = svc.onion || ''; - var shortOnion = onion.length > 30 ? onion.substring(0, 28) + '...' : onion; - - return E('div', { 'class': 'exp-service-item' }, [ - E('div', { 'class': 'exp-service-icon' }, '\uD83E\uDDC5'), - E('div', { 'class': 'exp-service-info' }, [ - E('div', { 'class': 'exp-service-name' }, svc.service), - E('div', { 'class': 'exp-service-detail exp-onion' }, shortOnion) - ]), - E('button', { - 'class': 'exp-btn exp-btn-danger exp-btn-xs', - 'title': 'Remove', - 'click': function() { self.handleRemoveTor(svc.service); } - }, '\u2715') - ]); - }); - - var cardBody = [E('div', { 'class': 'exp-services-list' }, items)]; - - if (torServices.length > 5) { - cardBody.push(E('div', { 'style': 'text-align: center; margin-top: 12px;' }, - E('a', { 'href': L.url('admin/secubox/network/exposure/tor') }, - '+' + (torServices.length - 5) + ' more'))); - } - - return E('div', { 'class': 'exp-card' }, [ - E('div', { 'class': 'exp-card-header' }, [ - E('div', { 'class': 'exp-card-title' }, [ - E('span', { 'class': 'exp-card-title-icon' }, '\uD83E\uDDC5'), - 'Tor Hidden Services (' + torServices.length + ')' - ]), - E('a', { 'href': L.url('admin/secubox/network/exposure/tor'), 'class': 'exp-btn exp-btn-secondary exp-btn-sm' }, - 'Manage') - ]), - E('div', { 'class': 'exp-card-body' }, cardBody) - ]); - }, - - renderSslBackendsCard: function(sslBackends) { - var self = this; - - if (sslBackends.length === 0) { - return E('div', { 'class': 'exp-card' }, [ - E('div', { 'class': 'exp-card-header' }, [ - E('div', { 'class': 'exp-card-title' }, [ - E('span', { 'class': 'exp-card-title-icon' }, '\uD83D\uDD12'), - 'SSL Backends' - ]), - E('a', { 'href': L.url('admin/secubox/network/exposure/ssl'), 'class': 'exp-btn exp-btn-ssl exp-btn-sm' }, - '+ Add') - ]), - E('div', { 'class': 'exp-card-body' }, [ - E('div', { 'class': 'exp-empty' }, [ - E('div', { 'class': 'exp-empty-icon' }, '\uD83D\uDD12'), - E('div', { 'class': 'exp-empty-text' }, 'No SSL backends'), - E('div', { 'class': 'exp-empty-hint' }, 'Add HTTPS reverse proxy via HAProxy') - ]) - ]) - ]); - } - - var items = sslBackends.slice(0, 5).map(function(backend) { - return E('div', { 'class': 'exp-service-item' }, [ - E('div', { 'class': 'exp-service-icon' }, '\uD83D\uDD12'), - E('div', { 'class': 'exp-service-info' }, [ - E('div', { 'class': 'exp-service-name' }, backend.service), - E('div', { 'class': 'exp-service-detail exp-domain' }, backend.domain) - ]), - E('button', { - 'class': 'exp-btn exp-btn-danger exp-btn-xs', - 'title': 'Remove', - 'click': function() { self.handleRemoveSsl(backend.service); } - }, '\u2715') - ]); - }); - - var cardBody = [E('div', { 'class': 'exp-services-list' }, items)]; - - if (sslBackends.length > 5) { - cardBody.push(E('div', { 'style': 'text-align: center; margin-top: 12px;' }, - E('a', { 'href': L.url('admin/secubox/network/exposure/ssl') }, - '+' + (sslBackends.length - 5) + ' more'))); - } - - return E('div', { 'class': 'exp-card' }, [ - E('div', { 'class': 'exp-card-header' }, [ - E('div', { 'class': 'exp-card-title' }, [ - E('span', { 'class': 'exp-card-title-icon' }, '\uD83D\uDD12'), - 'SSL Backends (' + sslBackends.length + ')' - ]), - E('a', { 'href': L.url('admin/secubox/network/exposure/ssl'), 'class': 'exp-btn exp-btn-secondary exp-btn-sm' }, - 'Manage') - ]), - E('div', { 'class': 'exp-card-body' }, cardBody) - ]); - }, - - renderQuickActions: function() { - var self = this; - - return E('div', { 'class': 'exp-card' }, [ - E('div', { 'class': 'exp-card-header' }, [ - E('div', { 'class': 'exp-card-title' }, [ - E('span', { 'class': 'exp-card-title-icon' }, '\u26A1'), - 'Quick Actions' - ]) - ]), - E('div', { 'class': 'exp-card-body' }, [ - E('div', { 'class': 'exp-quick-actions' }, [ - E('a', { - 'href': L.url('admin/secubox/network/exposure/services'), - 'class': 'exp-action-btn' - }, [ - E('span', { 'class': 'exp-action-icon' }, '\uD83D\uDD0C'), - E('span', { 'class': 'exp-action-label' }, 'All Services') - ]), - E('a', { - 'href': L.url('admin/secubox/network/exposure/tor'), - 'class': 'exp-action-btn' - }, [ - E('span', { 'class': 'exp-action-icon' }, '\uD83E\uDDC5'), - E('span', { 'class': 'exp-action-label' }, 'Tor Services') - ]), - E('a', { - 'href': L.url('admin/secubox/network/exposure/ssl'), - 'class': 'exp-action-btn' - }, [ - E('span', { 'class': 'exp-action-icon' }, '\uD83D\uDD12'), - E('span', { 'class': 'exp-action-label' }, 'SSL Backends') - ]), - E('button', { - 'class': 'exp-action-btn', - 'click': function() { self.refreshDashboard(); } - }, [ - E('span', { 'class': 'exp-action-icon' }, '\uD83D\uDD04'), - E('span', { 'class': 'exp-action-label' }, 'Refresh') - ]) - ]) - ]) - ]); - }, - - // === Progress Modal Helpers === - - createProgressModal: function(title, steps) { - var stepsContainer = E('div', { 'class': 'exp-progress-steps', 'id': 'progress-steps' }); - - steps.forEach(function(step, index) { - stepsContainer.appendChild(E('div', { - 'class': 'exp-progress-step', - 'id': 'step-' + index, - 'data-status': 'pending' - }, [ - E('div', { 'class': 'exp-step-indicator' }, [ - E('span', { 'class': 'exp-step-number' }, String(index + 1)), - E('span', { 'class': 'exp-step-icon' }) - ]), - E('div', { 'class': 'exp-step-content' }, [ - E('div', { 'class': 'exp-step-label' }, step.label), - E('div', { 'class': 'exp-step-detail', 'id': 'step-detail-' + index }, step.detail || '') - ]) - ])); - }); - - return E('div', { 'class': 'exp-progress-modal' }, [ - E('div', { 'class': 'exp-progress-header' }, [ - E('div', { 'class': 'exp-progress-title' }, title), - E('div', { 'class': 'exp-progress-subtitle', 'id': 'progress-subtitle' }, 'Initializing...') - ]), - stepsContainer, - E('div', { 'class': 'exp-progress-result', 'id': 'progress-result', 'style': 'display: none;' }) - ]); - }, - - updateStep: function(index, status, detail) { - var stepEl = document.getElementById('step-' + index); - var detailEl = document.getElementById('step-detail-' + index); - var subtitleEl = document.getElementById('progress-subtitle'); - - if (stepEl) { - stepEl.setAttribute('data-status', status); - if (detail && detailEl) { - detailEl.textContent = detail; - } - } - - if (subtitleEl && status === 'active') { - var labelEl = stepEl ? stepEl.querySelector('.exp-step-label') : null; - if (labelEl) { - subtitleEl.textContent = labelEl.textContent + '...'; - } - } - }, - - showProgressResult: function(success, message, details) { - var resultEl = document.getElementById('progress-result'); - var subtitleEl = document.getElementById('progress-subtitle'); - - if (resultEl) { - resultEl.style.display = 'block'; - resultEl.className = 'exp-progress-result ' + (success ? 'success' : 'error'); - resultEl.innerHTML = ''; - resultEl.appendChild(E('div', { 'class': 'exp-result-icon' }, success ? '\u2705' : '\u274C')); - resultEl.appendChild(E('div', { 'class': 'exp-result-message' }, message)); - if (details) { - resultEl.appendChild(E('div', { 'class': 'exp-result-details' }, details)); - } - } - - if (subtitleEl) { - subtitleEl.textContent = success ? 'Completed successfully' : 'Operation failed'; - } - }, - - // === Action Handlers === - - handleAddTor: function(svc, serviceName) { - var self = this; - - ui.showModal('Add Tor Hidden Service', [ - E('p', {}, 'Create a .onion address for ' + (svc.name || svc.process)), - E('div', { 'style': 'margin: 1rem 0;' }, [ - E('div', { 'style': 'margin-bottom: 0.75rem;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; color: #ccc; font-size: 0.9rem;' }, 'Service Name'), - E('input', { - 'type': 'text', - 'id': 'tor-svc-name', - 'value': serviceName, - 'style': 'width: 100%; padding: 10px; background: #1a1a2e; border: 1px solid #333; color: #fff; border-radius: 6px;' - }) - ]), - E('div', { 'style': 'margin-bottom: 0.75rem;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; color: #ccc; font-size: 0.9rem;' }, 'Local Port'), - E('input', { - 'type': 'number', - 'id': 'tor-local-port', - 'value': svc.port, - 'style': 'width: 100%; padding: 10px; background: #1a1a2e; border: 1px solid #333; color: #fff; border-radius: 6px;' - }) - ]), - E('div', {}, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; color: #ccc; font-size: 0.9rem;' }, 'Onion Port (public)'), - E('input', { - 'type': 'number', - 'id': 'tor-onion-port', - 'value': '80', - 'style': 'width: 100%; padding: 10px; background: #1a1a2e; border: 1px solid #333; color: #fff; border-radius: 6px;' - }) - ]) - ]), - E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 8px; margin-top: 1rem;' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, 'Cancel'), - E('button', { - 'class': 'btn cbi-button-action', - 'click': function() { - var name = document.getElementById('tor-svc-name').value; - var localPort = parseInt(document.getElementById('tor-local-port').value); - var onionPort = parseInt(document.getElementById('tor-onion-port').value); - - if (!name) { - ui.addNotification(null, E('p', {}, 'Service name is required'), 'warning'); - return; - } - - ui.hideModal(); - - // Show progress modal - var progressContent = self.createProgressModal('Creating Tor Hidden Service', [ - { label: 'Validating configuration', detail: 'Checking service name and ports' }, - { label: 'Creating hidden service directory', detail: '/var/lib/tor/hidden_services/' + name }, - { label: 'Updating Tor configuration', detail: 'Adding HiddenServiceDir and HiddenServicePort' }, - { label: 'Restarting Tor daemon', detail: 'Applying configuration changes' }, - { label: 'Generating .onion address', detail: 'This may take up to 30 seconds' }, - { label: 'Finalizing', detail: 'Saving to UCI and syncing with Tor Shield' } - ]); - - ui.showModal('Creating Tor Hidden Service', [ - progressContent, - E('div', { 'id': 'progress-actions', 'style': 'display: none; margin-top: 1rem; text-align: right;' }, [ - E('button', { - 'class': 'btn cbi-button-action', - 'click': function() { - ui.hideModal(); - self.refreshDashboard(); - } - }, 'Close') - ]) - ]); - - // Simulate step progression (actual API call happens in background) - self.updateStep(0, 'active', 'Validating ' + name + ':' + localPort + ' -> :' + onionPort); - - setTimeout(function() { - self.updateStep(0, 'complete', 'Configuration valid'); - self.updateStep(1, 'active'); - }, 500); - - setTimeout(function() { - self.updateStep(1, 'complete', 'Directory created'); - self.updateStep(2, 'active'); - }, 1000); - - setTimeout(function() { - self.updateStep(2, 'complete', 'torrc updated'); - self.updateStep(3, 'active'); - }, 1500); - - // Make actual API call - api.torAdd(name, localPort, onionPort).then(function(res) { - if (res.success) { - self.updateStep(3, 'complete', 'Tor restarted'); - self.updateStep(4, 'complete', 'Address generated'); - self.updateStep(5, 'complete', 'UCI and Tor Shield synced'); - - self.showProgressResult(true, - 'Hidden service created successfully!', - res.onion ? E('code', { 'style': 'color: #9b59b6; font-size: 12px; word-break: break-all;' }, res.onion) : null - ); - } else { - // Mark current step as error - for (var i = 3; i <= 5; i++) { - var stepEl = document.getElementById('step-' + i); - if (stepEl && stepEl.getAttribute('data-status') === 'active') { - self.updateStep(i, 'error', res.error || 'Failed'); - break; - } - } - self.showProgressResult(false, 'Failed to create hidden service', res.error || 'Unknown error'); - } - - document.getElementById('progress-actions').style.display = 'block'; - }).catch(function(err) { - self.updateStep(3, 'error', 'Connection failed'); - self.showProgressResult(false, 'API Error', err.message || 'Unknown error'); - document.getElementById('progress-actions').style.display = 'block'; - }); - } - }, 'Create Hidden Service') - ]) - ]); - }, - - handleAddSsl: function(svc, serviceName) { - var self = this; - - ui.showModal('Add SSL Backend', [ - E('p', {}, 'Configure HTTPS reverse proxy for ' + (svc.name || svc.process)), - E('div', { 'style': 'margin: 1rem 0;' }, [ - E('div', { 'style': 'margin-bottom: 0.75rem;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; color: #ccc; font-size: 0.9rem;' }, 'Service Name'), - E('input', { - 'type': 'text', - 'id': 'ssl-svc-name', - 'value': serviceName, - 'style': 'width: 100%; padding: 10px; background: #1a1a2e; border: 1px solid #333; color: #fff; border-radius: 6px;' - }) - ]), - E('div', { 'style': 'margin-bottom: 0.75rem;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; color: #ccc; font-size: 0.9rem;' }, 'Domain (FQDN)'), - E('input', { - 'type': 'text', - 'id': 'ssl-domain', - 'placeholder': serviceName + '.example.com', - 'style': 'width: 100%; padding: 10px; background: #1a1a2e; border: 1px solid #333; color: #fff; border-radius: 6px;' - }) - ]), - E('div', {}, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; color: #ccc; font-size: 0.9rem;' }, 'Backend Port'), - E('input', { - 'type': 'number', - 'id': 'ssl-port', - 'value': svc.port, - 'style': 'width: 100%; padding: 10px; background: #1a1a2e; border: 1px solid #333; color: #fff; border-radius: 6px;' - }) - ]) - ]), - E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 8px; margin-top: 1rem;' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, 'Cancel'), - E('button', { - 'class': 'btn cbi-button-action', - 'click': function() { - var name = document.getElementById('ssl-svc-name').value; - var domain = document.getElementById('ssl-domain').value; - var port = parseInt(document.getElementById('ssl-port').value); - - if (!domain) { - ui.addNotification(null, E('p', {}, 'Domain is required'), 'warning'); - return; - } - - ui.hideModal(); - - // Show progress modal - var progressContent = self.createProgressModal('Configuring SSL Backend', [ - { label: 'Validating configuration', detail: 'Checking domain and backend port' }, - { label: 'Creating HAProxy backend', detail: 'Adding server 127.0.0.1:' + port }, - { label: 'Creating virtual host', detail: 'Domain: ' + domain }, - { label: 'Committing UCI changes', detail: 'Saving to /etc/config/haproxy' }, - { label: 'Regenerating HAProxy config', detail: 'Running haproxyctl generate' }, - { label: 'Reloading HAProxy', detail: 'Applying changes without downtime' } - ]); - - ui.showModal('Configuring SSL Backend', [ - progressContent, - E('div', { 'id': 'progress-actions', 'style': 'display: none; margin-top: 1rem; text-align: right;' }, [ - E('button', { - 'class': 'btn cbi-button-action', - 'click': function() { - ui.hideModal(); - self.refreshDashboard(); - } - }, 'Close') - ]) - ]); - - // Simulate step progression - self.updateStep(0, 'active', 'Validating ' + name + ' -> ' + domain); - - setTimeout(function() { - self.updateStep(0, 'complete', 'Configuration valid'); - self.updateStep(1, 'active'); - }, 400); - - setTimeout(function() { - self.updateStep(1, 'complete', 'Backend ' + name + ' created'); - self.updateStep(2, 'active'); - }, 800); - - setTimeout(function() { - self.updateStep(2, 'complete', 'VHost for ' + domain + ' created'); - self.updateStep(3, 'active'); - }, 1200); - - // Make actual API call - api.sslAdd(name, domain, port).then(function(res) { - if (res.success) { - self.updateStep(3, 'complete', 'UCI committed'); - self.updateStep(4, 'complete', 'Config regenerated'); - self.updateStep(5, 'complete', 'HAProxy reloaded'); - - self.showProgressResult(true, - 'SSL backend configured successfully!', - E('div', {}, [ - E('div', { 'style': 'margin-bottom: 8px;' }, [ - 'Access via: ', - E('a', { - 'href': 'https://' + domain, - 'target': '_blank', - 'style': 'color: #27ae60;' - }, 'https://' + domain) - ]), - E('div', { 'style': 'font-size: 12px; color: #888;' }, - 'Note: Ensure SSL certificate is configured for ' + domain) - ]) - ); - } else { - for (var i = 3; i <= 5; i++) { - var stepEl = document.getElementById('step-' + i); - if (stepEl && stepEl.getAttribute('data-status') !== 'complete') { - self.updateStep(i, 'error', res.error || 'Failed'); - break; - } - } - self.showProgressResult(false, 'Failed to configure SSL backend', res.error || 'Unknown error'); - } - - document.getElementById('progress-actions').style.display = 'block'; - }).catch(function(err) { - self.updateStep(3, 'error', 'Connection failed'); - self.showProgressResult(false, 'API Error', err.message || 'Unknown error'); - document.getElementById('progress-actions').style.display = 'block'; - }); - } - }, 'Create SSL Backend') - ]) - ]); - }, - - handleRemoveTor: function(serviceName) { - var self = this; - - ui.showModal('Remove Tor Hidden Service', [ - E('p', {}, 'Remove the .onion address for ' + serviceName + '?'), - E('p', { 'style': 'color: #e74c3c;' }, 'Warning: The onion address will be permanently deleted.'), - E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 8px; margin-top: 1rem;' }, [ - E('button', { 'class': 'btn', 'click': ui.hideModal }, 'Cancel'), - E('button', { - 'class': 'btn cbi-button-negative', - 'click': function() { - ui.hideModal(); - - // Show progress modal - var progressContent = self.createProgressModal('Removing Tor Hidden Service', [ - { label: 'Removing from torrc', detail: 'Deleting HiddenServiceDir entry' }, - { label: 'Deleting hidden service directory', detail: '/var/lib/tor/hidden_services/' + serviceName }, - { label: 'Restarting Tor daemon', detail: 'Applying configuration changes' }, - { label: 'Updating UCI', detail: 'Removing from secubox-exposure and tor-shield' } - ]); - - ui.showModal('Removing Tor Hidden Service', [ - progressContent, - E('div', { 'id': 'progress-actions', 'style': 'display: none; margin-top: 1rem; text-align: right;' }, [ - E('button', { - 'class': 'btn cbi-button-action', - 'click': function() { - ui.hideModal(); - self.refreshDashboard(); - } - }, 'Close') - ]) - ]); - - self.updateStep(0, 'active'); - - api.torRemove(serviceName).then(function(res) { - if (res.success) { - self.updateStep(0, 'complete'); - self.updateStep(1, 'complete'); - self.updateStep(2, 'complete'); - self.updateStep(3, 'complete'); - self.showProgressResult(true, 'Hidden service removed successfully'); - } else { - self.updateStep(0, 'error', res.error); - self.showProgressResult(false, 'Failed to remove hidden service', res.error || 'Unknown error'); - } - document.getElementById('progress-actions').style.display = 'block'; - }).catch(function(err) { - self.updateStep(0, 'error', 'Connection failed'); - self.showProgressResult(false, 'API Error', err.message); - document.getElementById('progress-actions').style.display = 'block'; - }); - } - }, 'Remove') - ]) - ]); - }, - - handleRemoveSsl: function(serviceName) { - var self = this; - - ui.showModal('Remove SSL Backend', [ - E('p', {}, 'Remove HAProxy backend for ' + serviceName + '?'), - E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 8px; margin-top: 1rem;' }, [ - E('button', { 'class': 'btn', 'click': ui.hideModal }, 'Cancel'), - E('button', { - 'class': 'btn cbi-button-negative', - 'click': function() { - ui.hideModal(); - - // Show progress modal - var progressContent = self.createProgressModal('Removing SSL Backend', [ - { label: 'Removing virtual host', detail: 'Deleting vhost configuration' }, - { label: 'Removing backend', detail: 'Deleting backend ' + serviceName }, - { label: 'Committing UCI changes', detail: 'Saving to /etc/config/haproxy' }, - { label: 'Regenerating HAProxy config', detail: 'Running haproxyctl generate' }, - { label: 'Reloading HAProxy', detail: 'Applying changes' } - ]); - - ui.showModal('Removing SSL Backend', [ - progressContent, - E('div', { 'id': 'progress-actions', 'style': 'display: none; margin-top: 1rem; text-align: right;' }, [ - E('button', { - 'class': 'btn cbi-button-action', - 'click': function() { - ui.hideModal(); - self.refreshDashboard(); - } - }, 'Close') - ]) - ]); - - self.updateStep(0, 'active'); - - api.sslRemove(serviceName).then(function(res) { - if (res.success) { - self.updateStep(0, 'complete'); - self.updateStep(1, 'complete'); - self.updateStep(2, 'complete'); - self.updateStep(3, 'complete'); - self.updateStep(4, 'complete'); - self.showProgressResult(true, 'SSL backend removed successfully'); - } else { - self.updateStep(0, 'error', res.error); - self.showProgressResult(false, 'Failed to remove SSL backend', res.error || 'Unknown error'); - } - document.getElementById('progress-actions').style.display = 'block'; - }).catch(function(err) { - self.updateStep(0, 'error', 'Connection failed'); - self.showProgressResult(false, 'API Error', err.message); - document.getElementById('progress-actions').style.display = 'block'; - }); - } - }, 'Remove') - ]) - ]); - }, - - refreshDashboard: function() { - var self = this; - return Promise.all([ - api.status(), - api.scan(), - api.conflicts(), - api.torList(), - api.sslList() - ]).then(function(data) { - var container = document.querySelector('.exposure-dashboard'); - if (container) { - var newView = self.render(data); - container.parentNode.replaceChild(newView, container); - } - }); - }, - - showToast: function(message, type) { - var existing = document.querySelector('.exp-toast'); - if (existing) existing.remove(); - - var iconMap = { - 'success': '\u2705', - 'error': '\u274C', - 'warning': '\u26A0\uFE0F' - }; - - var colorMap = { - 'success': '#22c55e', - 'error': '#ef4444', - 'warning': '#f97316' - }; - - var toast = E('div', { - 'class': 'exp-toast', - 'style': 'position: fixed; bottom: 24px; right: 24px; background: #1a1a2e; border: 1px solid ' + (colorMap[type] || '#333') + '; padding: 12px 20px; border-radius: 8px; color: #fff; z-index: 10000; display: flex; align-items: center; gap: 8px;' - }, [ - E('span', {}, iconMap[type] || '\u2139\uFE0F'), - message - ]); - document.body.appendChild(toast); - - setTimeout(function() { - toast.remove(); - }, 4000); - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}); diff --git a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/ssl.js b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/ssl.js deleted file mode 100644 index 6c752d54..00000000 --- a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/ssl.js +++ /dev/null @@ -1,222 +0,0 @@ -'use strict'; -'require view'; -'require dom'; -'require ui'; -'require exposure/api as api'; - -return view.extend({ - load: function() { - return Promise.all([ - api.sslList(), - api.scan() - ]); - }, - - render: function(data) { - var sslResult = data[0] || {}; - var scanResult = data[1] || {}; - var sslBackends = Array.isArray(sslResult) ? sslResult : (sslResult.backends || []); - var allServices = Array.isArray(scanResult) ? scanResult : (scanResult.services || []); - var self = this; - - // Inject CSS - var cssLink = document.querySelector('link[href*="exposure/dashboard.css"]'); - if (!cssLink) { - var link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = L.resource('exposure/dashboard.css'); - document.head.appendChild(link); - } - - var view = E('div', { 'class': 'exposure-dashboard' }, [ - E('h2', {}, '\ud83d\udd12 HAProxy SSL Backends'), - E('p', { 'style': 'color: #8892b0; margin-bottom: 1.5rem;' }, - 'Configure HTTPS reverse proxy for your services'), - - // Add new backend form - E('div', { 'class': 'exposure-section' }, [ - E('div', { 'class': 'exposure-section-header' }, [ - E('div', { 'class': 'exposure-section-title' }, [ - E('span', { 'class': 'icon' }, '\u2795'), - 'Add SSL Backend' - ]) - ]), - E('div', { 'class': 'exposure-form' }, [ - E('div', { 'class': 'exposure-form-group' }, [ - E('label', {}, 'Service'), - E('select', { 'id': 'new-ssl-service' }, - [E('option', { 'value': '' }, '-- Select --')].concat( - allServices.filter(function(s) { return s.external; }).map(function(s) { - var name = s.name || s.process; - return E('option', { - 'value': s.process, - 'data-port': s.port, - 'data-name': name.toLowerCase().replace(/\s+/g, '') - }, name + ' (:' + s.port + ')'); - }) - ) - ) - ]), - E('div', { 'class': 'exposure-form-group' }, [ - E('label', {}, 'Domain'), - E('input', { - 'type': 'text', - 'id': 'new-ssl-domain', - 'placeholder': 'service.example.com' - }) - ]), - E('div', { 'class': 'exposure-form-group' }, [ - E('label', {}, 'Backend Port'), - E('input', { 'type': 'number', 'id': 'new-ssl-port', 'placeholder': '3000' }) - ]), - E('button', { - 'class': 'btn-action btn-primary', - 'click': ui.createHandlerFn(self, 'handleAdd') - }, 'Add Backend') - ]) - ]), - - // Info box - E('div', { - 'class': 'exposure-section', - 'style': 'background: rgba(0, 212, 255, 0.1); border-color: #00d4ff;' - }, [ - E('p', { 'style': 'margin: 0; color: #ccd6f6;' }, [ - E('strong', {}, '\u2139\ufe0f SSL Certificate: '), - 'After adding a backend, upload the SSL certificate to ', - E('code', {}, '/srv/lxc/haproxy/rootfs/etc/haproxy/certs/'), - '. The certificate file should be named ', - E('code', {}, 'domain.pem'), - ' and contain both the certificate and private key.' - ]) - ]), - - // Existing backends - E('div', { 'class': 'exposure-section' }, [ - E('div', { 'class': 'exposure-section-header' }, [ - E('div', { 'class': 'exposure-section-title' }, [ - E('span', { 'class': 'icon' }, '\ud83d\udd12'), - 'Active SSL Backends (' + sslBackends.length + ')' - ]), - E('button', { - 'class': 'btn-action btn-primary', - 'click': function() { location.reload(); } - }, 'Refresh') - ]), - - sslBackends.length > 0 ? - E('table', { 'class': 'exposure-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, 'Service'), - E('th', {}, 'Domain'), - E('th', {}, 'Backend'), - E('th', {}, 'Actions') - ]) - ]), - E('tbody', {}, - sslBackends.map(function(b) { - return E('tr', {}, [ - E('td', { 'style': 'font-weight: 600;' }, b.service), - E('td', {}, [ - E('a', { - 'href': 'https://' + b.domain, - 'target': '_blank', - 'style': 'color: #00d4ff;' - }, b.domain), - E('span', { 'style': 'margin-left: 0.5rem;' }, '\ud83d\udd17') - ]), - E('td', { 'style': 'font-family: monospace;' }, b.backend || 'N/A'), - E('td', {}, [ - E('button', { - 'class': 'btn-action btn-danger', - 'click': ui.createHandlerFn(self, 'handleRemove', b.service) - }, 'Remove') - ]) - ]); - }) - ) - ]) : - E('div', { 'class': 'exposure-empty' }, [ - E('div', { 'class': 'icon' }, '\ud83d\udd12'), - E('p', {}, 'No SSL backends configured'), - E('p', { 'style': 'font-size: 0.85rem;' }, 'Select a service above to add HTTPS access') - ]) - ]) - ]); - - // Wire up service selector - setTimeout(function() { - var sel = document.getElementById('new-ssl-service'); - var portInput = document.getElementById('new-ssl-port'); - var domainInput = document.getElementById('new-ssl-domain'); - if (sel && portInput) { - sel.addEventListener('change', function() { - var opt = sel.options[sel.selectedIndex]; - portInput.value = opt.dataset.port || ''; - if (opt.dataset.name) { - domainInput.placeholder = opt.dataset.name + '.example.com'; - } - }); - } - }, 100); - - return view; - }, - - handleAdd: function(ev) { - var service = document.getElementById('new-ssl-service').value; - var domain = document.getElementById('new-ssl-domain').value; - var port = parseInt(document.getElementById('new-ssl-port').value); - - if (!service) { - ui.addNotification(null, E('p', {}, 'Please select a service'), 'warning'); - return; - } - - if (!domain) { - ui.addNotification(null, E('p', {}, 'Please enter a domain'), 'warning'); - return; - } - - if (!port) { - ui.addNotification(null, E('p', {}, 'Please specify the backend port'), 'warning'); - return; - } - - api.sslAdd(service, domain, port).then(function(res) { - if (res.success) { - ui.addNotification(null, E('p', {}, [ - 'SSL backend configured for ', - E('strong', {}, domain), - E('br'), - 'Remember to upload the SSL certificate!' - ]), 'success'); - location.reload(); - } else { - ui.addNotification(null, E('p', {}, 'Error: ' + (res.error || 'Unknown error')), 'danger'); - } - }).catch(function(err) { - ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'danger'); - }); - }, - - handleRemove: function(service, ev) { - if (!confirm('Remove SSL backend for ' + service + '?')) { - return; - } - - api.sslRemove(service).then(function(res) { - if (res.success) { - ui.addNotification(null, E('p', {}, 'SSL backend removed'), 'success'); - location.reload(); - } else { - ui.addNotification(null, E('p', {}, 'Error: ' + (res.error || 'Unknown error')), 'danger'); - } - }); - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}); diff --git a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/tor.js b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/tor.js deleted file mode 100644 index 686d70b2..00000000 --- a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/tor.js +++ /dev/null @@ -1,201 +0,0 @@ -'use strict'; -'require view'; -'require dom'; -'require ui'; -'require exposure/api as api'; - -return view.extend({ - load: function() { - return Promise.all([ - api.torList(), - api.scan() - ]); - }, - - render: function(data) { - var torResult = data[0] || {}; - var scanResult = data[1] || {}; - var torServices = Array.isArray(torResult) ? torResult : (torResult.services || []); - var allServices = Array.isArray(scanResult) ? scanResult : (scanResult.services || []); - var self = this; - - // Inject CSS - var cssLink = document.querySelector('link[href*="exposure/dashboard.css"]'); - if (!cssLink) { - var link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = L.resource('exposure/dashboard.css'); - document.head.appendChild(link); - } - - var view = E('div', { 'class': 'exposure-dashboard' }, [ - E('h2', {}, '\ud83e\uddc5 Tor Hidden Services'), - E('p', { 'style': 'color: #8892b0; margin-bottom: 1.5rem;' }, - 'Expose services on the Tor network with .onion addresses'), - - // Add new service form - E('div', { 'class': 'exposure-section' }, [ - E('div', { 'class': 'exposure-section-header' }, [ - E('div', { 'class': 'exposure-section-title' }, [ - E('span', { 'class': 'icon' }, '\u2795'), - 'Add Hidden Service' - ]) - ]), - E('div', { 'class': 'exposure-form' }, [ - E('div', { 'class': 'exposure-form-group' }, [ - E('label', {}, 'Service'), - E('select', { 'id': 'new-tor-service' }, - [E('option', { 'value': '' }, '-- Select --')].concat( - allServices.filter(function(s) { return s.external; }).map(function(s) { - var name = s.name || s.process; - return E('option', { 'value': s.process, 'data-port': s.port }, - name + ' (:' + s.port + ')'); - }) - ) - ) - ]), - E('div', { 'class': 'exposure-form-group' }, [ - E('label', {}, 'Local Port'), - E('input', { 'type': 'number', 'id': 'new-tor-port', 'placeholder': '3000' }) - ]), - E('div', { 'class': 'exposure-form-group' }, [ - E('label', {}, 'Onion Port'), - E('input', { 'type': 'number', 'id': 'new-tor-onion-port', 'value': '80' }) - ]), - E('button', { - 'class': 'btn-action btn-primary', - 'click': ui.createHandlerFn(self, 'handleAdd') - }, 'Create .onion') - ]) - ]), - - // Existing services - E('div', { 'class': 'exposure-section' }, [ - E('div', { 'class': 'exposure-section-header' }, [ - E('div', { 'class': 'exposure-section-title' }, [ - E('span', { 'class': 'icon' }, '\ud83e\uddc5'), - 'Active Hidden Services (' + torServices.length + ')' - ]), - E('button', { - 'class': 'btn-action btn-primary', - 'click': function() { location.reload(); } - }, 'Refresh') - ]), - - torServices.length > 0 ? - E('table', { 'class': 'exposure-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, 'Service'), - E('th', {}, 'Onion Address'), - E('th', {}, 'Port'), - E('th', {}, 'Backend'), - E('th', {}, 'Actions') - ]) - ]), - E('tbody', {}, - torServices.map(function(svc) { - return E('tr', {}, [ - E('td', { 'style': 'font-weight: 600;' }, svc.service), - E('td', {}, [ - E('code', { 'class': 'onion-address' }, svc.onion), - E('button', { - 'class': 'btn-action', - 'style': 'margin-left: 0.5rem; padding: 0.25rem 0.5rem;', - 'click': function() { - navigator.clipboard.writeText(svc.onion); - ui.addNotification(null, E('p', {}, 'Copied to clipboard'), 'info'); - } - }, '\ud83d\udccb') - ]), - E('td', {}, svc.port || '80'), - E('td', { 'style': 'font-family: monospace;' }, svc.backend || 'N/A'), - E('td', {}, [ - E('button', { - 'class': 'btn-action btn-danger', - 'click': ui.createHandlerFn(self, 'handleRemove', svc.service) - }, 'Remove') - ]) - ]); - }) - ) - ]) : - E('div', { 'class': 'exposure-empty' }, [ - E('div', { 'class': 'icon' }, '\ud83e\uddc5'), - E('p', {}, 'No Tor hidden services configured'), - E('p', { 'style': 'font-size: 0.85rem;' }, 'Select a service above to create a .onion address') - ]) - ]) - ]); - - // Wire up service selector - setTimeout(function() { - var sel = document.getElementById('new-tor-service'); - var portInput = document.getElementById('new-tor-port'); - if (sel && portInput) { - sel.addEventListener('change', function() { - var opt = sel.options[sel.selectedIndex]; - portInput.value = opt.dataset.port || ''; - }); - } - }, 100); - - return view; - }, - - handleAdd: function(ev) { - var service = document.getElementById('new-tor-service').value; - var port = parseInt(document.getElementById('new-tor-port').value); - var onionPort = parseInt(document.getElementById('new-tor-onion-port').value) || 80; - - if (!service) { - ui.addNotification(null, E('p', {}, 'Please select a service'), 'warning'); - return; - } - - if (!port) { - ui.addNotification(null, E('p', {}, 'Please specify the local port'), 'warning'); - return; - } - - ui.showModal('Creating Hidden Service...', [ - E('p', { 'class': 'spinning' }, 'Please wait, generating .onion address (this may take a moment)...') - ]); - - api.torAdd(service, port, onionPort).then(function(res) { - ui.hideModal(); - if (res.success) { - ui.addNotification(null, E('p', {}, [ - 'Hidden service created! ', - E('br'), - E('code', { 'style': 'word-break: break-all;' }, res.onion || 'Refresh to see address') - ]), 'success'); - location.reload(); - } else { - ui.addNotification(null, E('p', {}, 'Error: ' + (res.error || 'Unknown error')), 'danger'); - } - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'danger'); - }); - }, - - handleRemove: function(service, ev) { - if (!confirm('Remove hidden service for ' + service + '?\n\nThe .onion address will be permanently lost.')) { - return; - } - - api.torRemove(service).then(function(res) { - if (res.success) { - ui.addNotification(null, E('p', {}, 'Hidden service removed'), 'success'); - location.reload(); - } else { - ui.addNotification(null, E('p', {}, 'Error: ' + (res.error || 'Unknown error')), 'danger'); - } - }); - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -});