feat: basic account activate/deactivate

This commit is contained in:
Vlad Stan
2026-02-05 12:21:01 +02:00
parent 2fe7ab4f83
commit 54fdb0948c
6 changed files with 92 additions and 81 deletions
+4
View File
@@ -61,7 +61,9 @@ from .users import (
get_user,
get_user_access_control_lists,
get_user_from_account,
is_account_activated,
update_account,
update_account_activation,
)
from .wallets import (
create_wallet,
@@ -144,11 +146,13 @@ __all__ = [
"get_wallets",
"get_webpush_subscription",
"get_webpush_subscriptions_for_user",
"is_account_activated",
"is_internal_status_success",
"mark_webhook_sent",
"remove_deleted_wallets",
"reset_core_settings",
"update_account",
"update_account_activation",
"update_admin_settings",
"update_installed_extension",
"update_installed_extension_state",
+66 -59
View File
@@ -4,12 +4,10 @@ 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 clear_wallet_cache, create_wallet, get_wallets
from lnbits.core.crud.wallets import 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,
@@ -38,12 +36,28 @@ async def update_account(account: Account, conn: Connection | None = None) -> Ac
return account
async def update_account_activation(
user_id: str, activated: bool, conn: Connection | None = None
) -> None:
await (conn or db).execute(
f"""
UPDATE accounts
SET activated = :activated, updated_at = {db.timestamp_placeholder('now')}
WHERE id = :user_id
""", # noqa: S608
{
"activated": activated,
"now": int(time()),
"user_id": user_id,
},
)
async def delete_account(user_id: str, conn: Connection | None = None) -> None:
await (conn or db).execute(
"DELETE from accounts WHERE id = :user",
{"user": user_id},
)
await clear_user_id_cache(user_id)
async def get_accounts(
@@ -98,21 +112,32 @@ async def get_accounts(
async def get_account(
user_id: str, active_only: bool = True, conn: Connection | None = None
user_id: str, activated: bool | None = True, conn: Connection | None = None
) -> Account | None:
if len(user_id) == 0:
return None
return await (conn or db).fetchone(
"""
f"""
SELECT * FROM accounts
WHERE id = :id AND (activated = true OR activated = :activated)
""",
{"id": user_id, "activated": active_only},
WHERE id = :id {_activated_clause(activated)}
""", # noqa: S608
{"id": user_id, "activated": activated},
Account,
)
async def is_account_activated(
user_id: str, conn: Connection | None = None
) -> bool | None:
result = await (conn or db).execute(
"SELECT activated FROM accounts WHERE id = :id",
{"id": user_id},
)
row = result.mappings().first()
return row.get("activated", False) if row is not None else None
async def delete_accounts_no_wallets(
time_delta: int,
conn: Connection | None = None,
@@ -134,79 +159,72 @@ async def delete_accounts_no_wallets(
async def get_account_by_username(
username: str, active_only: bool = True, conn: Connection | None = None
username: str, activated: bool | None = True, conn: Connection | None = None
) -> Account | None:
if len(username) == 0:
return None
return await (conn or db).fetchone(
"""
f"""
SELECT * FROM accounts
WHERE
LOWER(username) = :username
AND (activated = true OR activated = :activated)
""",
{"username": username.lower(), "activated": active_only},
WHERE LOWER(username) = :username {_activated_clause(activated)}
""", # noqa: S608
{"username": username.lower(), "activated": activated},
Account,
)
async def get_account_by_pubkey(
pubkey: str, active_only: bool = True, conn: Connection | None = None
pubkey: str, activated: bool | None = True, conn: Connection | None = None
) -> Account | None:
return await (conn or db).fetchone(
"""
f"""
SELECT * FROM accounts
WHERE
LOWER(pubkey) = :pubkey
AND (activated = true OR activated = :activated)
""",
{"pubkey": pubkey.lower(), "activated": active_only},
WHERE LOWER(pubkey) = :pubkey {_activated_clause(activated)}
""", # noqa: S608
{"pubkey": pubkey.lower(), "activated": activated},
Account,
)
async def get_account_by_email(
email: str, active_only: bool = True, conn: Connection | None = None
email: str, activated: bool | None = True, conn: Connection | None = None
) -> Account | None:
if len(email) == 0:
return None
return await (conn or db).fetchone(
"""
f"""
SELECT * FROM accounts
WHERE
LOWER(email) = :email
AND (activated = true OR activated = :activated)
""",
{"email": email.lower(), "activated": active_only},
WHERE LOWER(email) = :email {_activated_clause(activated)}
""", # noqa: S608
{"email": email.lower(), "activated": activated},
Account,
)
async def get_account_by_username_or_email(
username_or_email: str,
active_only: bool = True,
activated: bool | None = True,
conn: Connection | None = None,
) -> Account | None:
return await (conn or db).fetchone(
"""
f"""
SELECT * FROM accounts
WHERE
(LOWER(email) = :value or LOWER(username) = :value)
AND (activated = true OR activated = :activated)
""",
{"value": username_or_email.lower(), "activated": active_only},
WHERE (LOWER(email) = :value or LOWER(username) = :value)
{_activated_clause(activated)}
""", # noqa: S608
{"value": username_or_email.lower(), "activated": activated},
Account,
)
async def get_user(
user_id: str, active_only: bool = True, conn: Connection | None = None
user_id: str, activated: bool | None = 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, active_only, conn=conn)
account = await get_account(user_id, activated=activated, conn=conn)
if not account:
return None
return await get_user_from_account(account, conn=conn)
@@ -225,7 +243,6 @@ async def get_user_from_account(
return User(
id=account.id,
activated=account.activated,
email=account.email,
username=account.username,
pubkey=account.pubkey,
@@ -251,31 +268,21 @@ async def update_user_access_control_list(
async def get_user_access_control_lists(
user_id: str, active_only: bool = True, conn: Connection | None = None
user_id: str, activated: bool | None = True, conn: Connection | None = None
) -> UserAcls:
user_acls = await (conn or db).fetchone(
"""
f"""
SELECT id, access_control_list FROM accounts
WHERE id = :user_id AND (activated = true OR activated = :activated)
""",
{"user_id": user_id, "activated": active_only},
WHERE id = :user_id {_activated_clause(activated)}
""", # noqa: S608
{"user_id": user_id, "activated": activated},
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)
def _activated_clause(activated: bool | None) -> str:
if activated is None:
return ""
return "AND activated = :activated"
+1 -3
View File
@@ -181,7 +181,6 @@ class AccountId(BaseModel):
class Account(AccountId):
activated: bool = True
external_id: str | None = None # for external account linking
username: str | None = None
password_hash: str | None = None
@@ -242,7 +241,7 @@ class Account(AccountId):
class AccountOverview(Account):
activated: bool = True
activated: bool
transaction_count: int | None = 0
wallet_count: int | None = 0
balance_msat: int | None = 0
@@ -278,7 +277,6 @@ class AccountFilters(FilterModel):
class User(BaseModel):
id: str
activated: bool = True
created_at: datetime
updated_at: datetime
email: str | None = None
+8 -6
View File
@@ -51,6 +51,7 @@ from ..crud import (
get_account_by_username,
get_account_by_username_or_email,
get_user_from_account,
is_account_activated,
update_account,
)
from ..models import (
@@ -83,10 +84,13 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
raise HTTPException(
HTTPStatus.FORBIDDEN, "Login by 'Username and Password' not allowed."
)
account = await get_account_by_username_or_email(data.username)
account = await get_account_by_username_or_email(data.username, activated=None)
if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
is_activated = await is_account_activated(account.id)
if is_activated is False:
raise HTTPException(HTTPStatus.FORBIDDEN, "Account is not activated.")
return _auth_success_response(account.username, account.id, account.email)
@@ -95,7 +99,7 @@ async def nostr_login(request: Request) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
event = _nostr_nip98_event(request)
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
account = await get_account_by_pubkey(event["pubkey"])
if not account:
account = Account(
id=uuid4().hex,
@@ -103,8 +107,6 @@ async def nostr_login(request: Request) -> JSONResponse:
extra=UserExtra(provider="nostr"),
)
await create_user_account(account)
if not account.activated:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
return _auth_success_response(account.username or "", account.id, account.email)
@@ -360,7 +362,7 @@ async def register(data: RegisterUser) -> JSONResponse:
if not is_valid_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
if await get_account_by_username(data.username, active_only=False):
if await get_account_by_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
if data.email and not is_valid_email_address(data.email):
@@ -530,7 +532,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
redirect_path = "/wallet"
account = await get_account_by_email(email, active_only=False)
account = await get_account_by_email(email)
if verified_user_id:
if account:
+11 -11
View File
@@ -17,10 +17,11 @@ from lnbits.core.crud import (
get_user,
get_wallet,
get_wallets,
is_account_activated,
update_account_activation,
update_admin_settings,
update_wallet,
)
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
from lnbits.core.crud.wallets import delete_wallet_by_id
from lnbits.core.models import (
AccountFilters,
@@ -74,7 +75,7 @@ async def api_get_users(
summary="Get user by Id",
)
async def api_get_user(user_id: str) -> User:
user = await get_user(user_id, active_only=False)
user = await get_user(user_id)
if not user:
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
return user
@@ -226,35 +227,34 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
name="Activate or deactivate a user",
)
async def api_users_toggle_activated(
user_id: str, admin_account: Account = Depends(check_admin)
user_id: str, account: Account = Depends(check_admin)
) -> SimpleStatus:
if user_id == settings.super_user:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Cannot deactivate super user.",
)
if user_id == admin_account.id:
if user_id == account.id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="You cannot deactivate yourself.",
detail="Users cannot deactivate themselves.",
)
if settings.is_admin_user(user_id):
settings.lnbits_admin_users.remove(user_id)
user_account = await get_account(user_id, active_only=False)
if not user_account:
is_activated = await is_account_activated(user_id)
if is_activated is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="User not found.",
)
user_account.activated = not user_account.activated
await update_account(user_account)
await clear_user_id_cache(user_id)
await update_account_activation(user_id, not is_activated)
return SimpleStatus(
success=True,
message=f"User {'activated' if user_account.activated else 'deactivated'}.",
message=f"User {'activated' if not is_activated else 'deactivated'}.",
)
+2 -2
View File
@@ -415,11 +415,11 @@ window.PageUsers = {
toggleUserActivated(userId) {
LNbits.api
.request('PUT', `/users/api/v1/user/${userId}/activate`)
.then(res => {
.then(() => {
this.fetchUsers()
Quasar.Notify.create({
type: 'positive',
message: res.data.message,
message: 'Toggled user activation!',
icon: null
})
})