diff --git a/.env.example b/.env.example index aa06f85e..c749ef23 100644 --- a/.env.example +++ b/.env.example @@ -141,6 +141,9 @@ LNBITS_ADMIN_USERS="" # Extensions only admin can access LNBITS_ADMIN_EXTENSIONS="ngrok, admin" +# Start LNbits core only. The extensions are not loaded. +# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true + # Disable account creation for new users # LNBITS_ALLOW_NEW_ACCOUNTS=false diff --git a/lnbits/app.py b/lnbits/app.py index 0a753409..991291fe 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -10,7 +10,7 @@ import traceback from hashlib import sha256 from http import HTTPStatus from pathlib import Path -from typing import Callable, List +from typing import Callable, List, Optional from fastapi import FastAPI, HTTPException, Request from fastapi.exceptions import RequestValidationError @@ -35,7 +35,7 @@ from lnbits.tasks import cancel_all_tasks, create_permanent_task from lnbits.utils.cache import cache from lnbits.wallets import get_wallet_class, set_wallet_class -from .commands import db_versions, load_disabled_extension_list, migrate_databases +from .commands import db_versions, migrate_databases from .core import init_core_routers from .core.db import core_app_extra from .core.services import check_admin_settings, check_webpush_settings @@ -112,7 +112,6 @@ def create_app() -> FastAPI: add_ratelimit_middleware(app) register_startup(app) - register_routes(app) register_async_tasks(app) register_exception_handlers(app) register_shutdown(app) @@ -189,8 +188,7 @@ async def check_installed_extensions(app: FastAPI): persist state. Zips that are missing will be re-downloaded. """ shutil.rmtree(os.path.join("lnbits", "upgrades"), True) - await load_disabled_extension_list() - installed_extensions = await build_all_installed_extensions_list() + installed_extensions = await build_all_installed_extensions_list(False) for ext in installed_extensions: try: @@ -212,7 +210,9 @@ async def check_installed_extensions(app: FastAPI): logger.info(f"{ext.id} ({ext.installed_version})") -async def build_all_installed_extensions_list() -> List[InstallableExtension]: +async def build_all_installed_extensions_list( + include_deactivated: Optional[bool] = True, +) -> List[InstallableExtension]: """ Returns a list of all the installed extensions plus the extensions that MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL). @@ -237,7 +237,17 @@ async def build_all_installed_extensions_list() -> List[InstallableExtension]: ) installed_extensions.append(ext_info) - return installed_extensions + if include_deactivated: + return installed_extensions + + if settings.lnbits_extensions_deactivate_all: + return [] + + return [ + e + for e in installed_extensions + if e.id not in settings.lnbits_deactivated_extensions + ] def check_installed_extension_files(ext: InstallableExtension) -> bool: @@ -273,7 +283,7 @@ def register_routes(app: FastAPI) -> None: """Register FastAPI routes / LNbits extensions.""" init_core_routers(app) - for ext in get_valid_extensions(): + for ext in get_valid_extensions(False): try: register_ext_routes(app, ext) except Exception as e: @@ -383,6 +393,9 @@ def register_startup(app: FastAPI): # check extensions after restart await check_installed_extensions(app) + # register core and extension routes + register_routes(app) + if settings.lnbits_admin_ui: initialize_server_logger() diff --git a/lnbits/commands.py b/lnbits/commands.py index 269b5c3e..5d8d8924 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -136,7 +136,11 @@ async def migrate_databases(): core_version = current_versions.get("core", 0) await run_migration(conn, core_migrations, "core", core_version) - for ext in get_valid_extensions(): + # here is the first place we can be sure that the + # `installed_extensions` table has been created + await load_disabled_extension_list() + + for ext in get_valid_extensions(False): current_version = current_versions.get(ext.code, 0) try: await migrate_extension_database(ext, current_version) diff --git a/lnbits/core/helpers.py b/lnbits/core/helpers.py index 741587d1..a8979798 100644 --- a/lnbits/core/helpers.py +++ b/lnbits/core/helpers.py @@ -54,8 +54,6 @@ async def stop_extension_background_work( """ Stop background work for extension (like asyncio.Tasks, WebSockets, etc). Extensions SHOULD expose a DELETE enpoint at the root level of their API. - This function tries first to call the endpoint using `http` - and if it fails it tries using `https`. """ async with httpx.AsyncClient() as client: try: diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 1355f208..b3b56ae1 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -1,4 +1,5 @@ import asyncio +import sys from http import HTTPStatus from pathlib import Path from typing import Annotated, List, Optional, Union @@ -11,7 +12,7 @@ from fastapi.routing import APIRouter from loguru import logger from pydantic.types import UUID4 -from lnbits.core.db import db +from lnbits.core.db import core_app_extra, db from lnbits.core.helpers import to_valid_user_id from lnbits.core.models import User from lnbits.decorators import check_admin, check_user_exists @@ -74,9 +75,6 @@ async def extensions_install( ): await toggle_extension(enable, disable, user.id) - # Update user as his extensions have been updated - if enable or disable: - user = await get_user(user.id) # type: ignore try: installed_exts: List["InstallableExtension"] = await get_installed_extensions() installed_exts_ids = [e.id for e in installed_exts] @@ -103,20 +101,28 @@ async def extensions_install( try: ext_id = activate or deactivate + all_extensions = get_valid_extensions() + ext = next((e for e in all_extensions if e.code == ext_id), None) if ext_id and user.admin: if deactivate and deactivate not in settings.lnbits_deactivated_extensions: settings.lnbits_deactivated_extensions += [deactivate] elif activate: + # if extension never loaded (was deactivated on server startup) + if ext_id not in sys.modules.keys(): + # run extension start-up routine + core_app_extra.register_new_ext_routes(ext) + settings.lnbits_deactivated_extensions = list( filter( lambda e: e != activate, settings.lnbits_deactivated_extensions ) ) + await update_installed_extension_state( ext_id=ext_id, active=activate is not None ) - all_extensions = list(map(lambda e: e.code, get_valid_extensions())) + all_ext_ids = list(map(lambda e: e.code, all_extensions)) inactive_extensions = await get_inactive_extensions() db_version = await get_dbversions() extensions = list( @@ -131,7 +137,7 @@ async def extensions_install( "dependencies": ext.dependencies, "isInstalled": ext.id in installed_exts_ids, "hasDatabaseTables": ext.id in db_version, - "isAvailable": ext.id in all_extensions, + "isAvailable": ext.id in all_ext_ids, "isAdminOnly": ext.id in settings.lnbits_admin_extensions, "isActive": ext.id not in inactive_extensions, "latestRelease": ( diff --git a/lnbits/extension_manager.py b/lnbits/extension_manager.py index c4e1739f..a4ac8f27 100644 --- a/lnbits/extension_manager.py +++ b/lnbits/extension_manager.py @@ -604,11 +604,23 @@ class CreateExtension(BaseModel): source_repo: str -def get_valid_extensions() -> List[Extension]: - return [ +def get_valid_extensions(include_deactivated: Optional[bool] = True) -> List[Extension]: + valid_extensions = [ extension for extension in ExtensionManager().extensions if extension.is_valid ] + if include_deactivated: + return valid_extensions + + if settings.lnbits_extensions_deactivate_all: + return [] + + return [ + e + for e in valid_extensions + if e.code not in settings.lnbits_deactivated_extensions + ] + def version_parse(v: str): """ diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 0aaca145..356f97f0 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -71,11 +71,7 @@ def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templa settings.lnbits_node_ui and get_node_class() is not None ) t.env.globals["LNBITS_NODE_UI_AVAILABLE"] = get_node_class() is not None - t.env.globals["EXTENSIONS"] = [ - e - for e in get_valid_extensions() - if e.code not in settings.lnbits_deactivated_extensions - ] + t.env.globals["EXTENSIONS"] = get_valid_extensions(False) if settings.lnbits_custom_logo: t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo diff --git a/lnbits/settings.py b/lnbits/settings.py index 57370ce4..e48b9f0d 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -347,6 +347,7 @@ class EnvSettings(LNbitsSettings): log_rotation: str = Field(default="100 MB") log_retention: str = Field(default="3 months") server_startup_time: int = Field(default=time()) + lnbits_extensions_deactivate_all: bool = Field(default=False) @property def has_default_extension_path(self) -> bool: