Add secubox-app-gotosocial and luci-app-gotosocial for running a lightweight ActivityPub social network server in LXC container. Features: - gotosocialctl CLI with install, start, stop, user management - LXC container deployment (ARM64) - HAProxy integration via emancipate command - UCI configuration for instance, container, proxy, federation settings - LuCI web interface with overview, users, and settings tabs - Mesh integration support for auto-federation between SecuBox nodes - Backup/restore functionality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
259 lines
7.5 KiB
JavaScript
259 lines
7.5 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require ui';
|
|
'require poll';
|
|
|
|
var callUsers = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'users',
|
|
expect: {}
|
|
});
|
|
|
|
var callCreateUser = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'create_user',
|
|
params: ['username', 'email', 'password', 'admin'],
|
|
expect: {}
|
|
});
|
|
|
|
var callDeleteUser = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'delete_user',
|
|
params: ['username'],
|
|
expect: {}
|
|
});
|
|
|
|
var callPromoteUser = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'promote_user',
|
|
params: ['username'],
|
|
expect: {}
|
|
});
|
|
|
|
var callDemoteUser = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'demote_user',
|
|
params: ['username'],
|
|
expect: {}
|
|
});
|
|
|
|
return view.extend({
|
|
users: [],
|
|
|
|
load: function() {
|
|
return callUsers();
|
|
},
|
|
|
|
pollUsers: function() {
|
|
return callUsers().then(L.bind(function(res) {
|
|
this.users = res.users || [];
|
|
this.updateUserTable();
|
|
}, this));
|
|
},
|
|
|
|
updateUserTable: function() {
|
|
var tbody = document.getElementById('users-tbody');
|
|
if (!tbody) return;
|
|
|
|
while (tbody.firstChild) {
|
|
tbody.removeChild(tbody.firstChild);
|
|
}
|
|
|
|
if (this.users.length === 0) {
|
|
tbody.appendChild(E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td', 'colspan': '5', 'style': 'text-align:center' }, _('No users found'))
|
|
]));
|
|
return;
|
|
}
|
|
|
|
this.users.forEach(L.bind(function(user) {
|
|
var row = E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, user.username || '-'),
|
|
E('td', { 'class': 'td' }, user.email || '-'),
|
|
E('td', { 'class': 'td' }, user.admin ?
|
|
E('span', { 'class': 'badge success' }, _('Admin')) :
|
|
E('span', { 'class': 'badge' }, _('User'))
|
|
),
|
|
E('td', { 'class': 'td' }, user.confirmed ?
|
|
E('span', { 'class': 'badge success' }, _('Confirmed')) :
|
|
E('span', { 'class': 'badge warning' }, _('Pending'))
|
|
),
|
|
E('td', { 'class': 'td' }, [
|
|
user.admin ?
|
|
E('button', {
|
|
'class': 'btn cbi-button-neutral',
|
|
'click': ui.createHandlerFn(this, this.handleDemote, user.username)
|
|
}, _('Demote')) :
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': ui.createHandlerFn(this, this.handlePromote, user.username)
|
|
}, _('Promote')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-negative',
|
|
'click': ui.createHandlerFn(this, this.handleDelete, user.username)
|
|
}, _('Delete'))
|
|
])
|
|
]);
|
|
tbody.appendChild(row);
|
|
}, this));
|
|
},
|
|
|
|
handleCreate: function() {
|
|
return ui.showModal(_('Create User'), [
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Username')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', { 'type': 'text', 'id': 'new-username', 'class': 'cbi-input-text', 'placeholder': 'johndoe' })
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Email')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', { 'type': 'email', 'id': 'new-email', 'class': 'cbi-input-text', 'placeholder': 'john@example.com' })
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Password')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', { 'type': 'password', 'id': 'new-password', 'class': 'cbi-input-text' })
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Admin')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', { 'type': 'checkbox', 'id': 'new-admin' }),
|
|
' ', _('Grant admin privileges')
|
|
])
|
|
]),
|
|
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 username = document.getElementById('new-username').value;
|
|
var email = document.getElementById('new-email').value;
|
|
var password = document.getElementById('new-password').value;
|
|
var admin = document.getElementById('new-admin').checked;
|
|
|
|
if (!username || !email || !password) {
|
|
ui.addNotification(null, E('p', _('All fields are required')), 'error');
|
|
return;
|
|
}
|
|
|
|
ui.hideModal();
|
|
ui.showModal(_('Creating User...'), [
|
|
E('p', { 'class': 'spinning' }, _('Creating user...'))
|
|
]);
|
|
|
|
return callCreateUser(username, email, password, admin).then(L.bind(function(res) {
|
|
ui.hideModal();
|
|
if (res.success) {
|
|
ui.addNotification(null, E('p', res.message || _('User created successfully')), 'success');
|
|
return this.pollUsers();
|
|
} else {
|
|
ui.addNotification(null, E('p', res.error || _('Failed to create user')), 'error');
|
|
}
|
|
}, this));
|
|
})
|
|
}, _('Create'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleDelete: function(username) {
|
|
return ui.showModal(_('Delete User'), [
|
|
E('p', _('Are you sure you want to delete user "%s"?').format(username)),
|
|
E('p', { 'class': 'alert-message warning' }, _('This action cannot be undone. All posts and data will be lost.')),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-negative',
|
|
'click': ui.createHandlerFn(this, function() {
|
|
ui.hideModal();
|
|
return callDeleteUser(username).then(L.bind(function(res) {
|
|
if (res.success) {
|
|
ui.addNotification(null, E('p', res.message || _('User deleted')), 'success');
|
|
return this.pollUsers();
|
|
} else {
|
|
ui.addNotification(null, E('p', res.error || _('Failed to delete user')), 'error');
|
|
}
|
|
}, this));
|
|
})
|
|
}, _('Delete'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
handlePromote: function(username) {
|
|
return callPromoteUser(username).then(L.bind(function(res) {
|
|
if (res.success) {
|
|
ui.addNotification(null, E('p', res.message || _('User promoted')), 'success');
|
|
return this.pollUsers();
|
|
} else {
|
|
ui.addNotification(null, E('p', res.error || _('Failed to promote user')), 'error');
|
|
}
|
|
}, this));
|
|
},
|
|
|
|
handleDemote: function(username) {
|
|
return callDemoteUser(username).then(L.bind(function(res) {
|
|
if (res.success) {
|
|
ui.addNotification(null, E('p', res.message || _('User demoted')), 'success');
|
|
return this.pollUsers();
|
|
} else {
|
|
ui.addNotification(null, E('p', res.error || _('Failed to demote user')), 'error');
|
|
}
|
|
}, this));
|
|
},
|
|
|
|
render: function(data) {
|
|
this.users = data.users || [];
|
|
|
|
var view = E('div', { 'class': 'cbi-map' }, [
|
|
E('h2', _('GoToSocial Users')),
|
|
E('div', { 'class': 'cbi-map-descr' }, _('Manage user accounts for your Fediverse instance.')),
|
|
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('div', { 'style': 'margin-bottom:10px' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': ui.createHandlerFn(this, this.handleCreate)
|
|
}, _('Create User'))
|
|
]),
|
|
|
|
E('table', { 'class': 'table' }, [
|
|
E('thead', {}, [
|
|
E('tr', { 'class': 'tr' }, [
|
|
E('th', { 'class': 'th' }, _('Username')),
|
|
E('th', { 'class': 'th' }, _('Email')),
|
|
E('th', { 'class': 'th' }, _('Role')),
|
|
E('th', { 'class': 'th' }, _('Status')),
|
|
E('th', { 'class': 'th' }, _('Actions'))
|
|
])
|
|
]),
|
|
E('tbody', { 'id': 'users-tbody' })
|
|
])
|
|
]),
|
|
|
|
E('style', {}, `
|
|
.badge { padding: 2px 8px; border-radius: 3px; background: #666; color: white; }
|
|
.badge.success { background: #4CAF50; }
|
|
.badge.warning { background: #FF9800; }
|
|
`)
|
|
]);
|
|
|
|
this.updateUserTable();
|
|
poll.add(L.bind(this.pollUsers, this), 10);
|
|
|
|
return view;
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|