From 9e3ab0ef2644b2cf3f3f4acf9e5c33f521db08f2 Mon Sep 17 00:00:00 2001 From: Arc <33088785+arcbtc@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:38:36 +0100 Subject: [PATCH] feat: max users + extensions env (#3919) --- .env.example | 4 ++++ .gitignore | 1 + lnbits/core/crud/__init__.py | 4 ++++ lnbits/core/crud/extensions.py | 7 +++++++ lnbits/core/crud/users.py | 7 +++++++ lnbits/core/services/extensions.py | 12 ++++++++++++ lnbits/core/services/users.py | 12 ++++++++++++ lnbits/core/views/extension_api.py | 8 ++++++-- lnbits/settings.py | 2 ++ 9 files changed, 55 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 9d59f19df..e74b8d0d1 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,10 @@ LOG_ROTATION="100 MB" LOG_RETENTION="3 months" # for database cleanup commands # CLEANUP_WALLETS_DAYS=90 +# Hard limit for total created users. Set to 0 to disable the limit. +# LNBITS_MAX_USERS=0 +# Hard limit for total installed extensions. Set to 0 to disable the limit. +# LNBITS_MAX_EXTENSIONS=0 # === Admin Settings === diff --git a/.gitignore b/.gitignore index 7790682cc..2b2c7c876 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__ *$py.class .mypy_cache .vscode +.codex *-lock.json .python-version diff --git a/lnbits/core/crud/__init__.py b/lnbits/core/crud/__init__.py index 2108d23d5..f98c01c8c 100644 --- a/lnbits/core/crud/__init__.py +++ b/lnbits/core/crud/__init__.py @@ -12,6 +12,7 @@ from .extensions import ( drop_extension_db, get_installed_extension, get_installed_extensions, + get_installed_extensions_count, get_user_active_extensions_ids, get_user_extension, get_user_extensions, @@ -58,6 +59,7 @@ from .users import ( get_account_by_username, get_account_by_username_or_email, get_accounts, + get_accounts_count, get_user, get_user_access_control_lists, get_user_from_account, @@ -117,11 +119,13 @@ __all__ = [ "get_account_by_username", "get_account_by_username_or_email", "get_accounts", + "get_accounts_count", "get_admin_settings", "get_db_version", "get_db_versions", "get_installed_extension", "get_installed_extensions", + "get_installed_extensions_count", "get_latest_payments_by_extension", "get_payment", "get_payments", diff --git a/lnbits/core/crud/extensions.py b/lnbits/core/crud/extensions.py index e4fbd7e80..497183fc5 100644 --- a/lnbits/core/crud/extensions.py +++ b/lnbits/core/crud/extensions.py @@ -90,6 +90,13 @@ async def get_installed_extensions( return all_extensions +async def get_installed_extensions_count(conn: Connection | None = None) -> int: + row: dict | None = await (conn or db).fetchone( + "SELECT COUNT(*) as count FROM installed_extensions" + ) + return int(row["count"]) if row else 0 + + async def get_user_extension( user_id: str, extension: str, conn: Connection | None = None ) -> UserExtension | None: diff --git a/lnbits/core/crud/users.py b/lnbits/core/crud/users.py index e85de36a2..e5001e0ec 100644 --- a/lnbits/core/crud/users.py +++ b/lnbits/core/crud/users.py @@ -37,6 +37,13 @@ async def create_account( return account +async def get_accounts_count(conn: Connection | None = None) -> int: + row: dict | None = await (conn or db).fetchone( + "SELECT COUNT(*) as count FROM accounts" + ) + return int(row["count"]) if row else 0 + + async def update_account(account: Account, conn: Connection | None = None) -> Account: account.updated_at = datetime.now(timezone.utc) await (conn or db).update("accounts", account) diff --git a/lnbits/core/services/extensions.py b/lnbits/core/services/extensions.py index 8743efbc2..411ed6cb5 100644 --- a/lnbits/core/services/extensions.py +++ b/lnbits/core/services/extensions.py @@ -9,6 +9,7 @@ from lnbits.core.crud import ( delete_installed_extension, get_db_version, get_installed_extension, + get_installed_extensions_count, update_installed_extension_state, ) from lnbits.core.crud.extensions import ( @@ -38,6 +39,8 @@ async def install_extension( if installed_ext and installed_ext.meta: ext_info.meta.payments = installed_ext.meta.payments + await check_extensions_limit(installed_ext) + if not skip_download: await ext_info.download_archive() @@ -63,6 +66,15 @@ async def install_extension( return extension +async def check_extensions_limit(installed_ext: InstallableExtension | None = None): + if settings.lnbits_max_extensions == 0 or installed_ext: + return + + extensions_count = await get_installed_extensions_count() + if extensions_count >= settings.lnbits_max_extensions: + raise ValueError("Max amount of extensions have been installed") + + async def uninstall_extension(ext_id: str): await stop_extension_background_work(ext_id) diff --git a/lnbits/core/services/users.py b/lnbits/core/services/users.py index 626709c1e..049583911 100644 --- a/lnbits/core/services/users.py +++ b/lnbits/core/services/users.py @@ -23,6 +23,7 @@ from ..crud import ( get_account_by_email, get_account_by_pubkey, get_account_by_username, + get_accounts_count, get_super_settings, get_user_extensions, get_user_from_account, @@ -55,6 +56,8 @@ async def create_user_account_no_ckeck( conn: Connection | None = None, ) -> User: async with db.reuse_conn(conn) if conn else db.connect() as conn: + await check_users_limit(conn) + if account: account.validate_fields() if account.username and await get_account_by_username( @@ -95,6 +98,15 @@ async def create_user_account_no_ckeck( return user +async def check_users_limit(conn: Connection | None = None): + if settings.lnbits_max_users == 0: + return + + users_count = await get_accounts_count(conn=conn) + if users_count >= settings.lnbits_max_users: + raise ValueError("Max amount of users have been created") + + async def update_user_account(account: Account) -> Account: account.validate_fields() diff --git a/lnbits/core/views/extension_api.py b/lnbits/core/views/extension_api.py index 3192b832c..46bf3b080 100644 --- a/lnbits/core/views/extension_api.py +++ b/lnbits/core/views/extension_api.py @@ -98,12 +98,16 @@ async def api_install_extension(data: CreateExtension): ext_info.clean_extension_files() detail = ( str(exc) - if isinstance(exc, AssertionError) + if isinstance(exc, (AssertionError, ValueError)) else f"Failed to install extension '{ext_info.id}'." f"({ext_info.installed_version})." ) raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + status_code=( + HTTPStatus.BAD_REQUEST + if isinstance(exc, (AssertionError, ValueError)) + else HTTPStatus.INTERNAL_SERVER_ERROR + ), detail=detail, ) from exc diff --git a/lnbits/settings.py b/lnbits/settings.py index c38eaa23e..2e110d991 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -1026,6 +1026,8 @@ class EnvSettings(LNbitsSettings): cleanup_wallets_days: int = Field(default=90, ge=0) funding_source_max_retries: int = Field(default=4, ge=0) + lnbits_max_users: int = Field(default=0, ge=0) + lnbits_max_extensions: int = Field(default=0, ge=0) @property def has_default_extension_path(self) -> bool: