secubox-openwrt/package/secubox/luci-app-jabber/htdocs/luci-static/resources/view/jabber/overview.js
CyberMind-FR b0d208777d feat(jabber): Add Jabber/XMPP server packages (Prosody)
New packages:
- secubox-app-jabber: LXC-based Prosody XMPP server with:
  - Debian Bookworm container
  - Full XMPP support (C2S, S2S, MUC, MAM)
  - HTTP upload for file sharing
  - BOSH and WebSocket for web clients
  - SSL/TLS encryption
  - User and room management via jabberctl

- luci-app-jabber: LuCI dashboard with:
  - Status overview and service controls
  - User management (add/delete)
  - Emancipate workflow (HAProxy + SSL + DNS)
  - Connection info display
  - Log viewer

CLI commands:
  jabberctl install/uninstall/start/stop/status
  jabberctl user add/del/passwd/list
  jabberctl room create/delete/list
  jabberctl emancipate <domain>

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-19 07:48:09 +01:00

421 lines
12 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require poll';
'require ui';
'require uci';
'require form';
'require jabber.api as api';
return view.extend({
handleAction: function(action, args) {
var self = this;
var btn = document.activeElement;
ui.showModal(_('Please wait...'), [
E('p', { 'class': 'spinning' }, _('Processing request...'))
]);
var promise;
switch(action) {
case 'start':
promise = api.start();
break;
case 'stop':
promise = api.stop();
break;
case 'install':
promise = api.install();
break;
case 'uninstall':
if (!confirm(_('This will remove the Jabber container. User data will be preserved. Continue?')))
return ui.hideModal();
promise = api.uninstall();
break;
case 'update':
promise = api.update();
break;
case 'configure_haproxy':
promise = api.configureHaproxy();
break;
case 'emancipate':
var domain = args;
if (!domain) {
ui.hideModal();
ui.addNotification(null, E('p', _('Domain is required')), 'error');
return;
}
promise = api.emancipate(domain);
break;
case 'user_add':
var jid = args.jid;
var password = args.password;
if (!jid) {
ui.hideModal();
ui.addNotification(null, E('p', _('JID is required')), 'error');
return;
}
promise = api.userAdd(jid, password);
break;
case 'user_del':
if (!confirm(_('Delete user ') + args + '?'))
return ui.hideModal();
promise = api.userDel(args);
break;
default:
ui.hideModal();
return;
}
promise.then(function(res) {
ui.hideModal();
if (res && res.success) {
var msg = res.message || _('Action completed');
if (res.password) {
msg += '\n' + _('Password: ') + res.password;
}
ui.addNotification(null, E('p', { 'style': 'white-space: pre-wrap;' }, msg), 'success');
self.load().then(function(data) {
dom.content(document.querySelector('#jabber-content'), self.renderContent(data));
});
} else {
ui.addNotification(null, E('p', res.error || _('Action failed')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
});
},
load: function() {
return Promise.all([
api.status(),
api.userList(),
uci.load('jabber')
]);
},
renderInstallWizard: function() {
var self = this;
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Jabber/XMPP Server')),
E('p', {}, _('Prosody is a modern XMPP server written in Lua. It aims to be easy to set up and configure, and efficient with system resources.')),
E('div', { 'class': 'cbi-value' }, [
E('h4', {}, _('Features')),
E('ul', {}, [
E('li', {}, _('Secure messaging with end-to-end encryption (OMEMO)')),
E('li', {}, _('Multi-user chat rooms (MUC)')),
E('li', {}, _('File sharing with HTTP upload')),
E('li', {}, _('Server-to-server federation (S2S)')),
E('li', {}, _('BOSH and WebSocket for web clients')),
E('li', {}, _('Message archiving (MAM)'))
])
]),
E('div', { 'class': 'cbi-value' }, [
E('h4', {}, _('Compatible Clients')),
E('ul', {}, [
E('li', {}, _('Conversations (Android)')),
E('li', {}, _('Monal (iOS/macOS)')),
E('li', {}, _('Gajim (Windows/Linux)')),
E('li', {}, _('Dino (Linux)')),
E('li', {}, _('Converse.js (Web)'))
])
]),
E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'btn cbi-button cbi-button-positive',
'click': function() { self.handleAction('install'); }
}, _('Install Jabber/XMPP'))
])
]);
},
renderStatusBadge: function(running) {
var color = running === 'true' ? '#4CAF50' : '#f44336';
var text = running === 'true' ? _('Running') : _('Stopped');
return E('span', {
'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + color
}, text);
},
renderContent: function(data) {
var self = this;
var status = data[0] || {};
var userListData = data[1] || {};
if (status.container_state === 'not_installed') {
return this.renderInstallWizard();
}
var running = status.running === 'true';
var haproxyConfigured = status.haproxy === '1';
var domain = status.domain || '';
var hostname = status.hostname || 'jabber.local';
// Parse user list
var users = [];
if (userListData.users) {
users = userListData.users.split(',').filter(function(u) { return u.length > 0; });
}
var accessUrl = '';
if (running) {
if (domain && haproxyConfigured) {
accessUrl = 'https://' + domain;
} else {
accessUrl = 'http://192.168.255.1:' + (status.http_port || '5280');
}
}
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Jabber/XMPP Server (Prosody)')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Status')),
E('div', { 'class': 'cbi-value-field' }, this.renderStatusBadge(status.running))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Hostname')),
E('div', { 'class': 'cbi-value-field' }, hostname)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('C2S Port')),
E('div', { 'class': 'cbi-value-field' }, status.c2s_port || '5222')
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('S2S Port')),
E('div', { 'class': 'cbi-value-field' }, [
status.s2s_port || '5269',
' ',
E('span', {
'style': 'display:inline-block;padding:2px 8px;border-radius:3px;color:#fff;background:' + (status.s2s_enabled === '1' ? '#4CAF50' : '#9e9e9e')
}, status.s2s_enabled === '1' ? _('Federation ON') : _('Federation OFF'))
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('HTTP/BOSH Port')),
E('div', { 'class': 'cbi-value-field' }, status.http_port || '5280')
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Users')),
E('div', { 'class': 'cbi-value-field' }, status.user_count || '0')
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('MUC (Chat Rooms)')),
E('div', { 'class': 'cbi-value-field' }, [
E('span', {
'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + (status.muc_enabled === '1' ? '#4CAF50' : '#9e9e9e')
}, status.muc_enabled === '1' ? _('Enabled') : _('Disabled'))
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('HAProxy')),
E('div', { 'class': 'cbi-value-field' }, [
E('span', {
'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + (haproxyConfigured ? '#4CAF50' : '#9e9e9e')
}, haproxyConfigured ? _('Configured') : _('Not configured')),
' ',
!haproxyConfigured ? E('button', {
'class': 'btn cbi-button',
'click': function() { self.handleAction('configure_haproxy'); }
}, _('Configure')) : ''
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Domain')),
E('div', { 'class': 'cbi-value-field' }, domain || _('Not configured'))
]),
E('hr'),
E('h4', {}, _('Service Controls')),
E('div', { 'class': 'cbi-page-actions', 'style': 'margin-bottom: 20px;' }, [
running ?
E('button', {
'class': 'btn cbi-button cbi-button-negative',
'click': function() { self.handleAction('stop'); }
}, _('Stop')) :
E('button', {
'class': 'btn cbi-button cbi-button-positive',
'click': function() { self.handleAction('start'); }
}, _('Start')),
' ',
E('button', {
'class': 'btn cbi-button',
'click': function() { self.handleAction('update'); }
}, _('Update')),
' ',
E('button', {
'class': 'btn cbi-button cbi-button-negative',
'click': function() { self.handleAction('uninstall'); }
}, _('Uninstall'))
]),
E('hr'),
E('h4', {}, _('User Management')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('New User')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'new-user-jid',
'class': 'cbi-input-text',
'placeholder': 'user@' + hostname,
'style': 'width: 200px;'
}),
' ',
E('input', {
'type': 'password',
'id': 'new-user-password',
'class': 'cbi-input-text',
'placeholder': _('Password (auto-generate if empty)'),
'style': 'width: 200px;'
}),
' ',
E('button', {
'class': 'btn cbi-button cbi-button-positive',
'click': function() {
var jid = document.getElementById('new-user-jid').value;
var password = document.getElementById('new-user-password').value;
self.handleAction('user_add', { jid: jid, password: password });
}
}, _('Add User'))
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Registered Users')),
E('div', { 'class': 'cbi-value-field' }, [
users.length > 0 ?
E('table', { 'class': 'table', 'style': 'width: auto;' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('JID')),
E('th', { 'class': 'th' }, _('Actions'))
])
].concat(users.map(function(user) {
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, user),
E('td', { 'class': 'td' }, [
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'click': function() { self.handleAction('user_del', user); }
}, _('Delete'))
])
]);
}))) :
E('em', {}, _('No users registered'))
])
]),
E('hr'),
E('h4', {}, _('Emancipate (Public Exposure)')),
E('p', {}, _('Make Jabber publicly accessible with SSL certificate, DNS records, and S2S federation.')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Domain')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'emancipate-domain',
'class': 'cbi-input-text',
'placeholder': 'xmpp.example.com',
'value': domain
})
])
]),
E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
var domainInput = document.getElementById('emancipate-domain');
self.handleAction('emancipate', domainInput.value);
}
}, _('Emancipate'))
]),
E('p', { 'style': 'font-size: 12px; color: #666;' }, [
_('DNS records needed: A record for domain, SRV records for _xmpp-client._tcp and _xmpp-server._tcp')
]),
E('hr'),
E('h4', {}, _('Connection Info')),
E('div', { 'style': 'background: #f5f5f5; padding: 15px; border-radius: 4px; font-family: monospace;' }, [
E('p', {}, [
E('strong', {}, _('XMPP Server: ')),
hostname + ':' + (status.c2s_port || '5222')
]),
E('p', {}, [
E('strong', {}, _('BOSH URL: ')),
accessUrl + '/http-bind'
]),
E('p', {}, [
E('strong', {}, _('WebSocket: ')),
(domain && haproxyConfigured ? 'wss://' + domain : 'ws://192.168.255.1:' + (status.http_port || '5280')) + '/xmpp-websocket'
]),
E('p', {}, [
E('strong', {}, _('Admin JID: ')),
(status.admin_user || 'admin') + '@' + hostname
])
]),
E('hr'),
E('h4', {}, _('Logs')),
E('div', { 'id': 'jabber-logs' }, [
E('pre', {
'style': 'background:#1e1e1e;color:#d4d4d4;padding:10px;max-height:300px;overflow:auto;font-size:12px;border-radius:4px;'
}, _('Loading logs...'))
]),
E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'btn cbi-button',
'click': function() {
api.logs(100).then(function(res) {
var logsEl = document.querySelector('#jabber-logs pre');
if (logsEl) {
logsEl.textContent = res.logs || _('No logs available');
}
});
}
}, _('Refresh Logs'))
])
]);
},
render: function(data) {
var self = this;
var content = E('div', { 'id': 'jabber-content' }, this.renderContent(data));
// Load logs initially
api.logs(50).then(function(res) {
var logsEl = document.querySelector('#jabber-logs pre');
if (logsEl) {
logsEl.textContent = res.logs || _('No logs available');
}
});
// Poll for status updates
poll.add(function() {
return api.status().then(function(status) {
// Update status badge if needed
});
}, 10);
return content;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});