[feat] User activation (#3749)

This commit is contained in:
Vlad Stan
2026-02-25 07:54:00 +01:00
committed by dni ⚡
parent 40c055e373
commit 7e327b2c6a
16 changed files with 339 additions and 50 deletions
+75 -21
View File
@@ -4,10 +4,12 @@ from typing import Any
from uuid import uuid4
from lnbits.core.crud.extensions import get_user_active_extensions_ids
from lnbits.core.crud.wallets import create_wallet, get_wallets
from lnbits.core.crud.wallets import clear_wallet_cache, create_wallet, get_wallets
from lnbits.core.db import db
from lnbits.core.models import UserAcls
from lnbits.db import Connection, Filters, Page
from lnbits.helpers import sha256s
from lnbits.utils.cache import cache
from ..models import (
Account,
@@ -41,6 +43,7 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
"DELETE from accounts WHERE id = :user",
{"user": user_id},
)
await clear_user_id_cache(user_id)
async def get_accounts(
@@ -69,6 +72,7 @@ async def get_accounts(
accounts.email,
accounts.pubkey,
accounts.external_id,
accounts.activated,
SUM(COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)) as balance_msat,
@@ -93,12 +97,18 @@ async def get_accounts(
)
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
async def get_account(
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
if len(user_id) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE id = :id",
{"id": user_id},
"""
SELECT * FROM accounts
WHERE id = :id AND (activated = true OR activated = :activated)
""",
{"id": user_id, "activated": active_only},
Account,
)
@@ -124,55 +134,79 @@ async def delete_accounts_no_wallets(
async def get_account_by_username(
username: str, conn: Connection | None = None
username: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
if len(username) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(username) = :username",
{"username": username.lower()},
"""
SELECT * FROM accounts
WHERE
LOWER(username) = :username
AND (activated = true OR activated = :activated)
""",
{"username": username.lower(), "activated": active_only},
Account,
)
async def get_account_by_pubkey(
pubkey: str, conn: Connection | None = None
pubkey: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
{"pubkey": pubkey.lower()},
"""
SELECT * FROM accounts
WHERE
LOWER(pubkey) = :pubkey
AND (activated = true OR activated = :activated)
""",
{"pubkey": pubkey.lower(), "activated": active_only},
Account,
)
async def get_account_by_email(
email: str, conn: Connection | None = None
email: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
if len(email) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(email) = :email",
{"email": email.lower()},
"""
SELECT * FROM accounts
WHERE
LOWER(email) = :email
AND (activated = true OR activated = :activated)
""",
{"email": email.lower(), "activated": active_only},
Account,
)
async def get_account_by_username_or_email(
username_or_email: str, conn: Connection | None = None
username_or_email: str,
active_only: bool = True,
conn: Connection | None = None,
) -> Account | None:
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE LOWER(email) = :value or LOWER(username) = :value
WHERE
(LOWER(email) = :value or LOWER(username) = :value)
AND (activated = true OR activated = :activated)
""",
{"value": username_or_email.lower()},
{"value": username_or_email.lower(), "activated": active_only},
Account,
)
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
async def get_user(
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> User | None:
async with db.reuse_conn(conn) if conn else db.connect() as conn:
account = await get_account(user_id, conn=conn)
account = await get_account(user_id, active_only, conn=conn)
if not account:
return None
return await get_user_from_account(account, conn=conn)
@@ -191,6 +225,7 @@ async def get_user_from_account(
return User(
id=account.id,
activated=account.activated,
email=account.email,
username=account.username,
pubkey=account.pubkey,
@@ -216,12 +251,31 @@ async def update_user_access_control_list(
async def get_user_access_control_lists(
user_id: str, conn: Connection | None = None
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> UserAcls:
user_acls = await (conn or db).fetchone(
"SELECT id, access_control_list FROM accounts WHERE id = :id",
{"id": user_id},
"""
SELECT id, access_control_list FROM accounts
WHERE id = :user_id AND (activated = true OR activated = :activated)
""",
{"user_id": user_id, "activated": active_only},
UserAcls,
)
return user_acls or UserAcls(id=user_id)
async def clear_user_id_cache(user_id: str):
user = await get_user(user_id, active_only=True)
if user:
clear_user_cache(user)
def clear_user_cache(user: User):
user_cache_key: str | None = cache.pop(
f"auth:user:cache_key:{sha256s(user.id)}", None
)
if user_cache_key:
cache.pop(user_cache_key)
for wallet in user.wallets:
clear_wallet_cache(wallet)
+20 -8
View File
@@ -50,7 +50,7 @@ async def delete_wallet(
deleted: bool = True,
conn: Connection | None = None,
) -> None:
_clear_wallet_cache(wallet_id)
clear_wallet_id_cache(wallet_id)
now = int(time())
await (conn or db).execute(
@@ -65,7 +65,7 @@ async def delete_wallet(
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
_clear_wallet_cache(wallet_id)
clear_wallet_id_cache(wallet_id)
await (conn or db).execute(
"DELETE FROM wallets WHERE id = :wallet",
{"wallet": wallet_id},
@@ -75,7 +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)
clear_wallet_id_cache(wallet_id)
now = int(time())
result = await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
@@ -226,11 +226,14 @@ async def get_wallet_for_key(
) -> Wallet | None:
wallet = await (conn or db).fetchone(
"""
SELECT *, COALESCE((
SELECT wallets.*, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)
AS balance_msat FROM wallets
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
INNER JOIN accounts ON wallets.user = accounts.id
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
""",
{"key": key},
Wallet,
@@ -250,8 +253,11 @@ async def get_base_wallet_for_key(
) -> BaseWallet | None:
wallet = await (conn or db).fetchone(
"""
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets
INNER JOIN accounts ON wallets.user = accounts.id
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
""",
{"key": key},
BaseWallet,
@@ -294,8 +300,14 @@ async def get_total_balance(conn: Connection | None = None):
return row.get("balance", 0) or 0
def _clear_wallet_cache(wallet_id):
def clear_wallet_id_cache(wallet_id: str):
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}")
def clear_wallet_cache(wallet: Wallet):
cache.pop(f"auth:wallet:{wallet.id}")
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
cache.pop(f"auth:x-api-key:{wallet.inkey}")