From af568d0f31fadf06a66845528797d296f1e82056 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 19 Nov 2024 10:33:57 +0200 Subject: [PATCH] feat: improve user admin (#2777) --- lnbits/core/crud/__init__.py | 2 + lnbits/core/crud/users.py | 6 +- lnbits/core/models/__init__.py | 2 + lnbits/core/models/users.py | 49 +- lnbits/core/services/__init__.py | 12 +- lnbits/core/services/users.py | 71 ++- lnbits/core/templates/core/wallet.html | 24 +- .../templates/users/_createUserDialog.html | 23 - .../templates/users/_createWalletDialog.html | 51 +- lnbits/core/templates/users/_manageUser.html | 187 +++++++ ...{_walletDialog.html => _manageWallet.html} | 121 +++-- lnbits/core/templates/users/index.html | 167 +++--- lnbits/core/views/auth_api.py | 4 +- lnbits/core/views/user_api.py | 167 +++++- lnbits/db.py | 4 +- lnbits/helpers.py | 10 + lnbits/settings.py | 5 +- lnbits/static/bundle.min.js | 2 +- lnbits/static/i18n/en.js | 1 + lnbits/static/js/users.js | 359 ++++++------ lnbits/static/js/wallet.js | 2 +- lnbits/templates/components.vue | 509 +++++++++--------- 22 files changed, 1145 insertions(+), 633 deletions(-) delete mode 100644 lnbits/core/templates/users/_createUserDialog.html create mode 100644 lnbits/core/templates/users/_manageUser.html rename lnbits/core/templates/users/{_walletDialog.html => _manageWallet.html} (62%) diff --git a/lnbits/core/crud/__init__.py b/lnbits/core/crud/__init__.py index 07887290d8..234e7b7e30 100644 --- a/lnbits/core/crud/__init__.py +++ b/lnbits/core/crud/__init__.py @@ -13,6 +13,7 @@ get_installed_extensions, get_user_active_extensions_ids, get_user_extension, + get_user_extensions, update_installed_extension, update_installed_extension_state, update_user_extension, @@ -98,6 +99,7 @@ "update_installed_extension", "update_installed_extension_state", "update_user_extension", + "get_user_extensions", # payments "DateTrunc", "check_internal", diff --git a/lnbits/core/crud/users.py b/lnbits/core/crud/users.py index e542591580..717e297ae3 100644 --- a/lnbits/core/crud/users.py +++ b/lnbits/core/crud/users.py @@ -20,7 +20,9 @@ async def create_account( account: Optional[Account] = None, conn: Optional[Connection] = None, ) -> Account: - if not account: + if account: + account.validate_fields() + else: now = datetime.now(timezone.utc) account = Account(id=uuid4().hex, created_at=now, updated_at=now) await (conn or db).insert("accounts", account) @@ -50,6 +52,8 @@ async def get_accounts( accounts.id, accounts.username, accounts.email, + accounts.pubkey, + wallets.id as wallet_id, SUM(COALESCE(( SELECT balance FROM balances WHERE wallet_id = wallets.id ), 0)) as balance_msat, diff --git a/lnbits/core/models/__init__.py b/lnbits/core/models/__init__.py index e4e65fdb3c..aef5146b87 100644 --- a/lnbits/core/models/__init__.py +++ b/lnbits/core/models/__init__.py @@ -28,6 +28,7 @@ CreateUser, LoginUsernamePassword, LoginUsr, + RegisterUser, ResetUserPassword, UpdateSuperuserPassword, UpdateUser, @@ -70,6 +71,7 @@ "AccountOverview", "CreateTopup", "CreateUser", + "RegisterUser", "LoginUsernamePassword", "LoginUsr", "ResetUserPassword", diff --git a/lnbits/core/models/users.py b/lnbits/core/models/users.py index a730c65018..cda1faf48d 100644 --- a/lnbits/core/models/users.py +++ b/lnbits/core/models/users.py @@ -2,12 +2,14 @@ from datetime import datetime, timezone from typing import Optional +from uuid import UUID from fastapi import Query from passlib.context import CryptContext from pydantic import BaseModel, Field from lnbits.db import FilterModel +from lnbits.helpers import is_valid_email_address, is_valid_pubkey, is_valid_username from lnbits.settings import settings from .wallets import Wallet @@ -36,13 +38,13 @@ class Account(BaseModel): created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) - @property - def is_super_user(self) -> bool: - return self.id == settings.super_user + is_super_user: bool = Field(default=False, no_database=True) + is_admin: bool = Field(default=False, no_database=True) - @property - def is_admin(self) -> bool: - return self.id in settings.lnbits_admin_users or self.is_super_user + def __init__(self, **data): + super().__init__(**data) + self.is_super_user = settings.is_super_user(self.id) + self.is_admin = settings.is_admin_user(self.id) def hash_password(self, password: str) -> str: """sets and returns the hashed password""" @@ -57,6 +59,17 @@ def verify_password(self, password: str) -> bool: pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") return pwd_context.verify(password, self.password_hash) + def validate_fields(self): + if self.username and not is_valid_username(self.username): + raise ValueError("Invalid username.") + if self.email and not is_valid_email_address(self.email): + raise ValueError("Invalid email.") + if self.pubkey and not is_valid_pubkey(self.pubkey): + raise ValueError("Invalid pubkey.") + user_uuid4 = UUID(hex=self.id, version=4) + if user_uuid4.hex != self.id: + raise ValueError("User ID is not valid UUID4 hex string.") + class AccountOverview(Account): transaction_count: Optional[int] = 0 @@ -66,7 +79,7 @@ class AccountOverview(Account): class AccountFilters(FilterModel): - __search_fields__ = ["id", "email", "username"] + __search_fields__ = ["user", "email", "username", "pubkey", "wallet_id"] __sort_fields__ = [ "balance_msat", "email", @@ -76,12 +89,11 @@ class AccountFilters(FilterModel): "last_payment", ] - id: str - last_payment: Optional[datetime] = None - transaction_count: Optional[int] = None - wallet_count: Optional[int] = None - username: Optional[str] = None email: Optional[str] = None + user: Optional[str] = None + username: Optional[str] = None + pubkey: Optional[str] = None + wallet_id: Optional[str] = None class User(BaseModel): @@ -117,13 +129,24 @@ def is_extension_for_user(cls, ext: str, user: str) -> bool: return False -class CreateUser(BaseModel): +class RegisterUser(BaseModel): email: Optional[str] = Query(default=None) username: str = Query(default=..., min_length=2, max_length=20) password: str = Query(default=..., min_length=8, max_length=50) password_repeat: str = Query(default=..., min_length=8, max_length=50) +class CreateUser(BaseModel): + id: Optional[str] = Query(default=None) + email: Optional[str] = Query(default=None) + username: Optional[str] = Query(default=None, min_length=2, max_length=20) + password: Optional[str] = Query(default=None, min_length=8, max_length=50) + password_repeat: Optional[str] = Query(default=None, min_length=8, max_length=50) + pubkey: str = Query(default=None, max_length=64) + extensions: Optional[list[str]] = None + extra: Optional[UserExtra] = None + + class UpdateUser(BaseModel): user_id: str email: Optional[str] = Query(default=None) diff --git a/lnbits/core/services/__init__.py b/lnbits/core/services/__init__.py index 6b094c2f50..9edf528ebe 100644 --- a/lnbits/core/services/__init__.py +++ b/lnbits/core/services/__init__.py @@ -20,7 +20,14 @@ check_webpush_settings, update_cached_settings, ) -from .users import check_admin_settings, create_user_account, init_admin_settings +from .users import ( + check_admin_settings, + create_user_account, + create_user_account_no_ckeck, + init_admin_settings, + update_user_account, + update_user_extensions, +) from .websockets import websocket_manager, websocket_updater __all__ = [ @@ -48,7 +55,10 @@ # users "check_admin_settings", "create_user_account", + "create_user_account_no_ckeck", "init_admin_settings", + "update_user_account", + "update_user_extensions", # websockets "websocket_manager", "websocket_updater", diff --git a/lnbits/core/services/users.py b/lnbits/core/services/users.py index 14f62aec8c..a42478b23d 100644 --- a/lnbits/core/services/users.py +++ b/lnbits/core/services/users.py @@ -1,6 +1,6 @@ from pathlib import Path from typing import Optional -from uuid import UUID, uuid4 +from uuid import uuid4 from loguru import logger @@ -15,13 +15,16 @@ from ..crud import ( create_account, create_admin_settings, + create_user_extension, create_wallet, get_account, get_account_by_email, get_account_by_pubkey, get_account_by_username, get_super_settings, + get_user_extensions, get_user_from_account, + update_account, update_super_user, update_user_extension, ) @@ -39,7 +42,16 @@ async def create_user_account( ) -> User: if not settings.new_accounts_allowed: raise ValueError("Account creation is disabled.") + + return await create_user_account_no_ckeck(account, wallet_name) + + +async def create_user_account_no_ckeck( + account: Optional[Account] = None, wallet_name: Optional[str] = None +) -> User: + if account: + account.validate_fields() if account.username and await get_account_by_username(account.username): raise ValueError("Username already exists.") @@ -49,10 +61,7 @@ async def create_user_account( if account.pubkey and await get_account_by_pubkey(account.pubkey): raise ValueError("Pubkey already exists.") - if account.id: - user_uuid4 = UUID(hex=account.id, version=4) - assert user_uuid4.hex == account.id, "User ID is not valid UUID4 hex string" - else: + if not account.id: account.id = uuid4().hex account = await create_account(account) @@ -71,6 +80,58 @@ async def create_user_account( return user +async def update_user_account(account: Account) -> Account: + account.validate_fields() + + existing_account = await get_account(account.id) + if not existing_account: + raise ValueError("User does not exist.") + + account.password_hash = existing_account.password_hash + + if existing_account.username and not account.username: + raise ValueError("Cannot remove username.") + + if account.username: + existing_account = await get_account_by_username(account.username) + if existing_account and existing_account.id != account.id: + raise ValueError("Username already exists.") + elif existing_account.username: + raise ValueError("Cannot remove username.") + + if account.email: + existing_account = await get_account_by_email(account.email) + if existing_account and existing_account.id != account.id: + raise ValueError("Email already exists.") + + if account.pubkey: + existing_account = await get_account_by_pubkey(account.pubkey) + if existing_account and existing_account.id != account.id: + raise ValueError("Pubkey already exists.") + + return await update_account(account) + + +async def update_user_extensions(user_id: str, extensions: list[str]): + user_extensions = await get_user_extensions(user_id) + for user_ext in user_extensions: + if user_ext.active: + if user_ext.extension not in extensions: + user_ext.active = False + await update_user_extension(user_ext) + else: + if user_ext.extension in extensions: + user_ext.active = True + await update_user_extension(user_ext) + + user_extension_ids = [ue.extension for ue in user_extensions] + for ext in extensions: + if ext in user_extension_ids: + continue + user_extension = UserExtension(user=user_id, extension=ext, active=True) + await create_user_extension(user_extension) + + async def check_admin_settings(): if settings.super_user: settings.super_user = to_valid_user_id(settings.super_user).hex diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index d1c73797da..a10a6ff7fc 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -101,11 +101,25 @@

- + + + + + {% if HIDE_API %}
diff --git a/lnbits/core/templates/users/_createUserDialog.html b/lnbits/core/templates/users/_createUserDialog.html deleted file mode 100644 index 65e7191583..0000000000 --- a/lnbits/core/templates/users/_createUserDialog.html +++ /dev/null @@ -1,23 +0,0 @@ - - -

Create User

-
-
- - -
- Create - Cancel -
-
-
-
-
-
diff --git a/lnbits/core/templates/users/_createWalletDialog.html b/lnbits/core/templates/users/_createWalletDialog.html index 43e6a8410f..5180f07107 100644 --- a/lnbits/core/templates/users/_createWalletDialog.html +++ b/lnbits/core/templates/users/_createWalletDialog.html @@ -1,22 +1,43 @@ - - -

Create Wallet

+ + + Create Wallet
- - -
- Create - Cancel +
+ + +
+
+
+
+
- +
+
+ Create + Cancel +
diff --git a/lnbits/core/templates/users/_manageUser.html b/lnbits/core/templates/users/_manageUser.html new file mode 100644 index 0000000000..e391d987f4 --- /dev/null +++ b/lnbits/core/templates/users/_manageUser.html @@ -0,0 +1,187 @@ +
+
+ + + +
+
+ + +
+ + +
+
+ + + + + + + Toggle Admin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Generate and copy password reset url + + + Delete User + +
diff --git a/lnbits/core/templates/users/_walletDialog.html b/lnbits/core/templates/users/_manageWallet.html similarity index 62% rename from lnbits/core/templates/users/_walletDialog.html rename to lnbits/core/templates/users/_manageWallet.html index 1ff7a74972..4f8c4a7f27 100644 --- a/lnbits/core/templates/users/_walletDialog.html +++ b/lnbits/core/templates/users/_manageWallet.html @@ -1,11 +1,37 @@ - - +
+
+
+ +
+
+ + + + + +
+
+
+
+ + +
+
+

Wallets

- - - - -