Merge pull request #135 from zjean/feature/oidc-user-identity
Feature/OIDC user identity
This commit is contained in:
@@ -14,6 +14,7 @@ pub struct UserDto {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub last_login_at: Option<DateTime<Utc>>,
|
||||
pub active: bool,
|
||||
pub auth_provider: String,
|
||||
}
|
||||
|
||||
impl From<User> for UserDto {
|
||||
@@ -29,6 +30,7 @@ impl From<User> for UserDto {
|
||||
updated_at: user.updated_at(),
|
||||
last_login_at: user.last_login_at(),
|
||||
active: user.is_active(),
|
||||
auth_provider: user.oidc_provider().unwrap_or("local").to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +490,15 @@ impl AuthApplicationService {
|
||||
// Get user
|
||||
let mut user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
|
||||
// Block password changes for OIDC-provisioned users
|
||||
if user.is_oidc_user() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Auth",
|
||||
"Password changes are not available for SSO/OIDC accounts. Your password is managed by your identity provider.",
|
||||
));
|
||||
}
|
||||
|
||||
// Verify current password using the injected hasher
|
||||
let is_valid = self
|
||||
.password_hasher
|
||||
@@ -780,6 +789,16 @@ impl AuthApplicationService {
|
||||
user_id: &str,
|
||||
new_password: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
// Block password reset for OIDC-provisioned users
|
||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
if user.is_oidc_user() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Auth",
|
||||
"Cannot reset password for SSO/OIDC accounts. The user's password is managed by their identity provider.",
|
||||
));
|
||||
}
|
||||
|
||||
if new_password.len() < 8 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
|
||||
+2
-1
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user