diff --git a/lnbits/core/crud/__init__.py b/lnbits/core/crud/__init__.py index 2108d23d5..708009db9 100644 --- a/lnbits/core/crud/__init__.py +++ b/lnbits/core/crud/__init__.py @@ -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", diff --git a/lnbits/core/crud/users.py b/lnbits/core/crud/users.py index fc68162a1..450617d02 100644 --- a/lnbits/core/crud/users.py +++ b/lnbits/core/crud/users.py @@ -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" diff --git a/lnbits/core/models/users.py b/lnbits/core/models/users.py index e7e60342e..576b97482 100644 --- a/lnbits/core/models/users.py +++ b/lnbits/core/models/users.py @@ -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 diff --git a/lnbits/core/views/auth_api.py b/lnbits/core/views/auth_api.py index 101d65b92..c416ba389 100644 --- a/lnbits/core/views/auth_api.py +++ b/lnbits/core/views/auth_api.py @@ -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: diff --git a/lnbits/core/views/user_api.py b/lnbits/core/views/user_api.py index dd500766b..3f125231f 100644 --- a/lnbits/core/views/user_api.py +++ b/lnbits/core/views/user_api.py @@ -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'}.", ) diff --git a/lnbits/static/js/pages/users.js b/lnbits/static/js/pages/users.js index 00bef033d..d33f9ec81 100644 --- a/lnbits/static/js/pages/users.js +++ b/lnbits/static/js/pages/users.js @@ -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 }) })