feat(nextcloud): Add user management and password reset

- Add list_users RPCD method to list Nextcloud users via OCC
- Add reset_password RPCD method for password reset via OCC
- Add Users tab in LuCI dashboard with user list
- Add password reset modal with confirmation
- Parse Nextcloud user:displayname JSON format

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-16 15:10:35 +01:00
parent 31aea08b0c
commit 4bd0c09b2e
2 changed files with 151 additions and 9 deletions

View File

@ -91,6 +91,19 @@ var callLogs = rpc.declare({
expect: {}
});
var callListUsers = rpc.declare({
object: 'luci.nextcloud',
method: 'list_users',
expect: { users: [] }
});
var callResetPassword = rpc.declare({
object: 'luci.nextcloud',
method: 'reset_password',
params: ['uid', 'password'],
expect: {}
});
// ============================================================================
// Helpers
// ============================================================================
@ -120,13 +133,15 @@ return view.extend({
status: {},
config: {},
backups: [],
users: [],
currentTab: 'overview',
load: function() {
return Promise.all([
callStatus(),
callGetConfig(),
callListBackups().catch(function() { return { backups: [] }; })
callListBackups().catch(function() { return { backups: [] }; }),
callListUsers().catch(function() { return { users: [] }; })
]);
},
@ -135,6 +150,7 @@ return view.extend({
this.status = data[0] || {};
this.config = data[1] || {};
this.backups = (data[2] || {}).backups || [];
this.users = (data[3] || {}).users || [];
// Not installed - show install view
if (!this.status.installed) {
@ -144,6 +160,7 @@ return view.extend({
// Tab navigation
var tabs = [
{ id: 'overview', label: 'Overview', icon: '🎛️' },
{ id: 'users', label: 'Users', icon: '👥' },
{ id: 'backups', label: 'Backups', icon: '💾' },
{ id: 'ssl', label: 'SSL', icon: '🔒' },
{ id: 'logs', label: 'Logs', icon: '📜' }
@ -197,6 +214,7 @@ return view.extend({
renderTabContent: function() {
switch (this.currentTab) {
case 'users': return this.renderUsersTab();
case 'backups': return this.renderBackupsTab();
case 'ssl': return this.renderSSLTab();
case 'logs': return this.renderLogsTab();
@ -343,6 +361,125 @@ return view.extend({
]);
},
// ========================================================================
// Users Tab
// ========================================================================
renderUsersTab: function() {
var self = this;
return E('div', {}, [
// Users List
KissTheme.card([
E('span', {}, '👥 Nextcloud Users'),
E('span', { 'style': 'margin-left:auto;font-size:12px;color:var(--kiss-muted);' }, this.users.length + ' users')
], E('div', { 'id': 'users-list' }, this.renderUsersList()))
]);
},
renderUsersList: function() {
var self = this;
if (!this.users.length) {
return E('div', { 'style': 'text-align:center;padding:40px;color:var(--kiss-muted);' }, [
E('div', { 'style': 'font-size:48px;margin-bottom:12px;' }, '👤'),
E('div', { 'style': 'font-size:16px;' }, 'No users found'),
E('div', { 'style': 'font-size:12px;margin-top:8px;' }, 'Container may not be running')
]);
}
return E('table', { 'class': 'kiss-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, 'User ID'),
E('th', {}, 'Display Name'),
E('th', { 'style': 'width:120px;' }, 'Actions')
])),
E('tbody', {}, this.users.map(function(u) {
return E('tr', {}, [
E('td', { 'style': 'font-family:monospace;' }, u.uid || u),
E('td', {}, u.displayname || '-'),
E('td', {}, [
E('button', {
'class': 'kiss-btn',
'style': 'padding:4px 10px;font-size:11px;',
'title': 'Reset Password',
'data-uid': u.uid || u,
'click': function(ev) { self.showResetPasswordModal(ev.currentTarget.dataset.uid); }
}, '🔑')
])
]);
}))
]);
},
showResetPasswordModal: function(uid) {
var self = this;
var passwordInput, confirmInput;
ui.showModal('Reset Password - ' + uid, [
E('div', { 'style': 'padding:16px;' }, [
E('p', { 'style': 'margin-bottom:16px;color:var(--kiss-muted);' },
'Enter new password for user: ' + uid),
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;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;color:var(--kiss-text);'
})
]),
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;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;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.handleResetPassword(uid, password);
}
}, 'Reset Password')
])
])
]);
},
handleResetPassword: function(uid, password) {
var self = this;
ui.showModal('Resetting Password', [
E('p', { 'class': 'spinning' }, 'Resetting password for ' + uid + '...')
]);
callResetPassword(uid, password).then(function(r) {
ui.hideModal();
if (r.success) {
ui.addNotification(null, E('p', 'Password reset for ' + uid), 'info');
} else {
ui.addNotification(null, E('p', 'Failed: ' + (r.error || 'Unknown error')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', 'Error: ' + e.message), 'error');
});
},
// ========================================================================
// Backups Tab
// ========================================================================
@ -701,10 +838,12 @@ return view.extend({
var self = this;
return Promise.all([
callStatus(),
callListBackups().catch(function() { return { backups: [] }; })
callListBackups().catch(function() { return { backups: [] }; }),
callListUsers().catch(function() { return { users: [] }; })
]).then(function(data) {
self.status = data[0] || {};
self.backups = (data[1] || {}).backups || [];
self.users = (data[2] || {}).users || [];
// Update tab content
var tabContent = document.getElementById('tab-content');

View File

@ -327,14 +327,17 @@ list_users() {
return
fi
# Convert from {uid: displayname} to [{uid: x, displayname: y}]
local users_array="[]"
users_array=$(echo "$users_json" | jsonfilter -e '@' 2>/dev/null | \
awk -F: '{gsub(/[{}"]/,"",$1); gsub(/[{}"]/,"",$2); if($1!="") printf "{\"uid\":\"%s\",\"displayname\":\"%s\"},", $1, $2}' | \
sed 's/,$//' | sed 's/^/[/;s/$/]/')
# Convert from {"uid":"displayname",...} to [{"uid":"x","displayname":"y"},...]
# Use sed to transform the JSON
local users_array
users_array=$(echo "$users_json" | sed 's/^{//;s/}$//' | tr ',' '\n' | while read -r line; do
uid=$(echo "$line" | cut -d'"' -f2)
displayname=$(echo "$line" | cut -d'"' -f4)
[ -n "$uid" ] && printf '{"uid":"%s","displayname":"%s"},' "$uid" "$displayname"
done | sed 's/,$//')
[ -z "$users_array" ] && users_array="[]"
echo "{\"users\": $users_array}"
[ -z "$users_array" ] && users_array=""
echo "{\"users\": [$users_array]}"
}
# Reset user password