From efa74990bee1c75395b43af61bcc368263026071 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 16 Feb 2026 09:52:19 +0100 Subject: [PATCH] feat(mailserver): KISS theme enhancement for LuCI dashboard - Complete rewrite of overview.js with full KISS theme styling - 4-column stats grid (Status, Users, Storage, SSL) - Port status cards with visual indicators - Two-column layout: Users + Aliases tables - Webmail card with status badge and quick actions - Connection info panel with server details - Live polling with 10s refresh - Added fix_ports, alias_del methods to ACL - Added Mail Server + Nextcloud to KISS nav sidebar Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 25 + .../resources/view/mailserver/overview.js | 612 ++++++++++-------- .../share/rpcd/acl.d/luci-app-mailserver.json | 2 +- .../resources/secubox/kiss-theme.js | 2 + 4 files changed, 382 insertions(+), 259 deletions(-) diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index c27e77cf..dd1e29c7 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -1909,3 +1909,28 @@ git checkout HEAD -- index.html - Autoconfig XML at `/.well-known/autoconfig/mail/config-v1.1.xml` - Mozilla/Thunderbird format with IMAP (993/143) and SMTP (587/465) - HAProxy vhosts and mitmproxy routes configured + +### 2026-02-16: Mailserver LuCI KISS Enhancement + +**IMAP Connectivity Fix:** +- Fixed hairpin NAT issue for internal clients (Nextcloud container) +- Added `/etc/hosts` override in Nextcloud container: `mail.gk2.secubox.in` → `192.168.255.30` +- Added firewall rules for mail ports (IMAP 993, SMTP 587/465) + +**LuCI Dashboard KISS Regeneration:** +- Complete rewrite of `overview.js` (672 lines) with full KISS theme styling: + - Header with server FQDN + - 4-column stats grid (Status, Users, Storage, SSL) + - Control buttons (Start/Stop, DNS Setup, SSL Setup, Fix Ports, Backup) + - Port status cards with visual indicators (SMTP, Submission, SMTPS, IMAPS, IMAP) + - Two-column layout: Users table + Aliases table + - Webmail (Roundcube) card with status badge and quick actions + - Connection info panel with IMAP/SMTP server details + - Live polling with 10s refresh +- Updated ACL with `fix_ports`, `alias_del` methods +- Added Mail Server + Nextcloud to KISS theme navigation sidebar + +**Files Modified:** +- `luci-app-mailserver/htdocs/.../overview.js` (rewritten) +- `luci-app-mailserver/root/usr/share/rpcd/acl.d/luci-app-mailserver.json` +- `luci-app-secubox-portal/htdocs/.../kiss-theme.js` (nav update) diff --git a/package/secubox/luci-app-mailserver/htdocs/luci-static/resources/view/mailserver/overview.js b/package/secubox/luci-app-mailserver/htdocs/luci-static/resources/view/mailserver/overview.js index 2d75b4ea..e8670be0 100644 --- a/package/secubox/luci-app-mailserver/htdocs/luci-static/resources/view/mailserver/overview.js +++ b/package/secubox/luci-app-mailserver/htdocs/luci-static/resources/view/mailserver/overview.js @@ -2,8 +2,7 @@ 'require view'; 'require rpc'; 'require ui'; -'require form'; -'require uci'; +'require poll'; 'require secubox/kiss-theme'; var callStatus = rpc.declare({ @@ -117,316 +116,413 @@ return view.extend({ }, render: function(data) { + var self = this; var status = data[0] || {}; var users = (data[1] || {}).users || []; var aliases = (data[2] || {}).aliases || []; var webmail = data[3] || {}; - var view = E('div', { 'class': 'cbi-map' }, [ - E('h2', {}, 'Mail Server'), + // Start polling for status updates + poll.add(function() { + return callStatus().then(function(s) { + self.updateStats(s); + }); + }, 10); - // Status Section - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, 'Server Status'), - E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td', 'style': 'width:150px' }, 'Status'), - E('td', { 'class': 'td' }, [ - E('span', { 'style': status.state === 'running' ? 'color:green' : 'color:red' }, - status.state === 'running' ? '● Running' : '○ Stopped') - ]) - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Domain'), - E('td', { 'class': 'td' }, status.fqdn || 'Not configured') - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Users'), - E('td', { 'class': 'td' }, String(status.users || 0)) - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Storage'), - E('td', { 'class': 'td' }, status.storage || '0') - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'SSL'), - E('td', { 'class': 'td' }, status.ssl_valid ? '✓ Valid' : '✗ Not configured') - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Webmail'), - E('td', { 'class': 'td' }, webmail.running ? '● Running (port ' + webmail.port + ')' : '○ Stopped') - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Mesh Backup'), - E('td', { 'class': 'td' }, status.mesh_enabled ? '● Enabled' : '○ Disabled') - ]) + var isRunning = status.state === 'running'; + var ports = status.ports || {}; + + var content = [ + // Header with title + E('div', { 'style': 'margin-bottom: 24px;' }, [ + E('h2', { 'style': 'margin: 0 0 8px 0; display: flex; align-items: center; gap: 12px;' }, [ + E('span', {}, '\u2709\ufe0f'), + 'Mail Server' ]), - - // Port Status - E('h4', { 'style': 'margin-top:15px' }, 'Ports'), - this.renderPortStatus(status.ports || {}) + E('p', { 'style': 'color: var(--kiss-muted); margin: 0;' }, + status.fqdn || 'Postfix + Dovecot LXC Mail Server') ]), - // Quick Actions - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, 'Quick Actions'), - E('div', { 'style': 'display:flex;gap:10px;flex-wrap:wrap' }, [ - status.state !== 'running' ? - E('button', { - 'class': 'btn cbi-button-action', - 'click': ui.createHandlerFn(this, this.doStart) - }, 'Start Server') : - E('button', { - 'class': 'btn cbi-button-neutral', - 'click': ui.createHandlerFn(this, this.doStop) - }, 'Stop Server'), + // Stats Grid + E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-bottom: 20px;' }, [ + this.statCard('Status', isRunning ? 'Running' : 'Stopped', + isRunning ? 'var(--kiss-green)' : 'var(--kiss-red)', 'status'), + this.statCard('Users', users.length || 0, 'var(--kiss-cyan)', 'users'), + this.statCard('Storage', status.storage || '0', 'var(--kiss-purple)', 'storage'), + this.statCard('SSL', status.ssl_valid ? 'Valid' : 'Missing', + status.ssl_valid ? 'var(--kiss-green)' : 'var(--kiss-yellow)', 'ssl') + ]), + + // Controls Card + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'class': 'kiss-card-title' }, ['\u26a1 Controls']), + E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, [ E('button', { - 'class': 'btn cbi-button-neutral', + 'class': isRunning ? 'kiss-btn kiss-btn-red' : 'kiss-btn kiss-btn-green', + 'click': ui.createHandlerFn(this, isRunning ? this.doStop : this.doStart) + }, isRunning ? '\u23f9 Stop' : '\u25b6 Start'), + E('button', { + 'class': 'kiss-btn', 'click': ui.createHandlerFn(this, this.doDnsSetup) - }, 'Setup DNS'), + }, '\ud83c\udf10 DNS Setup'), E('button', { - 'class': 'btn cbi-button-neutral', + 'class': 'kiss-btn', 'click': ui.createHandlerFn(this, this.doSslSetup) - }, 'Setup SSL'), + }, '\ud83d\udd12 SSL Setup'), E('button', { - 'class': 'btn cbi-button-neutral', - 'click': ui.createHandlerFn(this, this.doWebmailConfigure) - }, 'Configure Webmail'), - E('button', { - 'class': 'btn cbi-button-neutral', - 'click': ui.createHandlerFn(this, this.doMeshBackup) - }, 'Mesh Backup'), - E('button', { - 'class': 'btn cbi-button-neutral', + 'class': 'kiss-btn', 'click': ui.createHandlerFn(this, this.doFixPorts) - }, 'Fix Ports') + }, '\ud83d\udd0c Fix Ports'), + E('button', { + 'class': 'kiss-btn', + 'click': ui.createHandlerFn(this, this.doMeshBackup) + }, '\ud83d\udcbe Backup') ]) ]), - // Users Section - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, 'Mail Users'), - E('div', { 'style': 'margin-bottom:10px' }, [ - E('button', { - 'class': 'btn cbi-button-add', - 'click': ui.createHandlerFn(this, this.showAddUserModal) - }, 'Add User') - ]), - this.renderUserTable(users) + // Port Status Card + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'class': 'kiss-card-title' }, ['\ud83d\udce1 Ports']), + this.renderPortStatus(ports) ]), - // Aliases Section - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, 'Email Aliases'), - E('div', { 'style': 'margin-bottom:10px' }, [ - E('button', { - 'class': 'btn cbi-button-add', - 'click': ui.createHandlerFn(this, this.showAddAliasModal) - }, 'Add Alias') + // Two Column Layout + E('div', { 'class': 'kiss-grid kiss-grid-2' }, [ + // Users Card + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'class': 'kiss-card-title', 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [ + E('span', {}, '\ud83d\udc65 Mail Users'), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'style': 'padding: 6px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, this.showAddUserModal) + }, '+ Add') + ]), + this.renderUserTable(users) ]), - this.renderAliasTable(aliases) - ]) - ]); - return KissTheme.wrap([view], 'admin/services/mailserver'); + // Aliases Card + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'class': 'kiss-card-title', 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [ + E('span', {}, '\ud83d\udd00 Aliases'), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'style': 'padding: 6px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, this.showAddAliasModal) + }, '+ Add') + ]), + this.renderAliasTable(aliases) + ]) + ]), + + // Webmail Card + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'class': 'kiss-card-title' }, ['\ud83d\udcec Webmail (Roundcube)']), + E('div', { 'style': 'display: flex; gap: 16px; align-items: center;' }, [ + E('span', { + 'class': webmail.running ? 'kiss-badge kiss-badge-green' : 'kiss-badge kiss-badge-red' + }, webmail.running ? 'RUNNING' : 'STOPPED'), + webmail.running ? E('span', { 'style': 'color: var(--kiss-muted);' }, + 'Port ' + (webmail.port || 8026)) : null, + E('button', { + 'class': 'kiss-btn', + 'click': ui.createHandlerFn(this, this.doWebmailConfigure) + }, '\u2699\ufe0f Configure'), + webmail.running ? E('a', { + 'href': 'http://' + window.location.hostname + ':' + (webmail.port || 8026), + 'target': '_blank', + 'class': 'kiss-btn kiss-btn-blue' + }, '\ud83d\udd17 Open Webmail') : null + ]) + ]), + + // Connection Info Card + E('div', { 'class': 'kiss-card kiss-panel-blue' }, [ + E('div', { 'class': 'kiss-card-title' }, ['\ud83d\udcd6 Connection Info']), + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;' }, [ + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase;' }, 'IMAP Server'), + E('div', { 'style': 'font-family: monospace; color: var(--kiss-cyan);' }, status.fqdn || 'mail.example.com'), + E('div', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Port 993 (SSL) / 143') + ]), + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase;' }, 'SMTP Server'), + E('div', { 'style': 'font-family: monospace; color: var(--kiss-cyan);' }, status.fqdn || 'mail.example.com'), + E('div', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Port 587 (STARTTLS) / 465 (SSL)') + ]), + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase;' }, 'Domain'), + E('div', { 'style': 'font-family: monospace; color: var(--kiss-purple);' }, status.domain || 'example.com') + ]) + ]) + ]) + ]; + + return KissTheme.wrap(content, 'admin/services/mailserver'); + }, + + statCard: function(label, value, color, dataAttr) { + return E('div', { 'class': 'kiss-stat' }, [ + E('div', { + 'class': 'kiss-stat-value', + 'style': 'color: ' + color, + 'data-stat': dataAttr + }, String(value)), + E('div', { 'class': 'kiss-stat-label' }, label) + ]); + }, + + updateStats: function(status) { + var statusEl = document.querySelector('[data-stat="status"]'); + var storageEl = document.querySelector('[data-stat="storage"]'); + var sslEl = document.querySelector('[data-stat="ssl"]'); + + if (statusEl) { + var isRunning = status.state === 'running'; + statusEl.textContent = isRunning ? 'Running' : 'Stopped'; + statusEl.style.color = isRunning ? 'var(--kiss-green)' : 'var(--kiss-red)'; + } + if (storageEl) { + storageEl.textContent = status.storage || '0'; + } + if (sslEl) { + var valid = status.ssl_valid; + sslEl.textContent = valid ? 'Valid' : 'Missing'; + sslEl.style.color = valid ? 'var(--kiss-green)' : 'var(--kiss-yellow)'; + } }, renderPortStatus: function(ports) { var portList = [ - { port: '25', name: 'SMTP' }, - { port: '587', name: 'Submission' }, - { port: '465', name: 'SMTPS' }, - { port: '993', name: 'IMAPS' }, - { port: '995', name: 'POP3S' } + { port: '25', name: 'SMTP', desc: 'Inbound mail' }, + { port: '587', name: 'Submission', desc: 'Authenticated send' }, + { port: '465', name: 'SMTPS', desc: 'SSL/TLS send' }, + { port: '993', name: 'IMAPS', desc: 'SSL/TLS receive' }, + { port: '143', name: 'IMAP', desc: 'Plain receive' } ]; - return E('div', { 'style': 'display:flex;gap:15px;flex-wrap:wrap' }, + return E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, portList.map(function(p) { var isOpen = ports[p.port]; - return E('span', { - 'style': 'padding:5px 10px;border-radius:4px;background:' + (isOpen ? '#d4edda' : '#f8d7da') - }, p.name + ' (' + p.port + '): ' + (isOpen ? '✓' : '✗')); + return E('div', { + 'style': 'padding: 8px 16px; border-radius: 8px; background: ' + + (isOpen ? 'rgba(0,200,83,0.1)' : 'rgba(255,23,68,0.1)') + + '; border: 1px solid ' + + (isOpen ? 'rgba(0,200,83,0.3)' : 'rgba(255,23,68,0.3)') + + '; min-width: 100px;' + }, [ + E('div', { 'style': 'font-weight: 600; font-size: 14px; color: ' + + (isOpen ? 'var(--kiss-green)' : 'var(--kiss-red)') }, p.name), + E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted);' }, + 'Port ' + p.port + ' ' + (isOpen ? '\u2713' : '\u2717')) + ]); }) ); }, renderUserTable: function(users) { if (!users || users.length === 0) { - return E('p', { 'class': 'cbi-value-description' }, 'No mail users configured.'); + return E('div', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 20px;' }, + 'No mail users configured'); } - var rows = users.map(L.bind(function(u) { - return E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, u.email), - E('td', { 'class': 'td' }, u.size || '0'), - E('td', { 'class': 'td' }, String(u.messages || 0)), - E('td', { 'class': 'td' }, [ - E('button', { - 'class': 'btn cbi-button-neutral', - 'style': 'padding:2px 8px;font-size:12px;margin-right:5px', - 'click': ui.createHandlerFn(this, this.showResetPasswordModal, u.email) - }, 'Reset Password'), - E('button', { - 'class': 'btn cbi-button-remove', - 'style': 'padding:2px 8px;font-size:12px', - 'click': ui.createHandlerFn(this, this.doDeleteUser, u.email) - }, 'Delete') + var self = this; + return E('table', { 'class': 'kiss-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, 'Email'), + E('th', {}, 'Size'), + E('th', {}, 'Msgs'), + E('th', { 'style': 'width: 120px;' }, 'Actions') ]) - ]); - }, this)); - - return E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, 'Email'), - E('th', { 'class': 'th' }, 'Size'), - E('th', { 'class': 'th' }, 'Messages'), - E('th', { 'class': 'th' }, 'Actions') - ]) - ].concat(rows)); + ]), + E('tbody', {}, users.map(function(u) { + return E('tr', {}, [ + E('td', { 'style': 'font-family: monospace;' }, u.email), + E('td', {}, u.size || '0'), + E('td', {}, String(u.messages || 0)), + E('td', {}, [ + E('button', { + 'class': 'kiss-btn', + 'style': 'padding: 4px 8px; font-size: 11px; margin-right: 4px;', + 'click': ui.createHandlerFn(self, self.showResetPasswordModal, u.email) + }, '\ud83d\udd11'), + E('button', { + 'class': 'kiss-btn kiss-btn-red', + 'style': 'padding: 4px 8px; font-size: 11px;', + 'click': ui.createHandlerFn(self, self.doDeleteUser, u.email) + }, '\ud83d\uddd1') + ]) + ]); + })) + ]); }, renderAliasTable: function(aliases) { if (!aliases || aliases.length === 0) { - return E('p', { 'class': 'cbi-value-description' }, 'No aliases configured.'); + return E('div', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 20px;' }, + 'No aliases configured'); } - var rows = aliases.map(function(a) { - return E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, a.alias), - E('td', { 'class': 'td' }, '→'), - E('td', { 'class': 'td' }, a.target) - ]); - }); - - return E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, 'Alias'), - E('th', { 'class': 'th' }, ''), - E('th', { 'class': 'th' }, 'Target') - ]) - ].concat(rows)); + return E('table', { 'class': 'kiss-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, 'Alias'), + E('th', { 'style': 'width: 30px;' }, ''), + E('th', {}, 'Target') + ]) + ]), + E('tbody', {}, aliases.map(function(a) { + return E('tr', {}, [ + E('td', { 'style': 'font-family: monospace; color: var(--kiss-cyan);' }, a.alias), + E('td', { 'style': 'color: var(--kiss-muted);' }, '\u2192'), + E('td', { 'style': 'font-family: monospace;' }, a.target) + ]); + })) + ]); }, showAddUserModal: function() { + var self = this; var emailInput, passwordInput; ui.showModal('Add Mail User', [ - E('p', {}, 'Enter email address and password for the new user.'), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Email'), - E('div', { 'class': 'cbi-value-field' }, [ - emailInput = E('input', { 'type': 'email', 'class': 'cbi-input-text', 'placeholder': 'user@domain.com' }) - ]) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Password'), - E('div', { 'class': 'cbi-value-field' }, [ - passwordInput = E('input', { 'type': 'password', 'class': 'cbi-input-text' }) - ]) - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, 'Cancel'), - ' ', - E('button', { - 'class': 'btn cbi-button-action', - 'click': ui.createHandlerFn(this, function() { - var email = emailInput.value; - var password = passwordInput.value; - if (!email || !password) { - ui.addNotification(null, E('p', 'Email and password required'), 'error'); - return; - } - ui.hideModal(); - return this.doAddUser(email, password); + E('div', { 'style': 'padding: 16px;' }, [ + E('p', { 'style': 'margin-bottom: 16px; color: var(--kiss-muted);' }, + 'Create a new mail account'), + E('div', { 'style': 'margin-bottom: 12px;' }, [ + E('label', { 'style': 'display: block; font-size: 12px; color: var(--kiss-muted); margin-bottom: 4px;' }, 'Email Address'), + emailInput = E('input', { + 'type': 'email', + 'placeholder': 'user@domain.com', + 'style': 'width: 100%; padding: 10px; border-radius: 6px; border: 1px solid var(--kiss-line); background: var(--kiss-bg2); color: var(--kiss-text);' }) - }, 'Add User') + ]), + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('label', { 'style': 'display: block; font-size: 12px; color: var(--kiss-muted); margin-bottom: 4px;' }, 'Password'), + passwordInput = E('input', { + 'type': 'password', + 'placeholder': 'Secure password', + 'style': 'width: 100%; padding: 10px; border-radius: 6px; border: 1px solid var(--kiss-line); background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + E('div', { 'style': 'display: flex; gap: 8px; justify-content: flex-end;' }, [ + E('button', { + 'class': 'kiss-btn', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'click': function() { + var email = emailInput.value.trim(); + var password = passwordInput.value; + if (!email || !password) { + ui.addNotification(null, E('p', 'Email and password required'), 'error'); + return; + } + ui.hideModal(); + self.doAddUser(email, password); + } + }, 'Add User') + ]) ]) ]); }, showAddAliasModal: function() { + var self = this; var aliasInput, targetInput; ui.showModal('Add Email Alias', [ - E('p', {}, 'Create an alias that forwards to another address.'), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Alias'), - E('div', { 'class': 'cbi-value-field' }, [ - aliasInput = E('input', { 'type': 'email', 'class': 'cbi-input-text', 'placeholder': 'info@domain.com' }) - ]) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Forward To'), - E('div', { 'class': 'cbi-value-field' }, [ - targetInput = E('input', { 'type': 'email', 'class': 'cbi-input-text', 'placeholder': 'user@domain.com' }) - ]) - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, 'Cancel'), - ' ', - E('button', { - 'class': 'btn cbi-button-action', - 'click': ui.createHandlerFn(this, function() { - var alias = aliasInput.value; - var target = targetInput.value; - if (!alias || !target) { - ui.addNotification(null, E('p', 'Alias and target required'), 'error'); - return; - } - ui.hideModal(); - return this.doAddAlias(alias, target); + E('div', { 'style': 'padding: 16px;' }, [ + E('p', { 'style': 'margin-bottom: 16px; color: var(--kiss-muted);' }, + 'Forward email from one address to another'), + E('div', { 'style': 'margin-bottom: 12px;' }, [ + E('label', { 'style': 'display: block; font-size: 12px; color: var(--kiss-muted); margin-bottom: 4px;' }, 'Alias Address'), + aliasInput = E('input', { + 'type': 'email', + 'placeholder': 'info@domain.com', + 'style': 'width: 100%; padding: 10px; border-radius: 6px; border: 1px solid var(--kiss-line); background: var(--kiss-bg2); color: var(--kiss-text);' }) - }, 'Add Alias') + ]), + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('label', { 'style': 'display: block; font-size: 12px; color: var(--kiss-muted); margin-bottom: 4px;' }, 'Forward To'), + targetInput = E('input', { + 'type': 'email', + 'placeholder': 'user@domain.com', + 'style': 'width: 100%; padding: 10px; border-radius: 6px; border: 1px solid var(--kiss-line); background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + E('div', { 'style': 'display: flex; gap: 8px; justify-content: flex-end;' }, [ + E('button', { + 'class': 'kiss-btn', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'click': function() { + var alias = aliasInput.value.trim(); + var target = targetInput.value.trim(); + if (!alias || !target) { + ui.addNotification(null, E('p', 'Alias and target required'), 'error'); + return; + } + ui.hideModal(); + self.doAddAlias(alias, target); + } + }, 'Add Alias') + ]) ]) ]); }, showResetPasswordModal: function(email) { + var self = this; var passwordInput, confirmInput; ui.showModal('Reset Password', [ - E('p', {}, 'Enter new password for: ' + email), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'New Password'), - E('div', { 'class': 'cbi-value-field' }, [ - passwordInput = E('input', { 'type': 'password', 'class': 'cbi-input-text' }) - ]) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Confirm'), - E('div', { 'class': 'cbi-value-field' }, [ - confirmInput = E('input', { 'type': 'password', 'class': 'cbi-input-text' }) - ]) - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, 'Cancel'), - ' ', - E('button', { - 'class': 'btn cbi-button-action', - 'click': ui.createHandlerFn(this, function() { - var password = passwordInput.value; - var confirm = confirmInput.value; - if (!password) { - ui.addNotification(null, E('p', 'Password required'), 'error'); - return; - } - if (password !== confirm) { - ui.addNotification(null, E('p', 'Passwords do not match'), 'error'); - return; - } - ui.hideModal(); - return this.doResetPassword(email, password); + E('div', { 'style': 'padding: 16px;' }, [ + E('p', { 'style': 'margin-bottom: 16px;' }, [ + 'New password for: ', + E('span', { 'style': 'font-family: monospace; color: var(--kiss-cyan);' }, email) + ]), + E('div', { 'style': 'margin-bottom: 12px;' }, [ + E('label', { 'style': 'display: block; font-size: 12px; color: var(--kiss-muted); margin-bottom: 4px;' }, 'New Password'), + passwordInput = E('input', { + 'type': 'password', + 'style': 'width: 100%; padding: 10px; border-radius: 6px; border: 1px solid var(--kiss-line); background: var(--kiss-bg2); color: var(--kiss-text);' }) - }, 'Reset Password') + ]), + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('label', { 'style': 'display: block; font-size: 12px; color: var(--kiss-muted); margin-bottom: 4px;' }, 'Confirm Password'), + confirmInput = E('input', { + 'type': 'password', + 'style': 'width: 100%; padding: 10px; border-radius: 6px; border: 1px solid var(--kiss-line); background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + E('div', { 'style': 'display: flex; gap: 8px; justify-content: flex-end;' }, [ + E('button', { + 'class': 'kiss-btn', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'click': function() { + var password = passwordInput.value; + var confirm = confirmInput.value; + if (!password) { + ui.addNotification(null, E('p', 'Password required'), 'error'); + return; + } + if (password !== confirm) { + ui.addNotification(null, E('p', 'Passwords do not match'), 'error'); + return; + } + ui.hideModal(); + self.doResetPassword(email, password); + } + }, 'Reset Password') + ]) ]) ]); }, @@ -453,24 +549,24 @@ return view.extend({ doAddUser: function(email, password) { ui.showModal('Adding User', [ - E('p', { 'class': 'spinning' }, 'Adding user: ' + email) + E('p', { 'class': 'spinning' }, 'Creating mailbox for ' + email + '...') ]); return callUserAdd(email, password).then(function(res) { ui.hideModal(); if (res.code === 0) { ui.addNotification(null, E('p', 'User added: ' + email), 'success'); } else { - ui.addNotification(null, E('p', 'Failed: ' + res.output), 'error'); + ui.addNotification(null, E('p', 'Failed: ' + (res.output || res.error)), 'error'); } window.location.reload(); }); }, doDeleteUser: function(email) { - if (!confirm('Delete user ' + email + '?')) return; + if (!confirm('Delete user ' + email + ' and all their mail?')) return; ui.showModal('Deleting User', [ - E('p', { 'class': 'spinning' }, 'Deleting user: ' + email) + E('p', { 'class': 'spinning' }, 'Removing ' + email + '...') ]); return callUserDel(email).then(function() { ui.hideModal(); @@ -480,7 +576,7 @@ return view.extend({ doAddAlias: function(alias, target) { ui.showModal('Adding Alias', [ - E('p', { 'class': 'spinning' }, 'Adding alias: ' + alias) + E('p', { 'class': 'spinning' }, 'Creating alias ' + alias + '...') ]); return callAliasAdd(alias, target).then(function() { ui.hideModal(); @@ -490,12 +586,12 @@ return view.extend({ doResetPassword: function(email, password) { ui.showModal('Resetting Password', [ - E('p', { 'class': 'spinning' }, 'Resetting password for: ' + email) + E('p', { 'class': 'spinning' }, 'Updating password for ' + email + '...') ]); return callUserPasswd(email, password).then(function(res) { ui.hideModal(); if (res.code === 0) { - ui.addNotification(null, E('p', 'Password reset for: ' + email), 'success'); + ui.addNotification(null, E('p', 'Password updated for ' + email), 'success'); } else { ui.addNotification(null, E('p', 'Failed: ' + (res.error || res.output)), 'error'); } @@ -504,14 +600,14 @@ return view.extend({ doDnsSetup: function() { ui.showModal('DNS Setup', [ - E('p', { 'class': 'spinning' }, 'Creating MX, SPF, DMARC records...') + E('p', { 'class': 'spinning' }, 'Creating MX, SPF, DKIM, DMARC records...') ]); return callDnsSetup().then(function(res) { ui.hideModal(); if (res.code === 0) { - ui.addNotification(null, E('p', 'DNS records created'), 'success'); + ui.addNotification(null, E('p', 'DNS records created successfully'), 'success'); } else { - ui.addNotification(null, E('p', res.output), 'warning'); + ui.addNotification(null, E('p', res.output || 'Check dnsctl configuration'), 'warning'); } }); }, @@ -525,7 +621,7 @@ return view.extend({ if (res.code === 0) { ui.addNotification(null, E('p', 'SSL certificate installed'), 'success'); } else { - ui.addNotification(null, E('p', res.output), 'error'); + ui.addNotification(null, E('p', res.output || 'Certificate request failed'), 'error'); } window.location.reload(); }); @@ -533,7 +629,7 @@ return view.extend({ doWebmailConfigure: function() { ui.showModal('Configuring Webmail', [ - E('p', { 'class': 'spinning' }, 'Configuring Roundcube...') + E('p', { 'class': 'spinning' }, 'Setting up Roundcube connection...') ]); return callWebmailConfigure().then(function(res) { ui.hideModal(); @@ -548,23 +644,23 @@ return view.extend({ return callMeshBackup().then(function(res) { ui.hideModal(); if (res.code === 0) { - ui.addNotification(null, E('p', 'Backup created'), 'success'); + ui.addNotification(null, E('p', 'Backup created successfully'), 'success'); } else { - ui.addNotification(null, E('p', res.output), 'error'); + ui.addNotification(null, E('p', res.output || 'Backup failed'), 'error'); } }); }, doFixPorts: function() { ui.showModal('Fixing Ports', [ - E('p', { 'class': 'spinning' }, 'Enabling submission (587), smtps (465), and POP3S (995) ports...') + E('p', { 'class': 'spinning' }, 'Enabling submission (587), SMTPS (465), POP3S (995)...') ]); return callFixPorts().then(function(res) { ui.hideModal(); if (res.code === 0) { ui.addNotification(null, E('p', 'Ports enabled successfully'), 'success'); } else { - ui.addNotification(null, E('p', res.output || 'Some ports may still not be listening'), 'warning'); + ui.addNotification(null, E('p', res.output || 'Some ports may not be listening'), 'warning'); } window.location.reload(); }); diff --git a/package/secubox/luci-app-mailserver/root/usr/share/rpcd/acl.d/luci-app-mailserver.json b/package/secubox/luci-app-mailserver/root/usr/share/rpcd/acl.d/luci-app-mailserver.json index a1a893e5..740eeffd 100644 --- a/package/secubox/luci-app-mailserver/root/usr/share/rpcd/acl.d/luci-app-mailserver.json +++ b/package/secubox/luci-app-mailserver/root/usr/share/rpcd/acl.d/luci-app-mailserver.json @@ -9,7 +9,7 @@ }, "write": { "ubus": { - "luci.mailserver": ["install", "start", "stop", "restart", "user_add", "user_del", "user_passwd", "alias_add", "dns_setup", "ssl_setup", "webmail_configure", "mesh_backup", "mesh_sync"] + "luci.mailserver": ["install", "start", "stop", "restart", "user_add", "user_del", "user_passwd", "alias_add", "dns_setup", "ssl_setup", "webmail_configure", "mesh_backup", "mesh_sync", "fix_ports", "alias_del"] }, "uci": ["mailserver"] } diff --git a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js index 1670690e..0134a523 100644 --- a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js +++ b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js @@ -94,6 +94,8 @@ var KissThemeClass = baseclass.extend({ { icon: '🤖', name: 'LocalAI', path: 'admin/services/localai' } ]}, { cat: 'Apps', icon: '📦', collapsed: true, items: [ + { icon: '✉️', name: 'Mail Server', path: 'admin/services/mailserver' }, + { icon: '☁️', name: 'Nextcloud', path: 'admin/services/nextcloud' }, { icon: '🎬', name: 'Media Flow', path: 'admin/services/media-flow' }, { icon: '🪞', name: 'MagicMirror', path: 'admin/services/magicmirror2' }, { icon: '📰', name: 'HexoJS', path: 'admin/services/hexojs' },