diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0958327c..ff7347c6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -279,7 +279,12 @@ "WebFetch(domain:zigbeefordomoticz.github.io)", "WebFetch(domain:rustdesk.com)", "WebFetch(domain:deepwiki.com)", - "Bash(traceroute:*)" + "Bash(traceroute:*)", + "Bash(git -C /home/reepost/CyberMindStudio/secubox-openwrt add package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/container.sh .claude/WIP.md)", + "Bash(git -C /home/reepost/CyberMindStudio/secubox-openwrt commit -m \"$\\(cat <<''EOF''\nfix\\(mailserver\\): Use LMDB maps instead of hash for Alpine Postfix\n\nAlpine Linux''s Postfix is compiled with LMDB support, not BerkeleyDB\nhash support. This caused \"Temporary lookup failure\" errors on send.\n\nChanges:\n- Changed virtual_alias_maps and virtual_mailbox_maps to lmdb: prefix\n- Copy resolv.conf to Postfix chroot for DNS resolution\n- Added `mailctl fix-postfix` command to repair existing installations\n\nRoot cause: virtual_alias_maps was configured as hash:/etc/postfix/virtual\nbut the hash map type is not supported on Alpine, only lmdb.\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git -C /home/reepost/CyberMindStudio/secubox-openwrt add:*)", + "Bash(recipient table\" errors because Postfix treated the domain as local\ninstead of virtual.\n\nChanges:\n- Remove $mydomain from mydestination in setup.sh\n- Update fix-postfix command to also fix this issue\n- Ensure vdomains file is properly created\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git -C /home/reepost/CyberMindStudio/secubox-openwrt commit -m \"$\\(cat <<''EOF''\ndocs: Document mail port hijacking fix\n\nFirewall DNAT rules were redirecting ALL port 993/587/465 traffic\nto local mailserver, blocking external mail server connections.\n\nFix: Add -i $WAN_IF to only redirect inbound WAN traffic.\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" ] } } diff --git a/package/secubox/luci-app-vortex-dns/Makefile b/package/secubox/luci-app-vortex-dns/Makefile new file mode 100644 index 00000000..14009f4e --- /dev/null +++ b/package/secubox/luci-app-vortex-dns/Makefile @@ -0,0 +1,30 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-vortex-dns +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=CyberMind +PKG_LICENSE:=GPL-3.0 + +LUCI_TITLE:=LuCI Vortex DNS Dashboard +LUCI_DEPENDS:=+secubox-vortex-dns +luci-base +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-vortex-dns/install + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-vortex-dns.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-vortex-dns.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.vortex-dns $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/vortex-dns + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/vortex-dns/*.js $(1)/www/luci-static/resources/view/vortex-dns/ +endef + +$(eval $(call BuildPackage,luci-app-vortex-dns)) diff --git a/package/secubox/luci-app-vortex-dns/htdocs/luci-static/resources/view/vortex-dns/dashboard.js b/package/secubox/luci-app-vortex-dns/htdocs/luci-static/resources/view/vortex-dns/dashboard.js new file mode 100644 index 00000000..5f78d7ea --- /dev/null +++ b/package/secubox/luci-app-vortex-dns/htdocs/luci-static/resources/view/vortex-dns/dashboard.js @@ -0,0 +1,433 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require form'; +'require uci'; + +var callStatus = rpc.declare({ + object: 'luci.vortex-dns', + method: 'status', + expect: {} +}); + +var callGetSlaves = rpc.declare({ + object: 'luci.vortex-dns', + method: 'get_slaves', + expect: { slaves: [] } +}); + +var callGetPeers = rpc.declare({ + object: 'luci.vortex-dns', + method: 'get_peers', + expect: { peers: [] } +}); + +var callMasterInit = rpc.declare({ + object: 'luci.vortex-dns', + method: 'master_init', + params: ['domain'], + expect: {} +}); + +var callDelegate = rpc.declare({ + object: 'luci.vortex-dns', + method: 'delegate', + params: ['node', 'zone'], + expect: {} +}); + +var callSlaveJoin = rpc.declare({ + object: 'luci.vortex-dns', + method: 'slave_join', + params: ['master', 'token'], + expect: {} +}); + +var callMeshSync = rpc.declare({ + object: 'luci.vortex-dns', + method: 'mesh_sync', + expect: {} +}); + +return view.extend({ + load: function() { + return Promise.all([ + callStatus(), + callGetSlaves(), + callGetPeers() + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var slaves = data[1] || []; + var peers = data[2] || []; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, 'Vortex DNS'), + E('div', { 'class': 'cbi-map-descr' }, + 'Meshed multi-dynamic subdomain delegation system'), + + // Status Card + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Status'), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Mode'), + E('div', { 'class': 'td' }, this.renderModeBadge(status.mode)) + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Enabled'), + E('div', { 'class': 'td' }, status.enabled ? + E('span', { 'class': 'badge success' }, 'Yes') : + E('span', { 'class': 'badge warning' }, 'No')) + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Sync Interval'), + E('div', { 'class': 'td' }, (status.sync_interval || 300) + 's') + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Last Sync'), + E('div', { 'class': 'td' }, status.last_sync || 'Never') + ]) + ]) + ]), + + // Master Section (if master mode) + status.master ? this.renderMasterSection(status.master, slaves) : null, + + // Slave Section (if slave mode) + status.slave ? this.renderSlaveSection(status.slave) : null, + + // Mesh Peers Section + this.renderPeersSection(status.mesh, peers), + + // Actions Section + this.renderActionsSection(status) + ]); + + return view; + }, + + renderModeBadge: function(mode) { + var colors = { + 'master': 'primary', + 'slave': 'info', + 'submaster': 'warning', + 'standalone': 'secondary' + }; + return E('span', { + 'class': 'badge ' + (colors[mode] || 'secondary'), + 'style': 'padding: 4px 8px; border-radius: 4px; font-weight: bold;' + }, (mode || 'standalone').toUpperCase()); + }, + + renderMasterSection: function(master, slaves) { + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Master Node'), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Wildcard Domain'), + E('div', { 'class': 'td' }, E('strong', {}, '*.' + (master.wildcard_domain || 'not set'))) + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'DNS Provider'), + E('div', { 'class': 'td' }, master.dns_provider || 'not set') + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Delegated Slaves'), + E('div', { 'class': 'td' }, master.slave_count || 0) + ]) + ]), + + // Slaves Table + slaves.length > 0 ? E('div', { 'style': 'margin-top: 16px;' }, [ + E('h4', {}, 'Delegated Zones'), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Zone'), + E('th', { 'class': 'th' }, 'FQDN'), + E('th', { 'class': 'th' }, 'Node IP'), + E('th', { 'class': 'th' }, 'Created') + ]) + ].concat(slaves.map(function(s) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, s.zone), + E('td', { 'class': 'td' }, s.fqdn), + E('td', { 'class': 'td' }, s.node), + E('td', { 'class': 'td' }, s.created) + ]); + }))) + ]) : null + ]); + }, + + renderSlaveSection: function(slave) { + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Slave Node'), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Parent Master'), + E('div', { 'class': 'td' }, slave.parent_master || 'not set') + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Delegated Zone'), + E('div', { 'class': 'td' }, E('strong', {}, slave.delegated_zone || 'pending')) + ]) + ]) + ]); + }, + + renderPeersSection: function(mesh, peers) { + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Mesh Network'), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Gossip'), + E('div', { 'class': 'td' }, mesh && mesh.gossip_enabled ? + E('span', { 'class': 'badge success' }, 'Enabled') : + E('span', { 'class': 'badge warning' }, 'Disabled')) + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'First Peek'), + E('div', { 'class': 'td' }, mesh && mesh.first_peek ? 'Enabled' : 'Disabled') + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Connected Peers'), + E('div', { 'class': 'td' }, (mesh && mesh.peer_count) || 0) + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, 'Published Services'), + E('div', { 'class': 'td' }, (mesh && mesh.published_count) || 0) + ]) + ]), + + // Peers Table + peers.length > 0 ? E('div', { 'style': 'margin-top: 16px;' }, [ + E('h4', {}, 'Connected Peers'), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Name'), + E('th', { 'class': 'th' }, 'IP'), + E('th', { 'class': 'th' }, 'Status') + ]) + ].concat(peers.map(function(p) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, p.name || p.id), + E('td', { 'class': 'td' }, p.ip), + E('td', { 'class': 'td' }, p.online ? + E('span', { 'class': 'badge success' }, 'Online') : + E('span', { 'class': 'badge danger' }, 'Offline')) + ]); + }))) + ]) : E('p', { 'style': 'color: #888; margin-top: 8px;' }, 'No peers connected') + ]); + }, + + renderActionsSection: function(status) { + var self = this; + + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Actions'), + E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap;' }, [ + E('button', { + 'class': 'btn cbi-button-action', + 'click': function() { self.doMeshSync(); } + }, 'Sync Mesh'), + + status.mode === 'standalone' ? E('button', { + 'class': 'btn cbi-button-positive', + 'click': function() { self.showMasterInitDialog(); } + }, 'Initialize as Master') : null, + + status.mode === 'standalone' ? E('button', { + 'class': 'btn cbi-button-neutral', + 'click': function() { self.showSlaveJoinDialog(); } + }, 'Join as Slave') : null, + + status.mode === 'master' ? E('button', { + 'class': 'btn cbi-button-positive', + 'click': function() { self.showDelegateDialog(); } + }, 'Delegate Zone') : null + ]) + ]); + }, + + doMeshSync: function() { + ui.showModal('Syncing...', [ + E('p', { 'class': 'spinning' }, 'Syncing with mesh peers...') + ]); + + callMeshSync().then(function(res) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Mesh sync completed at ' + res.synced_at), 'success'); + }).catch(function(e) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Sync failed: ' + e.message), 'error'); + }); + }, + + showMasterInitDialog: function() { + var self = this; + ui.showModal('Initialize as Master', [ + E('p', {}, 'Enter the wildcard domain to manage:'), + E('input', { + 'type': 'text', + 'id': 'master-domain', + 'placeholder': 'secubox.io', + 'style': 'width: 100%; padding: 8px; margin: 8px 0;' + }), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'btn cbi-button-positive', + 'click': function() { + var domain = document.getElementById('master-domain').value; + if (domain) { + self.doMasterInit(domain); + } + } + }, 'Initialize') + ]) + ]); + }, + + doMasterInit: function(domain) { + ui.showModal('Initializing...', [ + E('p', { 'class': 'spinning' }, 'Initializing master for *.' + domain + '...') + ]); + + callMasterInit(domain).then(function(res) { + ui.hideModal(); + ui.showModal('Master Initialized', [ + E('p', {}, 'Successfully initialized as master for *.' + domain), + E('p', {}, [ + E('strong', {}, 'Enrollment Token: '), + E('code', {}, res.token) + ]), + E('p', {}, 'Slaves can join with: vortexctl slave join ' + res.token), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn cbi-button-positive', + 'click': function() { location.reload(); } + }, 'OK') + ]) + ]); + }).catch(function(e) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Failed: ' + e.message), 'error'); + }); + }, + + showSlaveJoinDialog: function() { + var self = this; + ui.showModal('Join as Slave', [ + E('p', {}, 'Enter master connection details:'), + E('label', {}, 'Master IP:'), + E('input', { + 'type': 'text', + 'id': 'master-ip', + 'placeholder': '192.168.1.1', + 'style': 'width: 100%; padding: 8px; margin: 8px 0;' + }), + E('label', {}, 'Enrollment Token:'), + E('input', { + 'type': 'text', + 'id': 'master-token', + 'placeholder': 'token from master', + 'style': 'width: 100%; padding: 8px; margin: 8px 0;' + }), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'btn cbi-button-positive', + 'click': function() { + var ip = document.getElementById('master-ip').value; + var token = document.getElementById('master-token').value; + if (ip && token) { + self.doSlaveJoin(ip, token); + } + } + }, 'Join') + ]) + ]); + }, + + doSlaveJoin: function(master, token) { + ui.showModal('Joining...', [ + E('p', { 'class': 'spinning' }, 'Joining master at ' + master + '...') + ]); + + callSlaveJoin(master, token).then(function(res) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Successfully joined master'), 'success'); + location.reload(); + }).catch(function(e) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Failed: ' + e.message), 'error'); + }); + }, + + showDelegateDialog: function() { + var self = this; + ui.showModal('Delegate Zone', [ + E('p', {}, 'Delegate a subzone to a slave node:'), + E('label', {}, 'Slave Node IP:'), + E('input', { + 'type': 'text', + 'id': 'delegate-node', + 'placeholder': '192.168.1.100', + 'style': 'width: 100%; padding: 8px; margin: 8px 0;' + }), + E('label', {}, 'Zone Name:'), + E('input', { + 'type': 'text', + 'id': 'delegate-zone', + 'placeholder': 'node1', + 'style': 'width: 100%; padding: 8px; margin: 8px 0;' + }), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'btn cbi-button-positive', + 'click': function() { + var node = document.getElementById('delegate-node').value; + var zone = document.getElementById('delegate-zone').value; + if (node && zone) { + self.doDelegate(node, zone); + } + } + }, 'Delegate') + ]) + ]); + }, + + doDelegate: function(node, zone) { + ui.showModal('Delegating...', [ + E('p', { 'class': 'spinning' }, 'Delegating ' + zone + ' to ' + node + '...') + ]); + + callDelegate(node, zone).then(function(res) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Zone ' + zone + ' delegated to ' + node), 'success'); + location.reload(); + }).catch(function(e) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Failed: ' + e.message), 'error'); + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-vortex-dns/root/usr/libexec/rpcd/luci.vortex-dns b/package/secubox/luci-app-vortex-dns/root/usr/libexec/rpcd/luci.vortex-dns new file mode 100644 index 00000000..20470e2e --- /dev/null +++ b/package/secubox/luci-app-vortex-dns/root/usr/libexec/rpcd/luci.vortex-dns @@ -0,0 +1,215 @@ +#!/bin/sh +# RPCD handler for Vortex DNS + +. /usr/share/libubox/jshn.sh + +CONFIG="vortex-dns" +STATE_DIR="/var/lib/vortex-dns" + +uci_get() { uci -q get "${CONFIG}.$1"; } + +case "$1" in + list) + echo '{"status":{},"get_slaves":{},"get_peers":{},"get_published":{},"master_init":{"domain":"str"},"delegate":{"node":"str","zone":"str"},"revoke":{"zone":"str"},"slave_join":{"master":"str","token":"str"},"mesh_sync":{},"mesh_publish":{"service":"str","domain":"str"}}' + ;; + call) + case "$2" in + status) + json_init + + enabled=$(uci_get main.enabled) + mode=$(uci_get main.mode) + sync_interval=$(uci_get main.sync_interval) + + json_add_boolean "enabled" "${enabled:-0}" + json_add_string "mode" "${mode:-standalone}" + json_add_int "sync_interval" "${sync_interval:-300}" + + # Master info + if [ "$(uci_get master.enabled)" = "1" ]; then + json_add_object "master" + json_add_string "wildcard_domain" "$(uci_get master.wildcard_domain)" + json_add_string "dns_provider" "$(uci_get master.dns_provider)" + + # Count slaves + slaves=$(uci show "$CONFIG" 2>/dev/null | grep -c "=delegation") + json_add_int "slave_count" "$slaves" + json_close_object + fi + + # Slave info + if [ "$(uci_get slave.enabled)" = "1" ]; then + json_add_object "slave" + json_add_string "parent_master" "$(uci_get slave.parent_master)" + json_add_string "delegated_zone" "$(uci_get slave.delegated_zone)" + json_close_object + fi + + # Mesh info + json_add_object "mesh" + json_add_boolean "gossip_enabled" "$(uci_get mesh.gossip_enabled)" + json_add_boolean "first_peek" "$(uci_get mesh.first_peek)" + json_add_boolean "auto_register" "$(uci_get mesh.auto_register)" + + # Count peers + if command -v secubox-p2p >/dev/null 2>&1; then + peers=$(secubox-p2p peers 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + json_add_int "peer_count" "$peers" + else + json_add_int "peer_count" 0 + fi + + # Count published + if [ -f "$STATE_DIR/published.json" ]; then + published=$(jsonfilter -i "$STATE_DIR/published.json" -e '@[*]' 2>/dev/null | wc -l) + json_add_int "published_count" "$published" + else + json_add_int "published_count" 0 + fi + json_close_object + + # Last sync + if [ -f "$STATE_DIR/last_sync" ]; then + json_add_string "last_sync" "$(cat "$STATE_DIR/last_sync")" + fi + + json_dump + ;; + + get_slaves) + json_init + json_add_array "slaves" + + uci show "$CONFIG" 2>/dev/null | grep "=delegation" | while read -r line; do + section=$(echo "$line" | cut -d= -f1 | cut -d. -f2) + json_add_object + json_add_string "zone" "$(uci_get "${section}.zone")" + json_add_string "node" "$(uci_get "${section}.node")" + json_add_string "fqdn" "$(uci_get "${section}.fqdn")" + json_add_string "created" "$(uci_get "${section}.created")" + json_close_object + done + + json_close_array + json_dump + ;; + + get_peers) + json_init + json_add_array "peers" + + if command -v secubox-p2p >/dev/null 2>&1; then + secubox-p2p peers 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | while read -r peer; do + json_add_object + json_add_string "id" "$(echo "$peer" | jsonfilter -e '@.id' 2>/dev/null)" + json_add_string "name" "$(echo "$peer" | jsonfilter -e '@.name' 2>/dev/null)" + json_add_string "ip" "$(echo "$peer" | jsonfilter -e '@.ip' 2>/dev/null)" + json_add_boolean "online" "$(echo "$peer" | jsonfilter -e '@.online' 2>/dev/null)" + json_close_object + done + fi + + json_close_array + json_dump + ;; + + get_published) + json_init + json_add_array "services" + + if [ -f "$STATE_DIR/published.json" ]; then + cat "$STATE_DIR/published.json" + else + echo "[]" + fi + ;; + + master_init) + read -r input + domain=$(echo "$input" | jsonfilter -e '@.domain') + + if [ -z "$domain" ]; then + echo '{"error":"Domain required"}' + exit 1 + fi + + output=$(vortexctl master init "$domain" 2>&1) + token=$(echo "$output" | grep "enrollment token:" | awk '{print $NF}') + + json_init + json_add_boolean "success" 1 + json_add_string "domain" "$domain" + json_add_string "token" "$token" + json_dump + ;; + + delegate) + read -r input + node=$(echo "$input" | jsonfilter -e '@.node') + zone=$(echo "$input" | jsonfilter -e '@.zone') + + if [ -z "$node" ] || [ -z "$zone" ]; then + echo '{"error":"Node and zone required"}' + exit 1 + fi + + vortexctl master delegate "$node" "$zone" >/dev/null 2>&1 + + json_init + json_add_boolean "success" 1 + json_add_string "zone" "$zone" + json_add_string "node" "$node" + json_dump + ;; + + slave_join) + read -r input + master=$(echo "$input" | jsonfilter -e '@.master') + token=$(echo "$input" | jsonfilter -e '@.token') + + if [ -z "$master" ] || [ -z "$token" ]; then + echo '{"error":"Master IP and token required"}' + exit 1 + fi + + vortexctl slave join "$master" "$token" >/dev/null 2>&1 + + json_init + json_add_boolean "success" 1 + json_dump + ;; + + mesh_sync) + vortexctl mesh sync >/dev/null 2>&1 + + json_init + json_add_boolean "success" 1 + json_add_string "synced_at" "$(date -Iseconds)" + json_dump + ;; + + mesh_publish) + read -r input + service=$(echo "$input" | jsonfilter -e '@.service') + domain=$(echo "$input" | jsonfilter -e '@.domain') + + if [ -z "$service" ] || [ -z "$domain" ]; then + echo '{"error":"Service and domain required"}' + exit 1 + fi + + vortexctl mesh publish "$service" "$domain" >/dev/null 2>&1 + + json_init + json_add_boolean "success" 1 + json_add_string "service" "$service" + json_add_string "domain" "$domain" + json_dump + ;; + + *) + echo '{"error":"Unknown method"}' + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-vortex-dns/root/usr/share/luci/menu.d/luci-app-vortex-dns.json b/package/secubox/luci-app-vortex-dns/root/usr/share/luci/menu.d/luci-app-vortex-dns.json new file mode 100644 index 00000000..a679a446 --- /dev/null +++ b/package/secubox/luci-app-vortex-dns/root/usr/share/luci/menu.d/luci-app-vortex-dns.json @@ -0,0 +1,14 @@ +{ + "admin/services/vortex-dns": { + "title": "Vortex DNS", + "order": 85, + "action": { + "type": "view", + "path": "vortex-dns/dashboard" + }, + "depends": { + "acl": ["luci-app-vortex-dns"], + "uci": {"vortex-dns": true} + } + } +} diff --git a/package/secubox/luci-app-vortex-dns/root/usr/share/rpcd/acl.d/luci-app-vortex-dns.json b/package/secubox/luci-app-vortex-dns/root/usr/share/rpcd/acl.d/luci-app-vortex-dns.json new file mode 100644 index 00000000..669470d9 --- /dev/null +++ b/package/secubox/luci-app-vortex-dns/root/usr/share/rpcd/acl.d/luci-app-vortex-dns.json @@ -0,0 +1,17 @@ +{ + "luci-app-vortex-dns": { + "description": "Grant access to Vortex DNS", + "read": { + "uci": ["vortex-dns"], + "ubus": { + "luci.vortex-dns": ["status", "get_slaves", "get_peers", "get_published"] + } + }, + "write": { + "uci": ["vortex-dns"], + "ubus": { + "luci.vortex-dns": ["master_init", "delegate", "revoke", "slave_join", "mesh_sync", "mesh_publish"] + } + } + } +}