From 442a28e741f2850cd4d3a6848ce24d7636a0a319 Mon Sep 17 00:00:00 2001 From: Olaf Date: Tue, 25 Aug 2026 21:05:17 +0200 Subject: [PATCH] Fixed admin - remove user --- backend/app/api/admin.py | 8 ++++++- backend/tests/test_api.py | 35 +++++++++++++++++++++++++++- frontend/static/admin.js | 44 ++++++++++++++++++++++++----------- frontend/templates/admin.html | 2 +- 4 files changed, 73 insertions(+), 16 deletions(-) diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index b71390e..c38ccc8 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -83,8 +83,11 @@ def create_user(payload: AdminUserCreate, _: dict = Depends(require_admin)): def update_user_privileges( user_id: str, payload: AdminUserUpdate, - _: dict = Depends(require_admin), + current_user: dict = Depends(require_admin), ): + if user_id == current_user['id']: + raise HTTPException(status_code=400, detail='You cannot change your own administrator status') + with get_connection() as conn: target = conn.execute( 'SELECT id, is_admin FROM users WHERE id = ?', @@ -126,6 +129,9 @@ def delete_user(user_id: str, current_user: dict = Depends(require_admin)): if admins <= 1: raise HTTPException(status_code=400, detail='Cannot delete the last administrator') + conn.execute('DELETE FROM tokens WHERE user_id = ?', (user_id,)) + conn.execute('DELETE FROM user_plugin_config WHERE user_id = ?', (user_id,)) + conn.execute('DELETE FROM links WHERE user_id = ?', (user_id,)) conn.execute('DELETE FROM users WHERE id = ?', (user_id,)) conn.commit() return {'status': 'deleted', 'id': user_id} diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 0da1620..2d9c4c9 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -4,6 +4,7 @@ import json import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from uuid import uuid4 from fastapi.testclient import TestClient @@ -70,6 +71,38 @@ def test_admin_can_add_list_and_remove_users(): assert client.delete(f"/api/admin/users/{user['id']}", headers=headers).status_code == 200 assert all(item['id'] != user['id'] for item in client.get('/api/admin/users', headers=headers).json()) assert client.delete('/api/admin/users/user-1', headers=headers).status_code == 400 + assert client.put('/api/admin/users/user-1', headers=headers, json={'is_admin': False}).status_code == 400 + + +def test_admin_can_remove_user_with_owned_data(): + headers = login_headers() + username = f'data-owner-{uuid4().hex}' + create_response = client.post('/api/admin/users', headers=headers, json={ + 'username': username, + 'email': f'{username}@example.com', + 'password': 'secret123', + }) + assert create_response.status_code == 201 + user_id = create_response.json()['id'] + user_headers = login_headers(username) + + link_response = client.post('/api/links', headers=user_headers, json={ + 'title': 'Owned link', + 'url': 'https://example.com/owned', + 'comment': 'Owned data', + 'tags': ['owned'], + }) + assert link_response.status_code == 201 + label_response = client.post('/api/user/labels', headers=user_headers, json={'name': f'{username}-label'}) + assert label_response.status_code == 201 + plugin_response = client.put('/api/user/plugins/mastodon', headers=user_headers, json={ + 'instance': 'mastodon.social', + }) + assert plugin_response.status_code == 200 + + removed = client.delete(f'/api/admin/users/{user_id}', headers=headers) + assert removed.status_code == 200 + assert client.get('/api/auth/me', params={'token': user_headers['Authorization'].removeprefix('Bearer ')}).status_code == 401 def test_admin_can_toggle_privileges_without_removing_last_admin(): @@ -277,7 +310,7 @@ def test_public_and_admin_pages_render_html(): assert 'id="auth-menu" class="auth-menu hidden"' in admin_page assert 'id="auth-home-link" href="/">Home' in admin_page assert '' in admin_page - assert 'admin.js?v=3' in admin_page + assert 'admin.js?v=5' in admin_page feed_script = client.get('/static/feed.js?v=7').text assert 'if (item.is_owner)' in feed_script assert 'tag.toLowerCase() === pref.tag.toLowerCase()' in feed_script diff --git a/frontend/static/admin.js b/frontend/static/admin.js index 2f2dd2e..ef1cdfe 100644 --- a/frontend/static/admin.js +++ b/frontend/static/admin.js @@ -9,6 +9,7 @@ const userForm = document.querySelector('#user-form'); const adminControls = document.querySelector('#admin-controls'); const adminAuthNotice = document.querySelector('#admin-auth-notice'); const accessToken = localStorage.getItem('linklogAccessToken'); +let currentUserId = null; function authHeaders(includeJson = false) { return { @@ -71,15 +72,22 @@ function renderUsers(users) { const privilegeCheckbox = document.createElement('input'); privilegeCheckbox.type = 'checkbox'; privilegeCheckbox.checked = user.is_admin; + const isCurrentUser = user.id === currentUserId; + privilegeCheckbox.disabled = isCurrentUser; privilegeCheckbox.setAttribute('aria-label', `Administrator rights for ${user.username}`); - privilegeCheckbox.addEventListener('change', () => updateUserPrivilege(user, privilegeCheckbox)); + if (!isCurrentUser) { + privilegeCheckbox.addEventListener('change', () => updateUserPrivilege(user, privilegeCheckbox)); + } privilegeLabel.append(privilegeCheckbox, document.createTextNode(' Administrator')); - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'danger-button'; - button.textContent = 'Remove'; - button.addEventListener('click', () => removeUser(user, button)); - row.append(label, privilegeLabel, button); + row.append(label, privilegeLabel); + if (!isCurrentUser) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'danger-button'; + button.textContent = 'Remove'; + button.addEventListener('click', () => removeUser(user, button)); + row.append(button); + } return row; })); } @@ -119,6 +127,7 @@ async function loadAdminState() { } const user = await sessionResponse.json(); + currentUserId = user.id; if (!user.is_admin) { showUnauthorizedState(); return; @@ -151,13 +160,22 @@ async function updatePlugin(plugin, button) { async function removeUser(user, button) { if (!window.confirm(`Remove ${user.username}?`)) return; button.disabled = true; - const response = await fetch(`/api/admin/users/${encodeURIComponent(user.id)}`, { - method: 'DELETE', - headers: authHeaders(), - }); - if (response.ok) { + const status = document.querySelector('#user-status'); + try { + const response = await fetch(`/api/admin/users/${encodeURIComponent(user.id)}`, { + method: 'DELETE', + headers: authHeaders(), + }); + if (!response.ok) { + const detail = await response.text(); + throw new Error(detail || `Request failed (${response.status})`); + } await loadUsers(); - } else { + status.textContent = `Removed ${user.username}.`; + status.style.color = '#94e2d5'; + } catch (error) { + status.textContent = `Could not remove ${user.username}: ${error.message}`; + status.style.color = '#f38ba8'; button.disabled = false; } } diff --git a/frontend/templates/admin.html b/frontend/templates/admin.html index 3c41557..bfed1bf 100644 --- a/frontend/templates/admin.html +++ b/frontend/templates/admin.html @@ -77,6 +77,6 @@ - +