feat(users): allow role assignment for Azure AD and LDAP users
- Backend: add admin-only guard + role validation to PUT /users/{id}
- Backend: prevent admins from changing their own role
- Frontend: role toggle button (person-check / person-dash) per user row
- Frontend: admin badge green, viewer badge secondary, ldap badge blue
- i18n: add makeAdmin / makeViewer translations (de + en)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -70,12 +70,31 @@ async def update_user(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update an existing user (email, is_active, role)."""
|
"""Update an existing user (email, is_active, role). Admin only."""
|
||||||
|
if current_user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Only admins can update users.",
|
||||||
|
)
|
||||||
|
|
||||||
user = db.query(User).filter(User.id == user_id).first()
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found.")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found.")
|
||||||
|
|
||||||
update_data = payload.model_dump(exclude_none=True)
|
update_data = payload.model_dump(exclude_none=True)
|
||||||
|
|
||||||
|
if "role" in update_data:
|
||||||
|
if update_data["role"] not in ("admin", "viewer"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Role must be 'admin' or 'viewer'.",
|
||||||
|
)
|
||||||
|
if user_id == current_user.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="You cannot change your own role.",
|
||||||
|
)
|
||||||
|
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
if hasattr(user, field):
|
if hasattr(user, field):
|
||||||
setattr(user, field, value)
|
setattr(user, field, value)
|
||||||
|
|||||||
@@ -1368,8 +1368,8 @@ async function loadUsers() {
|
|||||||
<td>${u.id}</td>
|
<td>${u.id}</td>
|
||||||
<td><strong>${esc(u.username)}</strong></td>
|
<td><strong>${esc(u.username)}</strong></td>
|
||||||
<td>${esc(u.email || '-')}</td>
|
<td>${esc(u.email || '-')}</td>
|
||||||
<td><span class="badge bg-info">${esc(u.role || 'admin')}</span></td>
|
<td><span class="badge bg-${u.role === 'admin' ? 'success' : 'secondary'}">${esc(u.role || 'admin')}</span></td>
|
||||||
<td><span class="badge bg-${u.auth_provider === 'azure' ? 'primary' : 'secondary'}">${esc(u.auth_provider || 'local')}</span></td>
|
<td><span class="badge bg-${u.auth_provider === 'azure' ? 'primary' : u.auth_provider === 'ldap' ? 'info' : 'secondary'}">${esc(u.auth_provider || 'local')}</span></td>
|
||||||
<td>${langDisplay}</td>
|
<td>${langDisplay}</td>
|
||||||
<td>${mfaDisplay}</td>
|
<td>${mfaDisplay}</td>
|
||||||
<td>${u.is_active ? `<span class="badge bg-success">${t('common.active')}</span>` : `<span class="badge bg-danger">${t('common.disabled')}</span>`}</td>
|
<td>${u.is_active ? `<span class="badge bg-success">${t('common.active')}</span>` : `<span class="badge bg-danger">${t('common.disabled')}</span>`}</td>
|
||||||
@@ -1381,6 +1381,11 @@ async function loadUsers() {
|
|||||||
}
|
}
|
||||||
${u.auth_provider === 'local' ? `<button class="btn btn-outline-info" title="${t('common.resetPassword')}" onclick="resetUserPassword(${u.id}, '${esc(u.username)}')"><i class="bi bi-key"></i></button>` : ''}
|
${u.auth_provider === 'local' ? `<button class="btn btn-outline-info" title="${t('common.resetPassword')}" onclick="resetUserPassword(${u.id}, '${esc(u.username)}')"><i class="bi bi-key"></i></button>` : ''}
|
||||||
${u.totp_enabled ? `<button class="btn btn-outline-secondary" title="${t('mfa.resetMfa')}" onclick="resetUserMfa(${u.id}, '${esc(u.username)}')"><i class="bi bi-shield-x"></i></button>` : ''}
|
${u.totp_enabled ? `<button class="btn btn-outline-secondary" title="${t('mfa.resetMfa')}" onclick="resetUserMfa(${u.id}, '${esc(u.username)}')"><i class="bi bi-shield-x"></i></button>` : ''}
|
||||||
|
${currentUser && currentUser.role === 'admin' && u.id !== currentUser.id
|
||||||
|
? (u.role === 'admin'
|
||||||
|
? `<button class="btn btn-outline-secondary" title="${t('settings.makeViewer')}" onclick="toggleUserRole(${u.id}, 'admin')"><i class="bi bi-person-dash"></i></button>`
|
||||||
|
: `<button class="btn btn-outline-success" title="${t('settings.makeAdmin')}" onclick="toggleUserRole(${u.id}, 'viewer')"><i class="bi bi-person-check"></i></button>`)
|
||||||
|
: ''}
|
||||||
<button class="btn btn-outline-danger" title="${t('common.delete')}" onclick="deleteUser(${u.id}, '${esc(u.username)}')"><i class="bi bi-trash"></i></button>
|
<button class="btn btn-outline-danger" title="${t('common.delete')}" onclick="deleteUser(${u.id}, '${esc(u.username)}')"><i class="bi bi-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -1440,6 +1445,16 @@ async function toggleUserActive(id, active) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleUserRole(id, currentRole) {
|
||||||
|
const newRole = currentRole === 'admin' ? 'viewer' : 'admin';
|
||||||
|
try {
|
||||||
|
await api('PUT', `/users/${id}`, { role: newRole });
|
||||||
|
loadUsers();
|
||||||
|
} catch (err) {
|
||||||
|
showSettingsAlert('danger', t('errors.updateFailed', { error: err.message }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function resetUserPassword(id, username) {
|
async function resetUserPassword(id, username) {
|
||||||
if (!confirm(t('messages.confirmResetPassword', { username }))) return;
|
if (!confirm(t('messages.confirmResetPassword', { username }))) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -170,6 +170,8 @@
|
|||||||
"saveBranding": "Branding speichern",
|
"saveBranding": "Branding speichern",
|
||||||
"userManagement": "Benutzerverwaltung",
|
"userManagement": "Benutzerverwaltung",
|
||||||
"newUser": "Neuer Benutzer",
|
"newUser": "Neuer Benutzer",
|
||||||
|
"makeAdmin": "Zum Admin befördern",
|
||||||
|
"makeViewer": "Zum Viewer degradieren",
|
||||||
"thId": "ID",
|
"thId": "ID",
|
||||||
"thUsername": "Benutzername",
|
"thUsername": "Benutzername",
|
||||||
"thEmail": "E-Mail",
|
"thEmail": "E-Mail",
|
||||||
|
|||||||
@@ -191,6 +191,8 @@
|
|||||||
"saveBranding": "Save Branding",
|
"saveBranding": "Save Branding",
|
||||||
"userManagement": "User Management",
|
"userManagement": "User Management",
|
||||||
"newUser": "New User",
|
"newUser": "New User",
|
||||||
|
"makeAdmin": "Promote to admin",
|
||||||
|
"makeViewer": "Demote to viewer",
|
||||||
"thId": "ID",
|
"thId": "ID",
|
||||||
"thUsername": "Username",
|
"thUsername": "Username",
|
||||||
"thEmail": "Email",
|
"thEmail": "Email",
|
||||||
|
|||||||
Reference in New Issue
Block a user