feat: max users + extensions env (#3919)

This commit is contained in:
Arc
2026-04-12 22:38:36 +01:00
committed by GitHub
parent 04c9b67997
commit 9e3ab0ef26
9 changed files with 55 additions and 2 deletions
+4
View File
@@ -24,6 +24,10 @@ LOG_ROTATION="100 MB"
LOG_RETENTION="3 months" LOG_RETENTION="3 months"
# for database cleanup commands # for database cleanup commands
# CLEANUP_WALLETS_DAYS=90 # 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 === # === Admin Settings ===
+1
View File
@@ -6,6 +6,7 @@ __pycache__
*$py.class *$py.class
.mypy_cache .mypy_cache
.vscode .vscode
.codex
*-lock.json *-lock.json
.python-version .python-version
+4
View File
@@ -12,6 +12,7 @@ from .extensions import (
drop_extension_db, drop_extension_db,
get_installed_extension, get_installed_extension,
get_installed_extensions, get_installed_extensions,
get_installed_extensions_count,
get_user_active_extensions_ids, get_user_active_extensions_ids,
get_user_extension, get_user_extension,
get_user_extensions, get_user_extensions,
@@ -58,6 +59,7 @@ from .users import (
get_account_by_username, get_account_by_username,
get_account_by_username_or_email, get_account_by_username_or_email,
get_accounts, get_accounts,
get_accounts_count,
get_user, get_user,
get_user_access_control_lists, get_user_access_control_lists,
get_user_from_account, get_user_from_account,
@@ -117,11 +119,13 @@ __all__ = [
"get_account_by_username", "get_account_by_username",
"get_account_by_username_or_email", "get_account_by_username_or_email",
"get_accounts", "get_accounts",
"get_accounts_count",
"get_admin_settings", "get_admin_settings",
"get_db_version", "get_db_version",
"get_db_versions", "get_db_versions",
"get_installed_extension", "get_installed_extension",
"get_installed_extensions", "get_installed_extensions",
"get_installed_extensions_count",
"get_latest_payments_by_extension", "get_latest_payments_by_extension",
"get_payment", "get_payment",
"get_payments", "get_payments",
+7
View File
@@ -90,6 +90,13 @@ async def get_installed_extensions(
return all_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( async def get_user_extension(
user_id: str, extension: str, conn: Connection | None = None user_id: str, extension: str, conn: Connection | None = None
) -> UserExtension | None: ) -> UserExtension | None:
+7
View File
@@ -37,6 +37,13 @@ async def create_account(
return 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: async def update_account(account: Account, conn: Connection | None = None) -> Account:
account.updated_at = datetime.now(timezone.utc) account.updated_at = datetime.now(timezone.utc)
await (conn or db).update("accounts", account) await (conn or db).update("accounts", account)
+12
View File
@@ -9,6 +9,7 @@ from lnbits.core.crud import (
delete_installed_extension, delete_installed_extension,
get_db_version, get_db_version,
get_installed_extension, get_installed_extension,
get_installed_extensions_count,
update_installed_extension_state, update_installed_extension_state,
) )
from lnbits.core.crud.extensions import ( from lnbits.core.crud.extensions import (
@@ -38,6 +39,8 @@ async def install_extension(
if installed_ext and installed_ext.meta: if installed_ext and installed_ext.meta:
ext_info.meta.payments = installed_ext.meta.payments ext_info.meta.payments = installed_ext.meta.payments
await check_extensions_limit(installed_ext)
if not skip_download: if not skip_download:
await ext_info.download_archive() await ext_info.download_archive()
@@ -63,6 +66,15 @@ async def install_extension(
return 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): async def uninstall_extension(ext_id: str):
await stop_extension_background_work(ext_id) await stop_extension_background_work(ext_id)
+12
View File
@@ -23,6 +23,7 @@ from ..crud import (
get_account_by_email, get_account_by_email,
get_account_by_pubkey, get_account_by_pubkey,
get_account_by_username, get_account_by_username,
get_accounts_count,
get_super_settings, get_super_settings,
get_user_extensions, get_user_extensions,
get_user_from_account, get_user_from_account,
@@ -55,6 +56,8 @@ async def create_user_account_no_ckeck(
conn: Connection | None = None, conn: Connection | None = None,
) -> User: ) -> User:
async with db.reuse_conn(conn) if conn else db.connect() as conn: async with db.reuse_conn(conn) if conn else db.connect() as conn:
await check_users_limit(conn)
if account: if account:
account.validate_fields() account.validate_fields()
if account.username and await get_account_by_username( if account.username and await get_account_by_username(
@@ -95,6 +98,15 @@ async def create_user_account_no_ckeck(
return user 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: async def update_user_account(account: Account) -> Account:
account.validate_fields() account.validate_fields()
+6 -2
View File
@@ -98,12 +98,16 @@ async def api_install_extension(data: CreateExtension):
ext_info.clean_extension_files() ext_info.clean_extension_files()
detail = ( detail = (
str(exc) str(exc)
if isinstance(exc, AssertionError) if isinstance(exc, (AssertionError, ValueError))
else f"Failed to install extension '{ext_info.id}'." else f"Failed to install extension '{ext_info.id}'."
f"({ext_info.installed_version})." f"({ext_info.installed_version})."
) )
raise HTTPException( 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, detail=detail,
) from exc ) from exc
+2
View File
@@ -1026,6 +1026,8 @@ class EnvSettings(LNbitsSettings):
cleanup_wallets_days: int = Field(default=90, ge=0) cleanup_wallets_days: int = Field(default=90, ge=0)
funding_source_max_retries: int = Field(default=4, 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 @property
def has_default_extension_path(self) -> bool: def has_default_extension_path(self) -> bool: