diff --git a/package/secubox/luci-app-secubox-portal/root/usr/libexec/rpcd/luci.secubox-portal b/package/secubox/luci-app-secubox-portal/root/usr/libexec/rpcd/luci.secubox-portal index e71a3f5d..3b2b6793 100644 --- a/package/secubox/luci-app-secubox-portal/root/usr/libexec/rpcd/luci.secubox-portal +++ b/package/secubox/luci-app-secubox-portal/root/usr/libexec/rpcd/luci.secubox-portal @@ -237,7 +237,7 @@ build_tree() { json_add_array "items" for app in $apps; do case "$app" in - luci-app-torrent*|luci-app-droplet*|luci-app-aria*|luci-app-transmission*|luci-app-nzb*|luci-app-sabnzbd*) + luci-app-torrent*|luci-app-droplet*|luci-app-webtorrent*|luci-app-aria*|luci-app-transmission*|luci-app-nzb*|luci-app-sabnzbd*) local name=$(echo "$app" | sed 's/luci-app-//' | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++)$i=toupper(substr($i,1,1))tolower(substr($i,2))}1') json_add_object "" json_add_string "name" "$name" diff --git a/package/secubox/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js b/package/secubox/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js index cc58ba39..cc35de3f 100644 --- a/package/secubox/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js +++ b/package/secubox/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js @@ -3,46 +3,43 @@ 'require dom'; 'require ui'; 'require rpc'; +'require poll'; 'require secubox/kiss-theme'; var callStatus = rpc.declare({ object: 'luci.secubox-users', method: 'status', - expect: { } + expect: {} }); var callUsers = rpc.declare({ object: 'luci.secubox-users', method: 'users', - expect: { } + expect: {} }); var callAddUser = rpc.declare({ object: 'luci.secubox-users', method: 'add', params: ['username', 'password', 'services'], - expect: { } + expect: {} }); var callDeleteUser = rpc.declare({ object: 'luci.secubox-users', method: 'delete', params: ['username'], - expect: { } + expect: {} }); var callPasswd = rpc.declare({ object: 'luci.secubox-users', method: 'passwd', params: ['username', 'password'], - expect: { } + expect: {} }); return view.extend({ - handleSaveApply: null, - handleSave: null, - handleReset: null, - load: function() { return Promise.all([ callStatus(), @@ -50,293 +47,335 @@ return view.extend({ ]); }, - renderStats: function(status, userCount) { - var c = KissTheme.colors; - var services = status.services || {}; - var activeServices = Object.keys(services).filter(function(s) { return services[s]; }).length; - return [ - KissTheme.stat(userCount, 'Users', c.blue), - KissTheme.stat(activeServices, 'Services', c.green), - KissTheme.stat(status.domain || 'N/A', 'Domain', c.purple) - ]; - }, - - renderServices: function(services) { - var serviceNames = ['nextcloud', 'peertube', 'matrix', 'jabber', 'email', 'gitea', 'jellyfin']; - var badges = serviceNames.map(function(name) { - var running = services[name]; - return KissTheme.badge(name.charAt(0).toUpperCase() + name.slice(1), running ? 'green' : 'muted'); - }); - return E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 8px;' }, badges); - }, - - renderUserRow: function(user) { - var self = this; - var services = (user.services || []).map(function(s) { - return E('span', { - 'style': 'display: inline-block; padding: 2px 6px; margin: 2px; border-radius: 4px; background: var(--kiss-bg); color: var(--kiss-cyan); font-size: 11px;' - }, s); - }); - - var lastLogin = user.last_login || 'never'; - var loginSuccess = user.login_success || 0; - var loginFailure = user.login_failure || 0; - var failColor = loginFailure > 10 ? 'var(--kiss-red)' : (loginFailure > 0 ? 'var(--kiss-orange)' : 'var(--kiss-green)'); - - return E('tr', {}, [ - E('td', { 'style': 'font-weight: 600;' }, user.username), - E('td', { 'style': 'color: var(--kiss-muted);' }, user.email), - E('td', { 'style': 'color: var(--kiss-muted);' }, lastLogin), - E('td', { 'style': 'text-align: center;' }, [ - E('span', { 'style': 'color: var(--kiss-green); font-weight: 600;' }, String(loginSuccess)), - E('span', { 'style': 'color: var(--kiss-muted);' }, ' / '), - E('span', { 'style': 'color: ' + failColor + '; font-weight: 600;' }, String(loginFailure)) - ]), - E('td', {}, services), - E('td', { 'style': 'width: 180px;' }, [ - E('div', { 'style': 'display: flex; gap: 8px;' }, [ - E('button', { - 'class': 'kiss-btn', - 'style': 'padding: 4px 10px; font-size: 11px;', - 'click': function() { self.handlePasswd(user.username); } - }, 'Password'), - E('button', { - 'class': 'kiss-btn kiss-btn-red', - 'style': 'padding: 4px 10px; font-size: 11px;', - 'click': function() { self.handleDelete(user.username); } - }, 'Delete') - ]) - ]) - ]); - }, - - renderUsersTable: function(users) { - var self = this; - if (!users || users.length === 0) { - return E('p', { 'style': 'color: var(--kiss-muted);' }, 'No users configured. Click "Add User" to create one.'); - } - - var rows = users.map(function(u) { return self.renderUserRow(u); }); - - return E('table', { 'class': 'kiss-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, 'Username'), - E('th', {}, 'Email'), - E('th', {}, 'Last Login'), - E('th', { 'style': 'text-align: center;' }, 'OK / Fail'), - E('th', {}, 'Services'), - E('th', {}, 'Actions') - ]) - ]), - E('tbody', {}, rows) - ]); - }, - - handleAdd: function() { - var self = this; - - ui.showModal('Add User', [ - E('div', { 'style': 'padding: 16px;' }, [ - E('div', { 'style': 'display: flex; flex-direction: column; gap: 16px;' }, [ - E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px;' }, [ - E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Username'), - E('input', { - 'type': 'text', - 'id': 'new-username', - 'style': 'background: var(--kiss-bg); border: 1px solid var(--kiss-line); color: var(--kiss-text); padding: 10px 12px; border-radius: 6px;' - }) - ]), - E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px;' }, [ - E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Password'), - E('input', { - 'type': 'password', - 'id': 'new-password', - 'placeholder': 'Leave empty to generate', - 'style': 'background: var(--kiss-bg); border: 1px solid var(--kiss-line); color: var(--kiss-text); padding: 10px 12px; border-radius: 6px;' - }) - ]), - E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px;' }, [ - E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Services'), - E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 12px;' }, [ - E('label', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-nextcloud', 'checked': true }), 'Nextcloud']), - E('label', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-peertube', 'checked': true }), 'PeerTube']), - E('label', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-jabber', 'checked': true }), 'Jabber']), - E('label', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-matrix', 'checked': true }), 'Matrix']), - E('label', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-email', 'checked': true }), 'Email']), - E('label', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-gitea', 'checked': true }), 'Gitea']), - E('label', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-jellyfin', 'checked': true }), 'Jellyfin']) - ]) - ]) - ]) - ]), - E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px; padding: 16px; border-top: 1px solid var(--kiss-line);' }, [ - E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'), - E('button', { - 'class': 'kiss-btn kiss-btn-green', - 'click': function() { - var username = document.getElementById('new-username').value; - var password = document.getElementById('new-password').value; - var services = []; - if (document.getElementById('svc-nextcloud').checked) services.push('nextcloud'); - if (document.getElementById('svc-peertube').checked) services.push('peertube'); - if (document.getElementById('svc-jabber').checked) services.push('jabber'); - if (document.getElementById('svc-matrix').checked) services.push('matrix'); - if (document.getElementById('svc-email').checked) services.push('email'); - if (document.getElementById('svc-gitea').checked) services.push('gitea'); - if (document.getElementById('svc-jellyfin').checked) services.push('jellyfin'); - - if (!username) { - ui.addNotification(null, E('p', 'Username required'), 'error'); - return; - } - - ui.hideModal(); - ui.showModal('Creating User...', [ - E('p', { 'class': 'spinning' }, 'Please wait...') - ]); - - callAddUser(username, password, services.join(',')).then(function(res) { - ui.hideModal(); - if (res && res.success) { - ui.showModal('User Created', [ - E('div', { 'style': 'padding: 20px;' }, [ - E('p', {}, 'User created successfully!'), - E('div', { 'style': 'margin: 16px 0; padding: 12px; background: var(--kiss-bg); border-radius: 6px;' }, [ - E('div', { 'style': 'display: flex; gap: 12px; margin-bottom: 8px;' }, [ - E('span', { 'style': 'color: var(--kiss-muted);' }, 'Username:'), - E('span', { 'style': 'font-weight: 600;' }, res.username) - ]), - E('div', { 'style': 'display: flex; gap: 12px; margin-bottom: 8px;' }, [ - E('span', { 'style': 'color: var(--kiss-muted);' }, 'Password:'), - E('span', { 'style': 'font-family: monospace; background: var(--kiss-bg2); padding: 4px 8px; border-radius: 4px;' }, res.password) - ]), - E('div', { 'style': 'display: flex; gap: 12px;' }, [ - E('span', { 'style': 'color: var(--kiss-muted);' }, 'Email:'), - E('span', {}, res.email) - ]) - ]) - ]), - E('div', { 'style': 'display: flex; justify-content: flex-end; padding: 16px;' }, [ - E('button', { 'class': 'kiss-btn kiss-btn-green', 'click': function() { ui.hideModal(); location.reload(); } }, 'OK') - ]) - ]); - } else { - ui.addNotification(null, E('p', 'Error: ' + (res.error || 'Unknown error')), 'error'); - } - }); - } - }, 'Create User') - ]) - ]); - }, - - handleDelete: function(username) { - if (!confirm('Delete user "' + username + '" from all services?')) { - return; - } - - ui.showModal('Deleting...', [ - E('p', { 'class': 'spinning' }, 'Please wait...') - ]); - - callDeleteUser(username).then(function(res) { - ui.hideModal(); - if (res && res.success) { - ui.addNotification(null, E('p', 'User deleted'), 'success'); - location.reload(); - } else { - ui.addNotification(null, E('p', 'Error: ' + (res.error || 'Unknown error')), 'error'); - } - }); - }, - - handlePasswd: function(username) { - ui.showModal('Change Password', [ - E('div', { 'style': 'padding: 16px;' }, [ - E('p', {}, 'Change password for: ' + username), - E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px; margin-top: 16px;' }, [ - E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'New Password'), - E('input', { - 'type': 'password', - 'id': 'new-passwd', - 'placeholder': 'Leave empty to generate', - 'style': 'background: var(--kiss-bg); border: 1px solid var(--kiss-line); color: var(--kiss-text); padding: 10px 12px; border-radius: 6px;' - }) - ]) - ]), - E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px; padding: 16px; border-top: 1px solid var(--kiss-line);' }, [ - E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'), - E('button', { - 'class': 'kiss-btn kiss-btn-blue', - 'click': function() { - var password = document.getElementById('new-passwd').value; - ui.hideModal(); - ui.showModal('Updating...', [ - E('p', { 'class': 'spinning' }, 'Please wait...') - ]); - - callPasswd(username, password).then(function(res) { - ui.hideModal(); - if (res && res.success) { - ui.showModal('Password Updated', [ - E('div', { 'style': 'padding: 20px;' }, [ - E('p', {}, 'Password updated for all services!'), - E('p', { 'style': 'font-family: monospace; background: var(--kiss-bg); padding: 12px; margin: 12px 0; border-radius: 6px;' }, res.password) - ]), - E('div', { 'style': 'display: flex; justify-content: flex-end; padding: 16px;' }, [ - E('button', { 'class': 'kiss-btn kiss-btn-green', 'click': ui.hideModal }, 'OK') - ]) - ]); - } else { - ui.addNotification(null, E('p', 'Error updating password'), 'error'); - } - }); - } - }, 'Update') - ]) - ]); - }, - render: function(data) { var self = this; var status = data[0] || {}; var usersData = data[1] || {}; var users = usersData.users || []; - var services = status.services || {}; + + // Start polling + poll.add(function() { + return callUsers().then(function(data) { + self.updateStats(data); + }); + }, 30); + + var totalLogins = 0; + var totalFailures = 0; + users.forEach(function(u) { + totalLogins += (u.login_success || 0); + totalFailures += (u.login_failure || 0); + }); var content = [ // Header E('div', { 'style': 'margin-bottom: 24px;' }, [ - E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [ - E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'SecuBox User Management'), - KissTheme.badge(users.length + ' Users', 'blue') + E('h2', { 'style': 'margin: 0 0 8px 0; display: flex; align-items: center; gap: 12px;' }, [ + E('span', {}, '\uD83D\uDC65'), + 'SecuBox Users' ]), - E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' }, - 'Manage unified user accounts across all SecuBox services') + E('p', { 'style': 'color: var(--kiss-muted); margin: 0;' }, + 'Unified user management across all SecuBox services') ]), - // Stats - E('div', { 'class': 'kiss-grid kiss-grid-3', 'style': 'margin: 20px 0;' }, - this.renderStats(status, users.length)), + // Stats Grid + E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-bottom: 20px;' }, [ + this.statCard('Users', users.length, 'var(--kiss-cyan)', 'users'), + this.statCard('Services', 6, 'var(--kiss-purple)', 'services'), + this.statCard('Logins', totalLogins, 'var(--kiss-green)', 'logins'), + this.statCard('Failures', totalFailures, + totalFailures > 50 ? 'var(--kiss-red)' : 'var(--kiss-yellow)', 'failures') + ]), - // Services - KissTheme.card('Available Services', this.renderServices(services)), - - // Users - KissTheme.card( - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [ - E('span', {}, 'Users'), + // 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 User Accounts'), E('button', { 'class': 'kiss-btn kiss-btn-green', - 'style': 'padding: 6px 14px;', - 'click': function() { self.handleAdd(); } - }, 'Add User') + 'style': 'padding: 6px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, this.showAddUserModal) + }, '+ Add User') ]), - this.renderUsersTable(users) - ) + this.renderUserTable(users) + ]), + + // Connected Services Card + E('div', { 'class': 'kiss-card kiss-panel-blue' }, [ + E('div', { 'class': 'kiss-card-title' }, ['\uD83D\uDD17 Connected Services']), + E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 16px;' }, [ + E('span', { 'class': 'kiss-badge kiss-badge-green' }, '\u2601 Nextcloud'), + E('span', { 'class': 'kiss-badge kiss-badge-green' }, '\uD83C\uDFAC PeerTube'), + E('span', { 'class': 'kiss-badge kiss-badge-green' }, '\uD83D\uDCAC Jabber'), + E('span', { 'class': 'kiss-badge kiss-badge-green' }, '\uD83D\uDCE1 Matrix'), + E('span', { 'class': 'kiss-badge kiss-badge-green' }, '\u2709 Email'), + E('span', { 'class': 'kiss-badge kiss-badge-green' }, '\uD83D\uDCBB Gitea') + ]), + E('p', { 'style': 'margin-top: 12px; color: var(--kiss-muted); font-size: 12px;' }, + 'Users are automatically provisioned across all enabled services.') + ]) ]; - return KissTheme.wrap(content, 'admin/system/secubox-users/overview'); - } + return KissTheme.wrap(content, 'admin/system/secubox-users'); + }, + + 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(data) { + var users = (data.users || []); + var usersEl = document.querySelector('[data-stat="users"]'); + var loginsEl = document.querySelector('[data-stat="logins"]'); + var failuresEl = document.querySelector('[data-stat="failures"]'); + + if (usersEl) usersEl.textContent = users.length; + + var totalLogins = 0, totalFailures = 0; + users.forEach(function(u) { + totalLogins += (u.login_success || 0); + totalFailures += (u.login_failure || 0); + }); + + if (loginsEl) loginsEl.textContent = totalLogins; + if (failuresEl) { + failuresEl.textContent = totalFailures; + failuresEl.style.color = totalFailures > 50 ? 'var(--kiss-red)' : 'var(--kiss-yellow)'; + } + }, + + renderUserTable: function(users) { + var self = this; + + if (!users.length) { + return E('div', { 'style': 'text-align: center; padding: 40px; color: var(--kiss-muted);' }, [ + E('p', { 'style': 'font-size: 48px; margin: 0;' }, '\uD83D\uDC65'), + E('p', {}, 'No users configured yet'), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'click': ui.createHandlerFn(this, this.showAddUserModal) + }, '+ Add First User') + ]); + } + + var rows = users.map(function(user) { + var services = (user.services || []).map(function(s) { + return E('span', { + 'style': 'display: inline-block; padding: 2px 8px; margin: 2px; border-radius: 4px; background: rgba(0,200,83,0.15); color: var(--kiss-green); font-size: 11px;' + }, s); + }); + + var failColor = (user.login_failure || 0) > 10 ? 'var(--kiss-red)' : + ((user.login_failure || 0) > 0 ? 'var(--kiss-yellow)' : 'var(--kiss-green)'); + + return E('tr', {}, [ + E('td', { 'style': 'padding: 12px 8px;' }, [ + E('div', { 'style': 'font-weight: 600;' }, user.username), + E('div', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, user.email || '') + ]), + E('td', { 'style': 'padding: 12px 8px; color: var(--kiss-muted);' }, user.last_login || 'Never'), + E('td', { 'style': 'padding: 12px 8px; text-align: center;' }, [ + E('span', { 'style': 'color: var(--kiss-green); font-weight: 600;' }, String(user.login_success || 0)), + E('span', { 'style': 'color: var(--kiss-muted);' }, ' / '), + E('span', { 'style': 'color: ' + failColor + '; font-weight: 600;' }, String(user.login_failure || 0)) + ]), + E('td', { 'style': 'padding: 12px 8px;' }, services), + E('td', { 'style': 'padding: 12px 8px; text-align: right;' }, [ + E('button', { + 'class': 'kiss-btn', + 'style': 'padding: 4px 10px; font-size: 12px; margin-right: 6px;', + 'click': function() { self.showPasswdModal(user.username); } + }, '\uD83D\uDD11 Password'), + E('button', { + 'class': 'kiss-btn kiss-btn-red', + 'style': 'padding: 4px 10px; font-size: 12px;', + 'click': function() { self.showDeleteModal(user.username); } + }, '\uD83D\uDDD1 Delete') + ]) + ]); + }); + + return E('table', { 'style': 'width: 100%; border-collapse: collapse;' }, [ + E('thead', {}, [ + E('tr', { 'style': 'border-bottom: 1px solid var(--kiss-border);' }, [ + E('th', { 'style': 'padding: 8px; text-align: left; color: var(--kiss-muted); font-size: 11px; text-transform: uppercase;' }, 'User'), + E('th', { 'style': 'padding: 8px; text-align: left; color: var(--kiss-muted); font-size: 11px; text-transform: uppercase;' }, 'Last Login'), + E('th', { 'style': 'padding: 8px; text-align: center; color: var(--kiss-muted); font-size: 11px; text-transform: uppercase;' }, 'Success / Fail'), + E('th', { 'style': 'padding: 8px; text-align: left; color: var(--kiss-muted); font-size: 11px; text-transform: uppercase;' }, 'Services'), + E('th', { 'style': 'padding: 8px; text-align: right; color: var(--kiss-muted); font-size: 11px; text-transform: uppercase;' }, 'Actions') + ]) + ]), + E('tbody', {}, rows) + ]); + }, + + showAddUserModal: function() { + var self = this; + + ui.showModal(_('Add User'), [ + E('div', { 'style': 'min-width: 400px;' }, [ + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'Username'), + E('input', { + 'type': 'text', + 'id': 'new-username', + 'placeholder': 'johndoe', + 'style': 'width: 100%; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'Password'), + E('input', { + 'type': 'password', + 'id': 'new-password', + 'placeholder': 'Leave empty to auto-generate', + 'style': 'width: 100%; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'Services'), + E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 12px;' }, [ + E('label', { 'style': 'display: flex; align-items: center; gap: 6px;' }, [ + E('input', { 'type': 'checkbox', 'id': 'svc-nextcloud', 'checked': true }), 'Nextcloud' + ]), + E('label', { 'style': 'display: flex; align-items: center; gap: 6px;' }, [ + E('input', { 'type': 'checkbox', 'id': 'svc-peertube', 'checked': true }), 'PeerTube' + ]), + E('label', { 'style': 'display: flex; align-items: center; gap: 6px;' }, [ + E('input', { 'type': 'checkbox', 'id': 'svc-jabber', 'checked': true }), 'Jabber' + ]), + E('label', { 'style': 'display: flex; align-items: center; gap: 6px;' }, [ + E('input', { 'type': 'checkbox', 'id': 'svc-matrix', 'checked': true }), 'Matrix' + ]), + E('label', { 'style': 'display: flex; align-items: center; gap: 6px;' }, [ + E('input', { 'type': 'checkbox', 'id': 'svc-email', 'checked': true }), 'Email' + ]), + E('label', { 'style': 'display: flex; align-items: center; gap: 6px;' }, [ + E('input', { 'type': 'checkbox', 'id': 'svc-gitea', 'checked': true }), 'Gitea' + ]) + ]) + ]), + E('div', { 'style': 'display: flex; gap: 12px; justify-content: flex-end;' }, [ + E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'click': function() { self.doAddUser(); } + }, '\u2714 Create User') + ]) + ]) + ]); + }, + + doAddUser: function() { + var username = document.getElementById('new-username').value.trim(); + var password = document.getElementById('new-password').value; + + if (!username) { + ui.addNotification(null, E('p', _('Username required')), 'warning'); + return; + } + + var services = []; + ['nextcloud', 'peertube', 'jabber', 'matrix', 'email', 'gitea'].forEach(function(s) { + if (document.getElementById('svc-' + s).checked) services.push(s); + }); + + ui.hideModal(); + + callAddUser(username, password, services.join(',')).then(function(res) { + if (res.success) { + var msg = 'User ' + username + ' created'; + if (res.password) msg += '. Password: ' + res.password; + ui.addNotification(null, E('p', msg), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', res.error || 'Failed'), 'error'); + } + }); + }, + + showPasswdModal: function(username) { + var self = this; + + ui.showModal(_('Change Password'), [ + E('div', { 'style': 'min-width: 350px;' }, [ + E('p', {}, 'Change password for: ' + E('strong', {}, username)), + E('div', { 'style': 'margin: 16px 0;' }, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'New Password'), + E('input', { + 'type': 'password', + 'id': 'change-password', + 'placeholder': 'Leave empty to auto-generate', + 'style': 'width: 100%; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + E('div', { 'style': 'display: flex; gap: 12px; justify-content: flex-end;' }, [ + E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'), + E('button', { + 'class': 'kiss-btn kiss-btn-blue', + 'click': function() { self.doPasswd(username); } + }, '\uD83D\uDD11 Change') + ]) + ]) + ]); + }, + + doPasswd: function(username) { + var password = document.getElementById('change-password').value; + ui.hideModal(); + + callPasswd(username, password).then(function(res) { + if (res.success) { + var msg = 'Password changed for ' + username; + if (res.password) msg += '. New: ' + res.password; + ui.addNotification(null, E('p', msg), 'info'); + } else { + ui.addNotification(null, E('p', res.error || 'Failed'), 'error'); + } + }); + }, + + showDeleteModal: function(username) { + var self = this; + + ui.showModal(_('Delete User'), [ + E('div', { 'style': 'min-width: 300px;' }, [ + E('p', { 'style': 'color: var(--kiss-red);' }, + 'Delete user: ' + E('strong', {}, username) + '?'), + E('p', { 'style': 'color: var(--kiss-muted); font-size: 13px;' }, + 'This removes the user from all services.'), + E('div', { 'style': 'display: flex; gap: 12px; justify-content: flex-end; margin-top: 16px;' }, [ + E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'), + E('button', { + 'class': 'kiss-btn kiss-btn-red', + 'click': function() { self.doDelete(username); } + }, '\uD83D\uDDD1 Delete') + ]) + ]) + ]); + }, + + doDelete: function(username) { + ui.hideModal(); + + callDeleteUser(username).then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', 'User ' + username + ' deleted'), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', res.error || 'Failed'), 'error'); + } + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null }); diff --git a/package/secubox/luci-app-smtp-relay/htdocs/luci-static/resources/view/smtp-relay/settings.js b/package/secubox/luci-app-smtp-relay/htdocs/luci-static/resources/view/smtp-relay/settings.js index 17599c3d..d8197f29 100644 --- a/package/secubox/luci-app-smtp-relay/htdocs/luci-static/resources/view/smtp-relay/settings.js +++ b/package/secubox/luci-app-smtp-relay/htdocs/luci-static/resources/view/smtp-relay/settings.js @@ -1,9 +1,10 @@ 'use strict'; 'require view'; -'require form'; -'require ui'; -'require uci'; 'require rpc'; +'require ui'; +'require poll'; +'require uci'; +'require secubox/kiss-theme'; var callGetStatus = rpc.declare({ object: 'luci.smtp-relay', @@ -11,12 +12,6 @@ var callGetStatus = rpc.declare({ expect: {} }); -var callGetConfig = rpc.declare({ - object: 'luci.smtp-relay', - method: 'get_config', - expect: {} -}); - var callTestEmail = rpc.declare({ object: 'luci.smtp-relay', method: 'test_email', @@ -34,248 +29,319 @@ return view.extend({ load: function() { return Promise.all([ callGetStatus().catch(function() { return {}; }), - callGetConfig().catch(function() { return {}; }), - callDetectLocal().catch(function() { return {}; }), uci.load('smtp-relay') ]); }, - handleTestEmail: function(ev) { - var recipient = document.getElementById('test_recipient').value; - var btn = ev.target; - var resultDiv = document.getElementById('test_result'); + render: function(data) { + var self = this; + var status = data[0] || {}; - if (!recipient) { - recipient = uci.get('smtp-relay', 'recipients', 'admin') || ''; + // Start polling + poll.add(function() { + return callGetStatus().then(function(s) { + self.updateStats(s); + }); + }, 15); + + var modeLabels = { + 'external': 'External', + 'local': 'Local', + 'direct': 'Direct' + }; + + var content = [ + // Header + E('div', { 'style': 'margin-bottom: 24px;' }, [ + E('h2', { 'style': 'margin: 0 0 8px 0; display: flex; align-items: center; gap: 12px;' }, [ + E('span', {}, '\u2709'), + 'SMTP Relay' + ]), + E('p', { 'style': 'color: var(--kiss-muted); margin: 0;' }, + 'Centralized outbound email for all SecuBox services') + ]), + + // Stats Grid + E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-bottom: 20px;' }, [ + this.statCard('Status', status.enabled ? 'Active' : 'Disabled', + status.enabled ? 'var(--kiss-green)' : 'var(--kiss-red)', 'status'), + this.statCard('Mode', modeLabels[status.mode] || 'N/A', + status.mode === 'local' ? 'var(--kiss-green)' : 'var(--kiss-cyan)', 'mode'), + this.statCard('Server', status.server || 'None', 'var(--kiss-purple)', 'server'), + this.statCard('TLS', status.tls ? 'Enabled' : 'Off', + status.tls ? 'var(--kiss-green)' : 'var(--kiss-yellow)', 'tls') + ]), + + // Quick Test Card + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'class': 'kiss-card-title' }, ['\u26a1 Quick Actions']), + E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap; align-items: center;' }, [ + E('button', { + 'class': 'kiss-btn kiss-btn-blue', + 'click': ui.createHandlerFn(this, this.doDetectLocal) + }, '\ud83d\udd0d Detect Local'), + E('input', { + 'type': 'email', + 'id': 'test_recipient', + 'placeholder': 'test@example.com', + 'value': status.admin_recipient || '', + 'style': 'flex: 1; min-width: 200px; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }), + E('button', { + 'id': 'btn-test', + 'class': 'kiss-btn kiss-btn-green', + 'click': ui.createHandlerFn(this, this.doTestEmail) + }, '\u2709 Send Test'), + E('span', { 'id': 'test_result' }, '') + ]) + ]), + + // Configuration Card + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'class': 'kiss-card-title' }, ['\u2699 Configuration']), + E('div', { 'style': 'display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;' }, [ + // Mode + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'Relay Mode'), + E('select', { + 'id': 'cfg_mode', + 'style': 'width: 100%; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }, [ + E('option', { 'value': 'external', 'selected': status.mode === 'external' }, 'External SMTP'), + E('option', { 'value': 'local', 'selected': status.mode === 'local' }, 'Local Mailserver'), + E('option', { 'value': 'direct', 'selected': status.mode === 'direct' }, 'Direct Delivery') + ]) + ]), + // Server + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'SMTP Server'), + E('input', { + 'type': 'text', + 'id': 'cfg_server', + 'placeholder': 'smtp.gmail.com', + 'value': status.server || '', + 'style': 'width: 100%; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + // Port + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'Port'), + E('input', { + 'type': 'number', + 'id': 'cfg_port', + 'placeholder': '587', + 'value': status.port || 587, + 'style': 'width: 100%; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + // TLS + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'Use TLS'), + E('div', { 'style': 'display: flex; align-items: center; gap: 8px;' }, [ + E('input', { + 'type': 'checkbox', + 'id': 'cfg_tls', + 'checked': status.tls, + 'style': 'width: 18px; height: 18px;' + }), + E('span', { 'style': 'color: var(--kiss-muted);' }, 'Enable STARTTLS') + ]) + ]), + // Username + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'Username'), + E('input', { + 'type': 'text', + 'id': 'cfg_user', + 'placeholder': 'user@gmail.com', + 'value': uci.get('smtp-relay', 'main', 'username') || '', + 'style': 'width: 100%; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + // Password + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'Password'), + E('input', { + 'type': 'password', + 'id': 'cfg_pass', + 'placeholder': '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022', + 'value': '', + 'style': 'width: 100%; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + // From Address + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'From Address'), + E('input', { + 'type': 'email', + 'id': 'cfg_from', + 'placeholder': 'noreply@secubox.in', + 'value': status.from || '', + 'style': 'width: 100%; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]), + // Admin Recipient + E('div', {}, [ + E('div', { 'style': 'color: var(--kiss-muted); font-size: 11px; text-transform: uppercase; margin-bottom: 6px;' }, 'Admin Email'), + E('input', { + 'type': 'email', + 'id': 'cfg_admin', + 'placeholder': 'admin@example.com', + 'value': status.admin_recipient || '', + 'style': 'width: 100%; padding: 8px 12px; border: 1px solid var(--kiss-border); border-radius: 6px; background: var(--kiss-bg2); color: var(--kiss-text);' + }) + ]) + ]), + E('div', { 'style': 'margin-top: 20px;' }, [ + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'click': ui.createHandlerFn(this, this.doSaveConfig) + }, '\u2714 Save Configuration') + ]) + ]), + + // Quick Presets Card + E('div', { 'class': 'kiss-card kiss-panel-blue' }, [ + E('div', { 'class': 'kiss-card-title' }, ['\ud83d\udce7 Provider Presets']), + E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, [ + this.presetButton('Gmail', 'smtp.gmail.com', 587, true), + this.presetButton('SendGrid', 'smtp.sendgrid.net', 587, true), + this.presetButton('Mailgun', 'smtp.mailgun.org', 587, true), + this.presetButton('AWS SES', 'email-smtp.us-east-1.amazonaws.com', 587, true), + this.presetButton('Local', '192.168.255.30', 25, false) + ]), + E('p', { 'style': 'margin-top: 12px; color: var(--kiss-muted); font-size: 12px;' }, + 'Click a preset to auto-fill server settings.') + ]) + ]; + + return KissTheme.wrap(content, 'admin/secubox/system/smtp-relay'); + }, + + 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 modeEl = document.querySelector('[data-stat="mode"]'); + var serverEl = document.querySelector('[data-stat="server"]'); + var tlsEl = document.querySelector('[data-stat="tls"]'); + + if (statusEl) { + statusEl.textContent = status.enabled ? 'Active' : 'Disabled'; + statusEl.style.color = status.enabled ? 'var(--kiss-green)' : 'var(--kiss-red)'; } + if (modeEl) { + var labels = { 'external': 'External', 'local': 'Local', 'direct': 'Direct' }; + modeEl.textContent = labels[status.mode] || 'N/A'; + } + if (serverEl) { + serverEl.textContent = status.server || 'None'; + } + if (tlsEl) { + tlsEl.textContent = status.tls ? 'Enabled' : 'Off'; + tlsEl.style.color = status.tls ? 'var(--kiss-green)' : 'var(--kiss-yellow)'; + } + }, + + presetButton: function(name, server, port, tls) { + return E('button', { + 'class': 'kiss-btn', + 'click': function() { + document.getElementById('cfg_server').value = server; + document.getElementById('cfg_port').value = port; + document.getElementById('cfg_tls').checked = tls; + document.getElementById('cfg_mode').value = name === 'Local' ? 'local' : 'external'; + } + }, name); + }, + + doDetectLocal: function() { + ui.showModal(_('Detecting Local Mailserver'), [ + E('p', { 'class': 'spinning' }, _('Scanning...')) + ]); + + callDetectLocal().then(function(res) { + ui.hideModal(); + if (res.detected) { + document.getElementById('cfg_server').value = res.server || '192.168.255.30'; + document.getElementById('cfg_port').value = res.port || 25; + document.getElementById('cfg_mode').value = 'local'; + ui.addNotification(null, E('p', _('Local mailserver detected')), 'info'); + } else { + ui.addNotification(null, E('p', _('No local mailserver found')), 'warning'); + } + }); + }, + + doTestEmail: function() { + var recipient = document.getElementById('test_recipient').value; + var btn = document.getElementById('btn-test'); + var result = document.getElementById('test_result'); if (!recipient) { - ui.addNotification(null, E('p', _('Please enter a recipient email address')), 'warning'); + ui.addNotification(null, E('p', _('Enter a recipient email')), 'warning'); return; } btn.disabled = true; - btn.textContent = _('Sending...'); - resultDiv.innerHTML = ''; + btn.textContent = 'Sending...'; + result.innerHTML = ''; callTestEmail(recipient).then(function(res) { btn.disabled = false; - btn.textContent = _('Send Test Email'); + btn.textContent = '\u2709 Send Test'; if (res.success) { - resultDiv.innerHTML = '✓ ' + _('Test email sent successfully!') + ''; - ui.addNotification(null, E('p', _('Test email sent to %s').format(recipient)), 'info'); + result.innerHTML = 'Sent'; } else { - resultDiv.innerHTML = '✗ ' + (res.error || _('Failed to send')) + ''; - ui.addNotification(null, E('p', res.error || _('Failed to send test email')), 'error'); + result.innerHTML = 'Failed'; + ui.addNotification(null, E('p', res.error || 'Send failed'), 'error'); } }).catch(function(err) { btn.disabled = false; - btn.textContent = _('Send Test Email'); - resultDiv.innerHTML = '✗ ' + err.message + ''; + btn.textContent = '\u2709 Send Test'; + result.innerHTML = 'Error'; }); }, - render: function(data) { - var status = data[0] || {}; - var config = data[1] || {}; - var localDetect = data[2] || {}; - var m, s, o; + doSaveConfig: function() { + var mode = document.getElementById('cfg_mode').value; + var server = document.getElementById('cfg_server').value; + var port = document.getElementById('cfg_port').value; + var user = document.getElementById('cfg_user').value; + var pass = document.getElementById('cfg_pass').value; + var tls = document.getElementById('cfg_tls').checked; + var from = document.getElementById('cfg_from').value; + var admin = document.getElementById('cfg_admin').value; - m = new form.Map('smtp-relay', _('SMTP Relay'), - _('Centralized outbound email configuration for all SecuBox services. Configure once, use everywhere.')); + uci.set('smtp-relay', 'main', 'mode', mode); + uci.set('smtp-relay', 'main', 'server', server); + uci.set('smtp-relay', 'main', 'port', port); + if (user) uci.set('smtp-relay', 'main', 'username', user); + if (pass) uci.set('smtp-relay', 'main', 'password', pass); + uci.set('smtp-relay', 'main', 'tls', tls ? '1' : '0'); + if (from) uci.set('smtp-relay', 'main', 'from', from); + uci.set('smtp-relay', 'recipients', 'admin', admin); - // Status section - s = m.section(form.NamedSection, 'main', 'smtp_relay', _('Status')); - s.anonymous = true; - - o = s.option(form.DummyValue, '_status', _('Current Status')); - o.rawhtml = true; - o.cfgvalue = function() { - var modeText = { - 'external': _('External SMTP Server'), - 'local': _('Local Mailserver'), - 'direct': _('Direct Delivery') - }; - var statusHtml = '