diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5d368fbb..487411ed 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install dependencies - env: + env: VIRTUAL_ENV: ./venv PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }} run: | @@ -43,9 +43,6 @@ jobs: with: poetry-version: ${{ matrix.poetry-version }} - name: Install dependencies - env: - VIRTUAL_ENV: ./venv - PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }} run: | poetry install - name: Run tests diff --git a/docs/_config.yml b/docs/_config.yml index 74e65187..6c3d6512 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,7 +1,7 @@ title: "LNbits docs" - remote_theme: pmarsceill/just-the-docs -logo: "/logos/lnbits-full.png" +color_scheme: dark +logo: "/logos/lnbits-full--inverse.png" search_enabled: true url: https://legend.lnbits.org aux_links: diff --git a/docs/guide/admin_ui.md b/docs/guide/admin_ui.md new file mode 100644 index 00000000..1248d3f3 --- /dev/null +++ b/docs/guide/admin_ui.md @@ -0,0 +1,42 @@ +--- +layout: default +title: Admin UI +nav_order: 4 +--- + + +Admin UI +======== +The LNbits Admin UI lets you change LNbits settings via the LNbits frontend. +It is disabled by default and the first time you set the enviroment variable LNBITS_ADMIN_UI=true +the settings are initialized and saved to the database and will be used from there as long the UI is enabled. +From there on the settings from the database are used. + + +Super User +========== +With the Admin UI we introduced the super user, it is created with the initialisation of the Admin UI and will be shown with a success message in the server logs. +The super user has access to the server and can change settings that may crash the server and make it unresponsive via the frontend and api, like changing funding sources. + +Also only the super user can brrrr satoshis to different wallets. + +The super user is only stored inside the settings table of the database and after the settings are "reset to defaults" and a restart happened, +a new super user is created. + +The super user is never sent over the api and the frontend only receives a bool if you are super user or not. + +We also added a decorator for the API routes to check for super user. + +There is also the possibility of posting the super user via webhook to another service when it is created. you can look it up here https://github.com/lnbits/lnbits/blob/main/lnbits/settings.py `class SaaSSettings` + + +Admin Users +=========== +enviroment variable: LNBITS_ADMIN_USERS, comma-seperated list of user ids +Admin Users can change settings in the admin ui aswell, with the exception of funding source settings, because they require e server restart and could potentially make the server inaccessable. Also they have access to all the extension defined in LNBITS_ADMIN_EXTENSIONS. + + +Allowed Users +============= +enviroment variable: LNBITS_ALLOWED_USERS, comma-seperated list of user ids +By defining this users, LNbits will no longer be useable by the public, only defined users and admins can then access the LNbits frontend. diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index f2c7da61..1c8c71ad 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -6,7 +6,7 @@ from uuid import uuid4 from lnbits import bolt11 from lnbits.db import COCKROACH, POSTGRES, Connection -from lnbits.settings import AdminSettings, EditableSetings, SuperSettings, settings +from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings from . import db from .models import BalanceCheck, Payment, User, Wallet @@ -579,7 +579,7 @@ async def delete_admin_settings(): await db.execute("DELETE FROM settings") -async def update_admin_settings(data: EditableSetings): +async def update_admin_settings(data: EditableSettings): await db.execute(f"UPDATE settings SET editable_settings = ?", (json.dumps(data),)) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 90910524..336d2665 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -23,7 +23,7 @@ from lnbits.helpers import url_for, urlsafe_short_hash from lnbits.requestvars import g from lnbits.settings import ( FAKE_WALLET, - EditableSetings, + EditableSettings, get_wallet_class, readonly_variables, send_admin_user_to_saas, @@ -474,7 +474,7 @@ async def init_admin_settings(super_user: str = None): if not account.wallets or len(account.wallets) == 0: await create_wallet(user_id=account.id) - editable_settings = EditableSetings.from_dict(settings.dict()) + editable_settings = EditableSettings.from_dict(settings.dict()) return await create_admin_settings(account.id, editable_settings.dict()) diff --git a/lnbits/core/views/admin_api.py b/lnbits/core/views/admin_api.py index 7c057adc..20eaeea3 100644 --- a/lnbits/core/views/admin_api.py +++ b/lnbits/core/views/admin_api.py @@ -9,7 +9,7 @@ from lnbits.core.models import User from lnbits.core.services import update_cached_settings, update_wallet_balance from lnbits.decorators import check_admin, check_super_user from lnbits.server import server_restart -from lnbits.settings import AdminSettings, EditableSetings +from lnbits.settings import AdminSettings, EditableSettings from .. import core_app from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings @@ -28,7 +28,7 @@ async def api_get_settings( status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)], ) -async def api_update_settings(data: EditableSetings): +async def api_update_settings(data: EditableSettings): await update_admin_settings(data) update_cached_settings(dict(data)) return {"status": "Success"} diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py index b0f89025..90f8a6b9 100644 --- a/lnbits/extensions/satspay/views.py +++ b/lnbits/extensions/satspay/views.py @@ -8,7 +8,7 @@ from starlette.requests import Request from starlette.responses import HTMLResponse from lnbits.core.models import User -from lnbits.decorators import check_admin +from lnbits.decorators import check_user_exists from lnbits.extensions.satspay.helpers import public_charge from . import satspay_ext, satspay_renderer @@ -18,7 +18,7 @@ templates = Jinja2Templates(directory="templates") @satspay_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_admin)): +async def index(request: Request, user: User = Depends(check_user_exists)): return satspay_renderer().TemplateResponse( "satspay/index.html", {"request": request, "user": user.dict(), "admin": user.admin}, diff --git a/lnbits/settings.py b/lnbits/settings.py index d46a061d..e75f5b62 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -22,7 +22,7 @@ def list_parse_fallback(v): return [] -class LNbitsSetings(BaseSettings): +class LNbitsSettings(BaseSettings): def validate(cls, val): if type(val) == str: val = val.split(",") if val else [] @@ -35,14 +35,14 @@ class LNbitsSetings(BaseSettings): json_loads = list_parse_fallback -class UsersSetings(LNbitsSetings): +class UsersSettings(LNbitsSettings): lnbits_admin_users: List[str] = Field(default=[]) lnbits_allowed_users: List[str] = Field(default=[]) lnbits_admin_extensions: List[str] = Field(default=[]) lnbits_disabled_extensions: List[str] = Field(default=[]) -class ThemesSetings(LNbitsSetings): +class ThemesSettings(LNbitsSettings): lnbits_site_title: str = Field(default="LNbits") lnbits_site_tagline: str = Field(default="free and open-source lightning wallet") lnbits_site_description: str = Field(default=None) @@ -58,7 +58,7 @@ class ThemesSetings(LNbitsSetings): lnbits_ad_space_enabled: bool = Field(default=False) -class OpsSetings(LNbitsSetings): +class OpsSettings(LNbitsSettings): lnbits_force_https: bool = Field(default=False) lnbits_reserve_fee_min: int = Field(default=2000) lnbits_reserve_fee_percent: float = Field(default=1.0) @@ -67,29 +67,29 @@ class OpsSetings(LNbitsSetings): lnbits_denomination: str = Field(default="sats") -class FakeWalletFundingSource(LNbitsSetings): +class FakeWalletFundingSource(LNbitsSettings): fake_wallet_secret: str = Field(default="ToTheMoon1") -class LNbitsFundingSource(LNbitsSetings): +class LNbitsFundingSource(LNbitsSettings): lnbits_endpoint: str = Field(default="https://legend.lnbits.com") lnbits_key: Optional[str] = Field(default=None) -class ClicheFundingSource(LNbitsSetings): +class ClicheFundingSource(LNbitsSettings): cliche_endpoint: Optional[str] = Field(default=None) -class CoreLightningFundingSource(LNbitsSetings): +class CoreLightningFundingSource(LNbitsSettings): corelightning_rpc: Optional[str] = Field(default=None) -class EclairFundingSource(LNbitsSetings): +class EclairFundingSource(LNbitsSettings): eclair_url: Optional[str] = Field(default=None) eclair_pass: Optional[str] = Field(default=None) -class LndRestFundingSource(LNbitsSetings): +class LndRestFundingSource(LNbitsSettings): lnd_rest_endpoint: Optional[str] = Field(default=None) lnd_rest_cert: Optional[str] = Field(default=None) lnd_rest_macaroon: Optional[str] = Field(default=None) @@ -99,7 +99,7 @@ class LndRestFundingSource(LNbitsSetings): lnd_invoice_macaroon: Optional[str] = Field(default=None) -class LndGrpcFundingSource(LNbitsSetings): +class LndGrpcFundingSource(LNbitsSettings): lnd_grpc_endpoint: Optional[str] = Field(default=None) lnd_grpc_cert: Optional[str] = Field(default=None) lnd_grpc_port: Optional[int] = Field(default=None) @@ -109,28 +109,28 @@ class LndGrpcFundingSource(LNbitsSetings): lnd_grpc_macaroon_encrypted: Optional[str] = Field(default=None) -class LnPayFundingSource(LNbitsSetings): +class LnPayFundingSource(LNbitsSettings): lnpay_api_endpoint: Optional[str] = Field(default=None) lnpay_api_key: Optional[str] = Field(default=None) lnpay_wallet_key: Optional[str] = Field(default=None) -class LnTxtBotFundingSource(LNbitsSetings): +class LnTxtBotFundingSource(LNbitsSettings): lntxbot_api_endpoint: Optional[str] = Field(default=None) lntxbot_key: Optional[str] = Field(default=None) -class OpenNodeFundingSource(LNbitsSetings): +class OpenNodeFundingSource(LNbitsSettings): opennode_api_endpoint: Optional[str] = Field(default=None) opennode_key: Optional[str] = Field(default=None) -class SparkFundingSource(LNbitsSetings): +class SparkFundingSource(LNbitsSettings): spark_url: Optional[str] = Field(default=None) spark_token: Optional[str] = Field(default=None) -class LnTipsFundingSource(LNbitsSetings): +class LnTipsFundingSource(LNbitsSettings): lntips_api_endpoint: Optional[str] = Field(default=None) lntips_api_key: Optional[str] = Field(default=None) lntips_admin_key: Optional[str] = Field(default=None) @@ -138,14 +138,14 @@ class LnTipsFundingSource(LNbitsSetings): # todo: must be extracted -class BoltzExtensionSettings(LNbitsSetings): +class BoltzExtensionSettings(LNbitsSettings): boltz_network: str = Field(default="main") boltz_url: str = Field(default="https://boltz.exchange/api") boltz_mempool_space_url: str = Field(default="https://mempool.space") boltz_mempool_space_url_ws: str = Field(default="wss://mempool.space") -class FundingSourcesSetings( +class FundingSourcesSettings( FakeWalletFundingSource, LNbitsFundingSource, ClicheFundingSource, @@ -162,11 +162,11 @@ class FundingSourcesSetings( lnbits_backend_wallet_class: str = Field(default="VoidWallet") -class EditableSetings( - UsersSetings, - ThemesSetings, - OpsSetings, - FundingSourcesSetings, +class EditableSettings( + UsersSettings, + ThemesSettings, + OpsSettings, + FundingSourcesSettings, BoltzExtensionSettings, ): @validator( @@ -187,7 +187,7 @@ class EditableSetings( ) -class EnvSettings(LNbitsSetings): +class EnvSettings(LNbitsSettings): debug: bool = Field(default=False) host: str = Field(default="127.0.0.1") port: int = Field(default=5000) @@ -197,18 +197,18 @@ class EnvSettings(LNbitsSetings): super_user: str = Field(default="") -class SaaSSettings(LNbitsSetings): +class SaaSSettings(LNbitsSettings): lnbits_saas_callback: Optional[str] = Field(default=None) lnbits_saas_secret: Optional[str] = Field(default=None) lnbits_saas_instance_id: Optional[str] = Field(default=None) -class PersistenceSettings(LNbitsSetings): +class PersistenceSettings(LNbitsSettings): lnbits_data_folder: str = Field(default="./data") lnbits_database_url: str = Field(default=None) -class SuperUserSettings(LNbitsSetings): +class SuperUserSettings(LNbitsSettings): lnbits_allowed_funding_sources: List[str] = Field( default=[ "VoidWallet", @@ -242,18 +242,18 @@ class ReadOnlySettings( return [f for f in inspect.signature(cls).parameters if not f.startswith("_")] -class Settings(EditableSetings, ReadOnlySettings): +class Settings(EditableSettings, ReadOnlySettings): @classmethod def from_row(cls, row: Row) -> "Settings": data = dict(row) return cls(**data) -class SuperSettings(EditableSetings): +class SuperSettings(EditableSettings): super_user: str -class AdminSettings(EditableSetings): +class AdminSettings(EditableSettings): super_user: bool lnbits_allowed_funding_sources: Optional[List[str]]