feat(admin): show auth source badge and hide password reset for OIDC users

The admin user list now displays an OIDC/Local badge per user and
hides the password reset button for OIDC-provisioned accounts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jan Wiebe
2026-02-21 20:28:09 +01:00
parent 0db9c3cd88
commit 3e0813b480
3 changed files with 14 additions and 6 deletions
+2 -1
View File
@@ -92,13 +92,14 @@
<tr>
<th>User</th>
<th>Role</th>
<th>Auth</th>
<th>Status</th>
<th>Storage</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="users-tbody"><tr><td colspan="6" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> Loading users…</td></tr></tbody>
<tbody id="users-tbody"><tr><td colspan="7" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> Loading users…</td></tr></tbody>
</table>
</div>
<div class="pagination">
+2
View File
@@ -134,6 +134,7 @@ tr:hover{background:#fafbfd}
.badge-active{background:#d1fae5;color:#065f46}
.badge-inactive{background:#fee2e2;color:#991b1b}
.badge-oidc{background:#ede9fe;color:#6d28d9}
.badge-local{background:#f1f5f9;color:#64748b}
.badge-env{background:#fef3c7;color:#92400e;font-size:10px;padding:1px 6px;margin-left:4px}
/* ── Buttons ── */
@@ -257,6 +258,7 @@ details[open] summary{margin-bottom:14px;color:#ff5e3a}
[data-theme="dark"] .badge-active{background:#052e16;color:#86efac}
[data-theme="dark"] .badge-inactive{background:#3b1111;color:#fca5a5}
[data-theme="dark"] .badge-oidc{background:#2e1065;color:#c4b5fd}
[data-theme="dark"] .badge-local{background:#334155;color:#94a3b8}
[data-theme="dark"] .btn-secondary{background:#1e293b;color:#e2e8f0;border-color:#334155}
[data-theme="dark"] .btn-secondary:hover{background:#334155;border-color:#475569}
[data-theme="dark"] .btn-danger{background:#3b1111;color:#fca5a5;border-color:#991b1b}
+10 -5
View File
@@ -93,29 +93,34 @@ async function loadDashboard() {
async function loadUsers() {
const tbody = document.getElementById('users-tbody');
tbody.innerHTML = '<tr><td colspan="6" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> Loading…</td></tr>';
tbody.innerHTML = '<tr><td colspan="7" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> Loading…</td></tr>';
try {
const resp = await fetch(API + '/admin/users?limit=' + PAGE_SIZE + '&offset=' + (usersPage * PAGE_SIZE), { headers: headers() });
if (!resp.ok) { tbody.innerHTML = '<tr><td colspan="6" class="table-status-error"><i class="fas fa-exclamation-circle"></i> Failed to load users</td></tr>'; return; }
if (!resp.ok) { tbody.innerHTML = '<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> Failed to load users</td></tr>'; return; }
const data = await resp.json();
totalUsers = data.total;
const users = data.users;
if (users.length === 0) { tbody.innerHTML = '<tr><td colspan="6" class="table-status-empty">No users found</td></tr>'; return; }
if (users.length === 0) { tbody.innerHTML = '<tr><td colspan="7" class="table-status-empty">No users found</td></tr>'; return; }
tbody.innerHTML = users.map(u => {
const quotaPct = u.storage_quota_bytes > 0 ? ((u.storage_used_bytes / u.storage_quota_bytes) * 100) : 0;
const quotaColor = quotaPct > 90 ? 'red' : quotaPct > 70 ? 'orange' : 'green';
const quotaText = u.storage_quota_bytes > 0 ? formatBytes(u.storage_used_bytes) + ' / ' + formatBytes(u.storage_quota_bytes) : formatBytes(u.storage_used_bytes) + ' / ∞';
const isSelf = u.id === currentAdminId;
const isOidc = u.auth_provider && u.auth_provider !== 'local';
const authBadge = isOidc
? '<span class="badge badge-oidc" title="Authenticated via ' + u.auth_provider + '"><i class="fas fa-key badge-admin-icon-small"></i> ' + u.auth_provider + '</span>'
: '<span class="badge badge-local">Local</span>';
return '<tr>' +
'<td><div class="user-info"><span class="user-name">' + u.username + (isSelf ? ' <span class="user-self-badge">(you)</span>' : '') + '</span><span class="user-email">' + u.email + '</span></div></td>' +
'<td><span class="badge badge-' + u.role + '">' + (u.role === 'admin' ? '<i class="fas fa-shield-alt badge-admin-icon-small"></i> ' : '') + u.role + '</span></td>' +
'<td>' + authBadge + '</td>' +
'<td><span class="badge badge-' + (u.active ? 'active' : 'inactive') + '">' + (u.active ? 'Active' : 'Inactive') + '</span></td>' +
'<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' + quotaColor + '" style="width:' + Math.min(quotaPct, 100) + '%"></div></div><span class="quota-text">' + quotaText + '</span></div></td>' +
'<td class="user-last-login-cell">' + timeAgo(u.last_login_at) + '</td>' +
'<td><div class="actions-row">' +
'<button class="btn btn-sm btn-secondary" onclick="openQuotaModal(\'' + u.id + '\',\'' + u.username + '\',' + u.storage_quota_bytes + ')" title="Edit quota"><i class="fas fa-box"></i></button>' +
'<button class="btn btn-sm btn-secondary" onclick="openResetPasswordModal(\'' + u.id + '\',\'' + u.username + '\')" title="Reset password"><i class="fas fa-key"></i></button>' +
(isOidc ? '' : '<button class="btn btn-sm btn-secondary" onclick="openResetPasswordModal(\'' + u.id + '\',\'' + u.username + '\')" title="Reset password"><i class="fas fa-key"></i></button>') +
'<button class="btn btn-sm btn-secondary" onclick="toggleRole(\'' + u.id + '\',\'' + u.role + '\')" title="Toggle role"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-' + (u.role === 'admin' ? 'user' : 'crown') + '"></i></button>' +
'<button class="btn btn-sm ' + (u.active ? 'btn-danger' : 'btn-success') + '" onclick="toggleActive(\'' + u.id + '\',' + u.active + ')" title="' + (u.active ? 'Deactivate' : 'Activate') + '"' + (isSelf && u.active ? ' disabled' : '') + '><i class="fas fa-' + (u.active ? 'ban' : 'check') + '"></i></button>' +
'<button class="btn btn-sm btn-danger" onclick="deleteUser(\'' + u.id + '\',\'' + u.username + '\')" title="Delete"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-trash-alt"></i></button>' +
@@ -126,7 +131,7 @@ async function loadUsers() {
document.getElementById('prev-btn').disabled = usersPage === 0;
document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
} catch (e) {
tbody.innerHTML = '<tr><td colspan="6" class="table-status-error"><i class="fas fa-exclamation-circle"></i> Error: ' + e.message + '</td></tr>';
tbody.innerHTML = '<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> Error: ' + e.message + '</td></tr>';
}
}