Merge branch 'main' into gerty

This commit is contained in:
ben 2022-12-19 13:03:19 +00:00
commit fcaf6967fa
8 changed files with 83 additions and 44 deletions

View file

@ -16,7 +16,7 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
env: env:
VIRTUAL_ENV: ./venv VIRTUAL_ENV: ./venv
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }} PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
run: | run: |
@ -43,9 +43,6 @@ jobs:
with: with:
poetry-version: ${{ matrix.poetry-version }} poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies - name: Install dependencies
env:
VIRTUAL_ENV: ./venv
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
run: | run: |
poetry install poetry install
- name: Run tests - name: Run tests

View file

@ -1,7 +1,7 @@
title: "LNbits docs" title: "LNbits docs"
remote_theme: pmarsceill/just-the-docs remote_theme: pmarsceill/just-the-docs
logo: "/logos/lnbits-full.png" color_scheme: dark
logo: "/logos/lnbits-full--inverse.png"
search_enabled: true search_enabled: true
url: https://legend.lnbits.org url: https://legend.lnbits.org
aux_links: aux_links:

42
docs/guide/admin_ui.md Normal file
View file

@ -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.

View file

@ -6,7 +6,7 @@ from uuid import uuid4
from lnbits import bolt11 from lnbits import bolt11
from lnbits.db import COCKROACH, POSTGRES, Connection 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 . import db
from .models import BalanceCheck, Payment, User, Wallet from .models import BalanceCheck, Payment, User, Wallet
@ -579,7 +579,7 @@ async def delete_admin_settings():
await db.execute("DELETE FROM 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),)) await db.execute(f"UPDATE settings SET editable_settings = ?", (json.dumps(data),))

View file

@ -23,7 +23,7 @@ from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.settings import ( from lnbits.settings import (
FAKE_WALLET, FAKE_WALLET,
EditableSetings, EditableSettings,
get_wallet_class, get_wallet_class,
readonly_variables, readonly_variables,
send_admin_user_to_saas, 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: if not account.wallets or len(account.wallets) == 0:
await create_wallet(user_id=account.id) 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()) return await create_admin_settings(account.id, editable_settings.dict())

View file

@ -9,7 +9,7 @@ from lnbits.core.models import User
from lnbits.core.services import update_cached_settings, update_wallet_balance from lnbits.core.services import update_cached_settings, update_wallet_balance
from lnbits.decorators import check_admin, check_super_user from lnbits.decorators import check_admin, check_super_user
from lnbits.server import server_restart from lnbits.server import server_restart
from lnbits.settings import AdminSettings, EditableSetings from lnbits.settings import AdminSettings, EditableSettings
from .. import core_app from .. import core_app
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings 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, status_code=HTTPStatus.OK,
dependencies=[Depends(check_admin)], dependencies=[Depends(check_admin)],
) )
async def api_update_settings(data: EditableSetings): async def api_update_settings(data: EditableSettings):
await update_admin_settings(data) await update_admin_settings(data)
update_cached_settings(dict(data)) update_cached_settings(dict(data))
return {"status": "Success"} return {"status": "Success"}

View file

