diff --git a/package/secubox/luci-app-reporter/Makefile b/package/secubox/luci-app-reporter/Makefile new file mode 100644 index 00000000..c04f0180 --- /dev/null +++ b/package/secubox/luci-app-reporter/Makefile @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +# LuCI App Reporter - Web UI for SecuBox Report Generator +# Copyright (C) 2025-2026 CyberMind.fr + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-reporter +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=CyberMind +PKG_LICENSE:=MIT + +LUCI_TITLE:=LuCI Reporter Dashboard +LUCI_DEPENDS:=+secubox-app-reporter +luci-base + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-reporter/install + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/reporter + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/reporter/*.js $(1)/www/luci-static/resources/view/reporter/ + + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.reporter $(1)/usr/libexec/rpcd/luci.reporter +endef + +$(eval $(call BuildPackage,luci-app-reporter)) diff --git a/package/secubox/luci-app-reporter/htdocs/luci-static/resources/view/reporter/overview.js b/package/secubox/luci-app-reporter/htdocs/luci-static/resources/view/reporter/overview.js new file mode 100644 index 00000000..f536a82f --- /dev/null +++ b/package/secubox/luci-app-reporter/htdocs/luci-static/resources/view/reporter/overview.js @@ -0,0 +1,448 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require poll'; +'require dom'; +'require secubox/kiss-theme'; + +var callGetStatus = rpc.declare({ + object: 'luci.reporter', + method: 'status', + expect: {} +}); + +var callListReports = rpc.declare({ + object: 'luci.reporter', + method: 'list_reports', + expect: {} +}); + +var callGenerate = rpc.declare({ + object: 'luci.reporter', + method: 'generate', + params: ['type'], + expect: {} +}); + +var callSend = rpc.declare({ + object: 'luci.reporter', + method: 'send', + params: ['type'], + expect: {} +}); + +var callDeleteReport = rpc.declare({ + object: 'luci.reporter', + method: 'delete_report', + params: ['filename'], + expect: {} +}); + +var callTestEmail = rpc.declare({ + object: 'luci.reporter', + method: 'test_email', + expect: {} +}); + +var callSchedule = rpc.declare({ + object: 'luci.reporter', + method: 'schedule', + params: ['type', 'frequency'], + expect: {} +}); + +return view.extend({ + load: function() { + return Promise.all([ + callGetStatus(), + callListReports() + ]); + }, + + formatTime: function(timestamp) { + if (!timestamp || timestamp === 0) return 'Never'; + var d = new Date(timestamp * 1000); + return d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); + }, + + formatSize: function(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + }, + + renderStats: function(status) { + var c = KissTheme.colors; + var emailConfigured = status.email && status.email.configured; + + return [ + KissTheme.stat(status.report_count || 0, 'Reports', c.purple), + KissTheme.stat( + status.schedules ? (status.schedules.dev !== 'off' ? 'ON' : 'OFF') : 'OFF', + 'Dev Schedule', + status.schedules && status.schedules.dev !== 'off' ? c.green : c.muted + ), + KissTheme.stat( + status.schedules ? (status.schedules.services !== 'off' ? 'ON' : 'OFF') : 'OFF', + 'Services Schedule', + status.schedules && status.schedules.services !== 'off' ? c.green : c.muted + ), + KissTheme.stat( + emailConfigured ? 'OK' : 'N/A', + 'Email', + emailConfigured ? c.green : c.muted + ) + ]; + }, + + renderQuickActions: function() { + var self = this; + + return E('div', { 'class': 'kiss-grid kiss-grid-3', 'style': 'margin-bottom: 24px;' }, [ + // Development Report + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'style': 'padding: 20px; text-align: center;' }, [ + E('div', { 'style': 'font-size: 32px; margin-bottom: 12px;' }, '📊'), + E('h3', { 'style': 'margin: 0 0 8px 0; color: var(--kiss-text);' }, 'Development Status'), + E('p', { 'style': 'color: var(--kiss-muted); font-size: 12px; margin: 0 0 16px 0;' }, + 'Roadmap progress, HISTORY.md, WIP items'), + E('div', { 'style': 'display: flex; gap: 8px; justify-content: center;' }, [ + E('button', { + 'class': 'kiss-btn kiss-btn-purple', + 'click': function() { self.handleGenerate('dev'); } + }, 'Generate'), + E('button', { + 'class': 'kiss-btn', + 'click': function() { self.handleSend('dev'); } + }, 'Send') + ]) + ]) + ]), + + // Services Report + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'style': 'padding: 20px; text-align: center;' }, [ + E('div', { 'style': 'font-size: 32px; margin-bottom: 12px;' }, '🌐'), + E('h3', { 'style': 'margin: 0 0 8px 0; color: var(--kiss-text);' }, 'Services Distribution'), + E('p', { 'style': 'color: var(--kiss-muted); font-size: 12px; margin: 0 0 16px 0;' }, + 'Tor services, DNS/SSL vhosts, mesh services'), + E('div', { 'style': 'display: flex; gap: 8px; justify-content: center;' }, [ + E('button', { + 'class': 'kiss-btn kiss-btn-cyan', + 'click': function() { self.handleGenerate('services'); } + }, 'Generate'), + E('button', { + 'class': 'kiss-btn', + 'click': function() { self.handleSend('services'); } + }, 'Send') + ]) + ]) + ]), + + // All Reports + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'style': 'padding: 20px; text-align: center;' }, [ + E('div', { 'style': 'font-size: 32px; margin-bottom: 12px;' }, '📦'), + E('h3', { 'style': 'margin: 0 0 8px 0; color: var(--kiss-text);' }, 'Full Report'), + E('p', { 'style': 'color: var(--kiss-muted); font-size: 12px; margin: 0 0 16px 0;' }, + 'Generate both reports at once'), + E('div', { 'style': 'display: flex; gap: 8px; justify-content: center;' }, [ + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'click': function() { self.handleGenerate('all'); } + }, 'Generate All'), + E('button', { + 'class': 'kiss-btn', + 'click': function() { self.handleSend('all'); } + }, 'Send All') + ]) + ]) + ]) + ]); + }, + + renderReportsList: function(reports) { + var self = this; + + if (!reports || reports.length === 0) { + return E('div', { 'style': 'text-align: center; padding: 32px; color: var(--kiss-muted);' }, [ + E('div', { 'style': 'font-size: 48px; margin-bottom: 12px;' }, '📄'), + E('p', {}, 'No reports generated yet'), + E('p', { 'style': 'font-size: 12px;' }, 'Use the buttons above to generate your first report') + ]); + } + + // Sort by mtime descending + reports.sort(function(a, b) { return b.mtime - a.mtime; }); + + return E('table', { 'class': 'kiss-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Report'), + E('th', {}, 'Type'), + E('th', {}, 'Size'), + E('th', {}, 'Generated'), + E('th', { 'style': 'width: 150px;' }, 'Actions') + ])), + E('tbody', { 'id': 'reports-body' }, reports.map(function(r) { + var typeColor = r.type === 'dev' ? 'purple' : (r.type === 'services' ? 'cyan' : 'muted'); + return E('tr', {}, [ + E('td', { 'style': 'font-family: monospace;' }, r.filename), + E('td', {}, KissTheme.badge(r.type.toUpperCase(), typeColor)), + E('td', { 'style': 'color: var(--kiss-muted);' }, self.formatSize(r.size)), + E('td', { 'style': 'color: var(--kiss-muted); font-size: 12px;' }, self.formatTime(r.mtime)), + E('td', {}, [ + E('a', { + 'href': r.url, + 'target': '_blank', + 'class': 'kiss-btn', + 'style': 'padding: 4px 12px; font-size: 11px; margin-right: 8px; text-decoration: none;' + }, 'View'), + E('button', { + 'class': 'kiss-btn kiss-btn-red', + 'style': 'padding: 4px 12px; font-size: 11px;', + 'click': function() { self.handleDelete(r.filename); } + }, 'Delete') + ]) + ]); + })) + ]); + }, + + renderEmailStatus: function(status) { + var self = this; + var email = status.email || {}; + + if (!email.configured) { + return E('div', { 'style': 'text-align: center; padding: 24px;' }, [ + E('p', { 'style': 'color: var(--kiss-muted); margin-bottom: 12px;' }, + 'Email not configured. Edit /etc/config/secubox-reporter to add SMTP settings.'), + E('code', { 'style': 'font-size: 11px; color: var(--kiss-cyan);' }, + 'uci set secubox-reporter.email.smtp_server="smtp.example.com"') + ]); + } + + return E('div', {}, [ + E('div', { 'style': 'display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 16px;' }, [ + E('div', {}, [ + E('span', { 'style': 'color: var(--kiss-muted); font-size: 11px;' }, 'SMTP Server'), + E('div', { 'style': 'font-family: monospace;' }, email.smtp_server || '-') + ]), + E('div', {}, [ + E('span', { 'style': 'color: var(--kiss-muted); font-size: 11px;' }, 'Recipient'), + E('div', { 'style': 'font-family: monospace;' }, email.recipient || '-') + ]) + ]), + E('button', { + 'class': 'kiss-btn', + 'click': function() { self.handleTestEmail(); } + }, 'Send Test Email') + ]); + }, + + renderSchedules: function(status) { + var self = this; + var schedules = status.schedules || {}; + + return E('div', { 'style': 'display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;' }, [ + // Dev schedule + E('div', {}, [ + E('div', { 'style': 'margin-bottom: 8px;' }, [ + E('span', { 'style': 'font-weight: 600;' }, 'Development Report'), + E('span', { 'style': 'margin-left: 8px;' }, + KissTheme.badge(schedules.dev === 'off' ? 'OFF' : schedules.dev.toUpperCase(), + schedules.dev === 'off' ? 'muted' : 'green')) + ]), + E('select', { + 'class': 'kiss-select', + 'style': 'width: 100%;', + 'id': 'dev-schedule', + 'change': function(ev) { self.handleScheduleChange('dev', ev.target.value); } + }, [ + E('option', { 'value': 'off', 'selected': schedules.dev === 'off' }, 'Off'), + E('option', { 'value': 'daily', 'selected': schedules.dev === 'daily' }, 'Daily (6 AM)'), + E('option', { 'value': 'weekly', 'selected': schedules.dev === 'weekly' }, 'Weekly (Monday)') + ]) + ]), + + // Services schedule + E('div', {}, [ + E('div', { 'style': 'margin-bottom: 8px;' }, [ + E('span', { 'style': 'font-weight: 600;' }, 'Services Report'), + E('span', { 'style': 'margin-left: 8px;' }, + KissTheme.badge(schedules.services === 'off' ? 'OFF' : schedules.services.toUpperCase(), + schedules.services === 'off' ? 'muted' : 'green')) + ]), + E('select', { + 'class': 'kiss-select', + 'style': 'width: 100%;', + 'id': 'services-schedule', + 'change': function(ev) { self.handleScheduleChange('services', ev.target.value); } + }, [ + E('option', { 'value': 'off', 'selected': schedules.services === 'off' }, 'Off'), + E('option', { 'value': 'daily', 'selected': schedules.services === 'daily' }, 'Daily (6 AM)'), + E('option', { 'value': 'weekly', 'selected': schedules.services === 'weekly' }, 'Weekly (Monday)') + ]) + ]) + ]); + }, + + handleGenerate: function(type) { + var self = this; + var typeLabel = type === 'all' ? 'all reports' : (type + ' report'); + + ui.showModal('Generating Report', [ + E('p', { 'class': 'spinning' }, 'Generating ' + typeLabel + '...') + ]); + + return callGenerate(type).then(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', {}, 'Report generated successfully'), 'success'); + // Refresh reports list + return self.refreshReports(); + } else { + ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'Unknown error')), 'error'); + } + }); + }, + + handleSend: function(type) { + var self = this; + var typeLabel = type === 'all' ? 'all reports' : (type + ' report'); + + ui.showModal('Sending Report', [ + E('p', { 'class': 'spinning' }, 'Generating and sending ' + typeLabel + '...') + ]); + + return callSend(type).then(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', {}, 'Report sent successfully'), 'success'); + return self.refreshReports(); + } else { + ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'Unknown error')), 'error'); + } + }); + }, + + handleDelete: function(filename) { + var self = this; + + if (!confirm('Delete report ' + filename + '?')) { + return; + } + + return callDeleteReport(filename).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, 'Report deleted'), 'success'); + return self.refreshReports(); + } else { + ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'Unknown error')), 'error'); + } + }); + }, + + handleTestEmail: function() { + ui.showModal('Testing Email', [ + E('p', { 'class': 'spinning' }, 'Sending test email...') + ]); + + return callTestEmail().then(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', {}, result.message || 'Test email sent'), 'success'); + } else { + ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'Unknown error')), 'error'); + } + }); + }, + + handleScheduleChange: function(type, frequency) { + var self = this; + + return callSchedule(type, frequency).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, + type.charAt(0).toUpperCase() + type.slice(1) + ' schedule set to ' + frequency), 'success'); + } else { + ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'Unknown error')), 'error'); + } + }); + }, + + refreshReports: function() { + var self = this; + return Promise.all([ + callGetStatus(), + callListReports() + ]).then(function(data) { + self.updateDashboard(data[0], data[1]); + }); + }, + + updateDashboard: function(status, reports) { + // Update stats + var statsEl = document.getElementById('reporter-stats'); + if (statsEl) { + dom.content(statsEl, this.renderStats(status)); + } + + // Update reports list + var reportsEl = document.getElementById('reports-list'); + if (reportsEl) { + dom.content(reportsEl, this.renderReportsList(reports.reports || [])); + } + }, + + render: function(data) { + var self = this; + var status = data[0] || {}; + var reports = data[1] || {}; + + var content = [ + // Header + E('div', { 'style': 'margin-bottom: 24px;' }, [ + E('div', { 'style': 'display: flex; align-items: center; gap: 16px; flex-wrap: wrap;' }, [ + E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Report Generator'), + KissTheme.badge(status.enabled ? 'ENABLED' : 'DISABLED', status.enabled ? 'green' : 'muted') + ]), + E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' }, + 'Generate and distribute SecuBox status reports') + ]), + + // Stats row + E('div', { 'class': 'kiss-grid kiss-grid-4', 'id': 'reporter-stats', 'style': 'margin: 20px 0;' }, + this.renderStats(status)), + + // Quick Actions + this.renderQuickActions(), + + // Two column layout + E('div', { 'class': 'kiss-grid kiss-grid-2' }, [ + // Schedules + KissTheme.card('Scheduled Reports', this.renderSchedules(status)), + // Email Status + KissTheme.card('Email Configuration', this.renderEmailStatus(status)) + ]), + + // Reports List + KissTheme.card('Generated Reports', E('div', { 'id': 'reports-list' }, + this.renderReportsList(reports.reports || []))), + + // Last generated info + E('div', { 'style': 'margin-top: 16px; color: var(--kiss-muted); font-size: 12px;' }, [ + status.last_reports && status.last_reports.dev_time > 0 ? + E('span', {}, 'Last dev report: ' + this.formatTime(status.last_reports.dev_time) + ' | ') : '', + status.last_reports && status.last_reports.services_time > 0 ? + E('span', {}, 'Last services report: ' + this.formatTime(status.last_reports.services_time)) : '' + ]) + ]; + + return KissTheme.wrap(content, 'admin/secubox/system/reporter'); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-reporter/root/usr/libexec/rpcd/luci.reporter b/package/secubox/luci-app-reporter/root/usr/libexec/rpcd/luci.reporter new file mode 100755 index 00000000..dcdd0114 --- /dev/null +++ b/package/secubox/luci-app-reporter/root/usr/libexec/rpcd/luci.reporter @@ -0,0 +1,523 @@ +#!/bin/sh +# RPCD backend for SecuBox Report Generator +# Provides LuCI integration for report generation and management + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +CONFIG_NAME="secubox-reporter" +OUTPUT_DIR="/www/reports" +REPORTCTL="/usr/sbin/secubox-reportctl" + +# Method: list +method_list() { + json_init + json_add_object "status" + json_close_object + json_add_object "list_reports" + json_close_object + json_add_object "get_report" + json_add_string "filename" "string" + json_close_object + json_add_object "preview" + json_add_string "type" "string" + json_close_object + json_add_object "generate" + json_add_string "type" "string" + json_close_object + json_add_object "send" + json_add_string "type" "string" + json_close_object + json_add_object "schedule" + json_add_string "type" "string" + json_add_string "frequency" "string" + json_close_object + json_add_object "delete_report" + json_add_string "filename" "string" + json_close_object + json_add_object "test_email" + json_close_object + json_add_object "get_config" + json_close_object + json_dump +} + +# Method: status - Get generator status +method_status() { + config_load "$CONFIG_NAME" + + local enabled output_dir theme + local recipient smtp_server + local dev_schedule services_schedule + + config_get enabled global enabled '0' + config_get output_dir global output_dir '/www/reports' + config_get theme global theme 'dark' + + config_get recipient email recipient '' + config_get smtp_server email smtp_server '' + + # Check schedules from cron + dev_schedule="off" + services_schedule="off" + if [ -f /etc/cron.d/secubox-reporter ]; then + grep -q "generate dev" /etc/cron.d/secubox-reporter && dev_schedule="daily" + grep -q "generate services" /etc/cron.d/secubox-reporter && services_schedule="weekly" + fi + + # Count reports + local report_count=0 + [ -d "$output_dir" ] && report_count=$(ls -1 "$output_dir"/*.html 2>/dev/null | wc -l) + + # Check for most recent reports + local last_dev="" + local last_services="" + local last_dev_time="" + local last_services_time="" + + if [ -f "$output_dir/dev-status.html" ]; then + last_dev="dev-status.html" + last_dev_time=$(stat -c %Y "$output_dir/dev-status.html" 2>/dev/null || echo "0") + fi + + if [ -f "$output_dir/services-status.html" ]; then + last_services="services-status.html" + last_services_time=$(stat -c %Y "$output_dir/services-status.html" 2>/dev/null || echo "0") + fi + + json_init + json_add_boolean "enabled" "$enabled" + json_add_string "theme" "$theme" + json_add_string "output_dir" "$output_dir" + json_add_int "report_count" "$report_count" + + json_add_object "schedules" + json_add_string "dev" "$dev_schedule" + json_add_string "services" "$services_schedule" + json_close_object + + json_add_object "email" + json_add_string "recipient" "$recipient" + json_add_string "smtp_server" "$smtp_server" + json_add_boolean "configured" "$([ -n "$smtp_server" ] && echo 1 || echo 0)" + json_close_object + + json_add_object "last_reports" + json_add_string "dev" "$last_dev" + json_add_int "dev_time" "${last_dev_time:-0}" + json_add_string "services" "$last_services" + json_add_int "services_time" "${last_services_time:-0}" + json_close_object + + json_dump +} + +# Method: list_reports - List all generated reports +method_list_reports() { + config_load "$CONFIG_NAME" + local output_dir + config_get output_dir global output_dir '/www/reports' + + json_init + json_add_array "reports" + + if [ -d "$output_dir" ]; then + for f in "$output_dir"/*.html; do + [ -f "$f" ] || continue + local filename=$(basename "$f") + local size=$(stat -c %s "$f" 2>/dev/null || echo "0") + local mtime=$(stat -c %Y "$f" 2>/dev/null || echo "0") + local type="unknown" + + case "$filename" in + dev-*) type="dev" ;; + services-*) type="services" ;; + esac + + json_add_object "" + json_add_string "filename" "$filename" + json_add_string "type" "$type" + json_add_int "size" "$size" + json_add_int "mtime" "$mtime" + json_add_string "url" "/reports/$filename" + json_close_object + done + fi + + json_close_array + json_dump +} + +# Method: get_report - Get report content +method_get_report() { + local filename="$1" + + json_init + + if [ -z "$filename" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Filename required" + json_dump + return + fi + + # Sanitize filename (prevent path traversal) + filename=$(basename "$filename") + + config_load "$CONFIG_NAME" + local output_dir + config_get output_dir global output_dir '/www/reports' + + local filepath="$output_dir/$filename" + + if [ ! -f "$filepath" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Report not found" + json_dump + return + fi + + # Get file info + local size=$(stat -c %s "$filepath" 2>/dev/null || echo "0") + local mtime=$(stat -c %Y "$filepath" 2>/dev/null || echo "0") + + json_add_boolean "success" 1 + json_add_string "filename" "$filename" + json_add_int "size" "$size" + json_add_int "mtime" "$mtime" + json_add_string "url" "/reports/$filename" + + json_dump +} + +# Method: preview - Preview report (generate to stdout) +method_preview() { + local type="$1" + + json_init + + if [ -z "$type" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Report type required (dev|services)" + json_dump + return + fi + + case "$type" in + dev|services|all) + local preview=$("$REPORTCTL" preview "$type" 2>&1) + local result=$? + + if [ $result -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "type" "$type" + # Store preview in temp file and return path (content too large for jshn) + local tmpfile="/tmp/reporter-preview-$$.html" + echo "$preview" > "$tmpfile" + json_add_string "preview_file" "$tmpfile" + else + json_add_boolean "success" 0 + json_add_string "error" "Preview failed: $preview" + fi + ;; + *) + json_add_boolean "success" 0 + json_add_string "error" "Invalid type. Use: dev, services, or all" + ;; + esac + + json_dump +} + +# Method: generate - Generate a report +method_generate() { + local type="$1" + + json_init + + if [ -z "$type" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Report type required (dev|services|all)" + json_dump + return + fi + + case "$type" in + dev|services|all) + local output=$("$REPORTCTL" generate "$type" 2>&1) + local result=$? + + if [ $result -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "type" "$type" + json_add_string "message" "Report generated successfully" + + # Return generated file info + config_load "$CONFIG_NAME" + local output_dir + config_get output_dir global output_dir '/www/reports' + + if [ "$type" = "dev" ] || [ "$type" = "all" ]; then + json_add_string "dev_url" "/reports/dev-status.html" + fi + if [ "$type" = "services" ] || [ "$type" = "all" ]; then + json_add_string "services_url" "/reports/services-status.html" + fi + else + json_add_boolean "success" 0 + json_add_string "error" "Generation failed: $output" + fi + ;; + *) + json_add_boolean "success" 0 + json_add_string "error" "Invalid type. Use: dev, services, or all" + ;; + esac + + json_dump +} + +# Method: send - Generate and send report via email +method_send() { + local type="$1" + + json_init + + if [ -z "$type" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Report type required (dev|services|all)" + json_dump + return + fi + + case "$type" in + dev|services|all) + local output=$("$REPORTCTL" send "$type" 2>&1) + local result=$? + + if [ $result -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "type" "$type" + json_add_string "message" "Report generated and sent" + else + json_add_boolean "success" 0 + json_add_string "error" "Send failed: $output" + fi + ;; + *) + json_add_boolean "success" 0 + json_add_string "error" "Invalid type. Use: dev, services, or all" + ;; + esac + + json_dump +} + +# Method: schedule - Set report schedule +method_schedule() { + local type="$1" + local frequency="$2" + + json_init + + if [ -z "$type" ] || [ -z "$frequency" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Type and frequency required" + json_dump + return + fi + + case "$type" in + dev|services) + local output=$("$REPORTCTL" schedule "$type" "$frequency" 2>&1) + local result=$? + + if [ $result -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "type" "$type" + json_add_string "frequency" "$frequency" + json_add_string "message" "Schedule updated" + else + json_add_boolean "success" 0 + json_add_string "error" "Schedule failed: $output" + fi + ;; + *) + json_add_boolean "success" 0 + json_add_string "error" "Invalid type. Use: dev or services" + ;; + esac + + json_dump +} + +# Method: delete_report - Delete a report file +method_delete_report() { + local filename="$1" + + json_init + + if [ -z "$filename" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Filename required" + json_dump + return + fi + + # Sanitize filename + filename=$(basename "$filename") + + config_load "$CONFIG_NAME" + local output_dir + config_get output_dir global output_dir '/www/reports' + + local filepath="$output_dir/$filename" + + if [ ! -f "$filepath" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Report not found" + json_dump + return + fi + + rm -f "$filepath" + + if [ ! -f "$filepath" ]; then + json_add_boolean "success" 1 + json_add_string "message" "Report deleted" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to delete report" + fi + + json_dump +} + +# Method: test_email - Test email configuration +method_test_email() { + # Source the mailer library + . /usr/share/secubox-reporter/lib/mailer.sh + + json_init + + local output=$(test_email 2>&1) + local result=$? + + if [ $result -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "$output" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + +# Method: get_config - Get configuration +method_get_config() { + config_load "$CONFIG_NAME" + + local enabled output_dir theme history_file + local recipient smtp_server smtp_port smtp_user smtp_tls + + config_get enabled global enabled '0' + config_get output_dir global output_dir '/www/reports' + config_get theme global theme 'dark' + + config_get recipient email recipient '' + config_get smtp_server email smtp_server '' + config_get smtp_port email smtp_port '587' + config_get smtp_user email smtp_user '' + config_get smtp_tls email smtp_tls '1' + + config_get history_file sources history_file '' + + json_init + + json_add_object "global" + json_add_boolean "enabled" "$enabled" + json_add_string "output_dir" "$output_dir" + json_add_string "theme" "$theme" + json_close_object + + json_add_object "email" + json_add_string "recipient" "$recipient" + json_add_string "smtp_server" "$smtp_server" + json_add_int "smtp_port" "$smtp_port" + json_add_string "smtp_user" "$smtp_user" + json_add_boolean "smtp_tls" "$smtp_tls" + json_close_object + + json_add_object "sources" + json_add_string "history_file" "$history_file" + json_close_object + + json_dump +} + +# Main dispatcher +case "$1" in + list) + method_list + ;; + call) + case "$2" in + status) + method_status + ;; + list_reports) + method_list_reports + ;; + get_report) + read -r input + json_load "$input" + json_get_var filename filename + method_get_report "$filename" + ;; + preview) + read -r input + json_load "$input" + json_get_var type type + method_preview "$type" + ;; + generate) + read -r input + json_load "$input" + json_get_var type type + method_generate "$type" + ;; + send) + read -r input + json_load "$input" + json_get_var type type + method_send "$type" + ;; + schedule) + read -r input + json_load "$input" + json_get_var type type + json_get_var frequency frequency + method_schedule "$type" "$frequency" + ;; + delete_report) + read -r input + json_load "$input" + json_get_var filename filename + method_delete_report "$filename" + ;; + test_email) + method_test_email + ;; + get_config) + method_get_config + ;; + *) + echo '{"error":"Unknown method"}' + ;; + esac + ;; + *) + echo '{"error":"Unknown command"}' + ;; +esac diff --git a/package/secubox/luci-app-reporter/root/usr/share/luci/menu.d/luci-app-reporter.json b/package/secubox/luci-app-reporter/root/usr/share/luci/menu.d/luci-app-reporter.json new file mode 100644 index 00000000..b8b90221 --- /dev/null +++ b/package/secubox/luci-app-reporter/root/usr/share/luci/menu.d/luci-app-reporter.json @@ -0,0 +1,13 @@ +{ + "admin/secubox/system/reporter": { + "title": "Report Generator", + "order": 6, + "action": { + "type": "view", + "path": "reporter/overview" + }, + "depends": { + "acl": ["luci-app-reporter"] + } + } +} diff --git a/package/secubox/luci-app-reporter/root/usr/share/rpcd/acl.d/luci-app-reporter.json b/package/secubox/luci-app-reporter/root/usr/share/rpcd/acl.d/luci-app-reporter.json new file mode 100644 index 00000000..c9b394c6 --- /dev/null +++ b/package/secubox/luci-app-reporter/root/usr/share/rpcd/acl.d/luci-app-reporter.json @@ -0,0 +1,29 @@ +{ + "luci-app-reporter": { + "description": "Grant access to SecuBox Report Generator", + "read": { + "ubus": { + "luci.reporter": [ + "status", + "list_reports", + "get_report", + "preview", + "get_config" + ] + }, + "uci": ["secubox-reporter"] + }, + "write": { + "ubus": { + "luci.reporter": [ + "generate", + "send", + "schedule", + "delete_report", + "test_email" + ] + }, + "uci": ["secubox-reporter"] + } + } +}