From 5b2937672cbe73f1c67ebdb490f22812a3cf64ae Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 8 Jan 2026 17:39:37 +0200 Subject: [PATCH] fix: delete user with wallets (#3711) --- lnbits/core/crud/wallets.py | 15 ++++++++----- lnbits/core/views/user_api.py | 8 +++---- tests/api/test_users.py | 42 +++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/lnbits/core/crud/wallets.py b/lnbits/core/crud/wallets.py index 529eb4e9c..21ccd141c 100644 --- a/lnbits/core/crud/wallets.py +++ b/lnbits/core/crud/wallets.py @@ -45,17 +45,13 @@ async def update_wallet( async def delete_wallet( - *, user_id: str, wallet_id: str, deleted: bool = True, conn: Connection | None = None, ) -> None: + _clear_wallet_cache(wallet_id) now = int(time()) - cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}") - if cached_wallet: - cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}") - cache.pop(f"auth:x-api-key:{cached_wallet.inkey}") await (conn or db).execute( # Timestamp placeholder is safe from SQL injection (not user input) @@ -69,6 +65,7 @@ async def delete_wallet( async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None: + _clear_wallet_cache(wallet_id) await (conn or db).execute( "DELETE FROM wallets WHERE id = :wallet", {"wallet": wallet_id}, @@ -78,6 +75,7 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> async def delete_wallet_by_id( wallet_id: str, conn: Connection | None = None ) -> int | None: + _clear_wallet_cache(wallet_id) now = int(time()) result = await (conn or db).execute( # Timestamp placeholder is safe from SQL injection (not user input) @@ -294,3 +292,10 @@ async def get_total_balance(conn: Connection | None = None): result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances") row = result.mappings().first() return row.get("balance", 0) or 0 + + +def _clear_wallet_cache(wallet_id): + cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}") + if cached_wallet: + cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}") + cache.pop(f"auth:x-api-key:{cached_wallet.inkey}") diff --git a/lnbits/core/views/user_api.py b/lnbits/core/views/user_api.py index 012e4f263..807e81b17 100644 --- a/lnbits/core/views/user_api.py +++ b/lnbits/core/views/user_api.py @@ -20,6 +20,7 @@ from lnbits.core.crud import ( update_admin_settings, update_wallet, ) +from lnbits.core.crud.wallets import delete_wallet_by_id from lnbits.core.models import ( AccountFilters, AccountOverview, @@ -157,11 +158,8 @@ async def api_users_delete_user( user_id: str, account: Account = Depends(check_admin) ) -> SimpleStatus: wallets = await get_wallets(user_id, deleted=False) - if len(wallets) > 0: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail="Cannot delete user with wallets.", - ) + for wallet in wallets: + await delete_wallet_by_id(wallet.id) if user_id == settings.super_user: raise HTTPException( diff --git a/tests/api/test_users.py b/tests/api/test_users.py index d6c6ce226..d486fe6b7 100644 --- a/tests/api/test_users.py +++ b/tests/api/test_users.py @@ -5,6 +5,7 @@ import pytest import shortuuid from httpx import AsyncClient +from lnbits.core.crud.wallets import get_wallets from lnbits.core.models.users import Account, User from lnbits.core.services.users import create_user_account from lnbits.settings import Settings @@ -510,3 +511,44 @@ async def test_search_users(http_client: AsyncClient, superuser_token): data = create_resp.json() assert data["total"] == 1 assert data["data"][0]["username"] == users[0].username + + +@pytest.mark.anyio +async def test_delete_user_success(http_client: AsyncClient, superuser_token): + # Create a user first + tiny_id = shortuuid.uuid()[:8] + data = { + "username": f"delete_{tiny_id}", + "password": "secret1234", + "password_repeat": "secret1234", + "email": f"delete_{tiny_id}@lnbits.com", + } + create_resp = await http_client.post( + "/users/api/v1/user", + json=data, + headers={"Authorization": f"Bearer {superuser_token}"}, + ) + assert create_resp.status_code == 200 + user = create_resp.json() + user_id = user["id"] + + wallets = await get_wallets(user_id=user_id) + assert len(wallets) == 1 + + # Delete the user + delete_resp = await http_client.delete( + f"/users/api/v1/user/{user_id}", + headers={"Authorization": f"Bearer {superuser_token}"}, + ) + assert delete_resp.status_code == 200 + assert delete_resp.json()["success"] is True + + # Ensure user is deleted + get_resp = await http_client.get( + f"/users/api/v1/user/{user_id}", + headers={"Authorization": f"Bearer {superuser_token}"}, + ) + assert get_resp.status_code == 404 + + wallets = await get_wallets(user_id=user_id) + assert len(wallets) == 0