Fixed admin - remove user
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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</a>' in admin_page
|
||||
assert '<a id="auth-username" class="user-name" href="/">' 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
|
||||
|
||||
@@ -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}`);
|
||||
if (!isCurrentUser) {
|
||||
privilegeCheckbox.addEventListener('change', () => updateUserPrivilege(user, privilegeCheckbox));
|
||||
}
|
||||
privilegeLabel.append(privilegeCheckbox, document.createTextNode(' Administrator'));
|
||||
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(label, privilegeLabel, 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 status = document.querySelector('#user-status');
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${encodeURIComponent(user.id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(),
|
||||
});
|
||||
if (response.ok) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,6 @@
|
||||
<footer class="site-footer">Copyright © 2026 Olaf Kolkman · <a href="https://git.kolkman.org/olaf/Link-Log">git.kolkman.org/LinkLog</a></footer>
|
||||
<script src="/static/auth-header.js?v=3"></script>
|
||||
<script src="/static/logout.js?v=2"></script>
|
||||
<script src="/static/admin.js?v=3"></script>
|
||||
<script src="/static/admin.js?v=5"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user