@ -8,7 +8,7 @@ from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
from lnbits.core.models import User 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 lnbits.extensions.satspay.helpers import public_charge
from . import satspay_ext, satspay_renderer from . import satspay_ext, satspay_renderer
@ -18,7 +18,7 @@ templates = Jinja2Templates(directory="templates")
@satspay_ext.get("/", response_class=HTMLResponse) @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( return satspay_renderer().TemplateResponse(
"satspay/index.html", "satspay/index.html",
{"request": request, "user": user.dict(), "admin": user.admin}, {"request": request, "user": user.dict(), "admin": user.admin},

View file

@ -22,7 +22,7 @@ def list_parse_fallback(v):
return [] return []
class LNbitsSetings(BaseSettings): class LNbitsSettings(BaseSettings):
def validate(cls, val): def validate(cls, val):
if type(val) == str: if type(val) == str:
val = val.split(",") if val else [] val = val.split(",") if val else []
@ -35,14 +35,14 @@ class LNbitsSetings(BaseSettings):
json_loads = list_parse_fallback json_loads = list_parse_fallback
class UsersSetings(LNbitsSetings): class UsersSettings(LNbitsSettings):
lnbits_admin_users: List[str] = Field(default=[]) lnbits_admin_users: List[str] = Field(default=[])
lnbits_allowed_users: List[str] = Field(default=[]) lnbits_allowed_users: List[str] = Field(default=[])
lnbits_admin_extensions: List[str] = Field(default=[]) lnbits_admin_extensions: List[str] = Field(default=[])
lnbits_disabled_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_title: str = Field(default="LNbits")
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet") lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
lnbits_site_description: str = Field(default=None) lnbits_site_description: str = Field(default=None)
@ -58,7 +58,7 @@ class ThemesSetings(LNbitsSetings):
lnbits_ad_space_enabled: bool = Field(default=False) lnbits_ad_space_enabled: bool = Field(default=False)
class OpsSetings(LNbitsSetings): class OpsSettings(LNbitsSettings):
lnbits_force_https: bool = Field(default=False) lnbits_force_https: bool = Field(default=False)
lnbits_reserve_fee_min: int = Field(default=2000) lnbits_reserve_fee_min: int = Field(default=2000)
lnbits_reserve_fee_percent: float = Field(default=1.0) lnbits_reserve_fee_percent: float = Field(default=1.0)
@ -67,29 +67,29 @@ class OpsSetings(LNbitsSetings):
lnbits_denomination: str = Field(default="sats") lnbits_denomination: str = Field(default="sats")
class FakeWalletFundingSource(LNbitsSetings): class FakeWalletFundingSource(LNbitsSettings):
fake_wallet_secret: str = Field(default="ToTheMoon1") fake_wallet_secret: str = Field(default="ToTheMoon1")
class LNbitsFundingSource(LNbitsSetings): class LNbitsFundingSource(LNbitsSettings):
lnbits_endpoint: str = Field(default="https://legend.lnbits.com") lnbits_endpoint: str = Field(default="https://legend.lnbits.com")
lnbits_key: Optional[str] = Field(default=None) lnbits_key: Optional[str] = Field(default=None)
class ClicheFundingSource(LNbitsSetings): class ClicheFundingSource(LNbitsSettings):
cliche_endpoint: Optional[str] = Field(default=None) cliche_endpoint: Optional[str] = Field(default=None)
class CoreLightningFundingSource(LNbitsSetings): class CoreLightningFundingSource(LNbitsSettings):
corelightning_rpc: Optional[str] = Field(default=None) corelightning_rpc: Optional[str] = Field(default=None)
class EclairFundingSource(LNbitsSetings): class EclairFundingSource(LNbitsSettings):
eclair_url: Optional[str] = Field(default=None) eclair_url: Optional[str] = Field(default=None)
eclair_pass: 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_endpoint: Optional[str] = Field(default=None)
lnd_rest_cert: Optional[str] = Field(default=None) lnd_rest_cert: Optional[str] = Field(default=None)
lnd_rest_macaroon: 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) lnd_invoice_macaroon: Optional[str] = Field(default=None)
class LndGrpcFundingSource(LNbitsSetings): class LndGrpcFundingSource(LNbitsSettings):
lnd_grpc_endpoint: Optional[str] = Field(default=None) lnd_grpc_endpoint: Optional[str] = Field(default=None)
lnd_grpc_cert: Optional[str] = Field(default=None) lnd_grpc_cert: Optional[str] = Field(default=None)
lnd_grpc_port: Optional[int] = 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) 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_endpoint: Optional[str] = Field(default=None)
lnpay_api_key: Optional[str] = Field(default=None) lnpay_api_key: Optional[str] = Field(default=None)
lnpay_wallet_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_api_endpoint: Optional[str] = Field(default=None)
lntxbot_key: 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_api_endpoint: Optional[str] = Field(default=None)
opennode_key: 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_url: Optional[str] = Field(default=None)
spark_token: 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_endpoint: Optional[str] = Field(default=None)
lntips_api_key: Optional[str] = Field(default=None) lntips_api_key: Optional[str] = Field(default=None)
lntips_admin_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 # todo: must be extracted
class BoltzExtensionSettings(LNbitsSetings): class BoltzExtensionSettings(LNbitsSettings):
boltz_network: str = Field(default="main") boltz_network: str = Field(default="main")
boltz_url: str = Field(default="https://boltz.exchange/api") boltz_url: str = Field(default="https://boltz.exchange/api")
boltz_mempool_space_url: str = Field(default="https://mempool.space") boltz_mempool_space_url: str = Field(default="https://mempool.space")
boltz_mempool_space_url_ws: str = Field(default="wss://mempool.space") boltz_mempool_space_url_ws: str = Field(default="wss://mempool.space")
class FundingSourcesSetings( class FundingSourcesSettings(
FakeWalletFundingSource, FakeWalletFundingSource,
LNbitsFundingSource, LNbitsFundingSource,
ClicheFundingSource, ClicheFundingSource,
@ -162,11 +162,11 @@ class FundingSourcesSetings(
lnbits_backend_wallet_class: str = Field(default="VoidWallet") lnbits_backend_wallet_class: str = Field(default="VoidWallet")
class EditableSetings( class EditableSettings(
UsersSetings, UsersSettings,
ThemesSetings, ThemesSettings,
OpsSetings, OpsSettings,
FundingSourcesSetings, FundingSourcesSettings,
BoltzExtensionSettings, BoltzExtensionSettings,
): ):
@validator( @validator(
@ -187,7 +187,7 @@ class EditableSetings(
) )
class EnvSettings(LNbitsSetings): class EnvSettings(LNbitsSettings):
debug: bool = Field(default=False) debug: bool = Field(default=False)
host: str = Field(default="127.0.0.1") host: str = Field(default="127.0.0.1")
port: int = Field(default=5000) port: int = Field(default=5000)
@ -197,18 +197,18 @@ class EnvSettings(LNbitsSetings):
super_user: str = Field(default="") super_user: str = Field(default="")
class SaaSSettings(LNbitsSetings): class SaaSSettings(LNbitsSettings):
lnbits_saas_callback: Optional[str] = Field(default=None) lnbits_saas_callback: Optional[str] = Field(default=None)
lnbits_saas_secret: Optional[str] = Field(default=None) lnbits_saas_secret: Optional[str] = Field(default=None)
lnbits_saas_instance_id: 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_data_folder: str = Field(default="./data")
lnbits_database_url: str = Field(default=None) lnbits_database_url: str = Field(default=None)
class SuperUserSettings(LNbitsSetings): class SuperUserSettings(LNbitsSettings):
lnbits_allowed_funding_sources: List[str] = Field( lnbits_allowed_funding_sources: List[str] = Field(
default=[ default=[
"VoidWallet", "VoidWallet",
@ -242,18 +242,18 @@ class ReadOnlySettings(
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")] return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
class Settings(EditableSetings, ReadOnlySettings): class Settings(EditableSettings, ReadOnlySettings):
@classmethod @classmethod
def from_row(cls, row: Row) -> "Settings": def from_row(cls, row: Row) -> "Settings":
data = dict(row) data = dict(row)
return cls(**data) return cls(**data)
class SuperSettings(EditableSetings): class SuperSettings(EditableSettings):
super_user: str super_user: str
class AdminSettings(EditableSetings): class AdminSettings(EditableSettings):
super_user: bool super_user: bool
lnbits_allowed_funding_sources: Optional[List[str]] lnbits_allowed_funding_sources: Optional[List[str]]