From f9de542361a6dd0a59135b39d2a3bb1ba521f069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 3 Oct 2022 16:46:46 +0200 Subject: [PATCH] use new settings and remove unused amdin extension stuff --- lnbits/commands.py | 40 +---- lnbits/core/crud.py | 8 +- lnbits/core/services.py | 6 +- lnbits/core/views/api.py | 8 +- lnbits/core/views/generic.py | 36 ++--- lnbits/db.py | 16 +- lnbits/decorators.py | 51 +++--- lnbits/helpers.py | 56 +++---- lnbits/server.py | 50 ++++-- lnbits/settings.py | 290 +++++++++++++++++++++++++---------- lnbits/wallets/fake.py | 9 +- 11 files changed, 340 insertions(+), 230 deletions(-) diff --git a/lnbits/commands.py b/lnbits/commands.py index 86868f1f7..830033144 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -8,6 +8,8 @@ import click from genericpath import exists from loguru import logger +from lnbits.settings import Settings, settings + from .core import db as core_db from .core import migrations as core_migrations from .db import COCKROACH, POSTGRES, SQLITE @@ -17,7 +19,8 @@ from .helpers import ( get_valid_extensions, url_for_vendored, ) -from .settings import LNBITS_PATH + +path = settings.lnbits_path @click.command("migrate") @@ -36,15 +39,15 @@ def transpile_scss(): warnings.simplefilter("ignore") from scss.compiler import compile_string # type: ignore - with open(os.path.join(LNBITS_PATH, "static/scss/base.scss")) as scss: - with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css: + with open(os.path.join(path, "static/scss/base.scss")) as scss: + with open(os.path.join(path, "static/css/base.css"), "w") as css: css.write(compile_string(scss.read())) def bundle_vendored(): for getfiles, outputpath in [ - (get_js_vendored, os.path.join(LNBITS_PATH, "static/bundle.js")), - (get_css_vendored, os.path.join(LNBITS_PATH, "static/bundle.css")), + (get_js_vendored, os.path.join(path, "static/bundle.js")), + (get_css_vendored, os.path.join(path, "static/bundle.css")), ]: output = "" for path in getfiles(): @@ -54,33 +57,6 @@ def bundle_vendored(): f.write(output) -async def get_admin_settings(): - from lnbits.extensions.admin.models import Admin - - try: - ext_db = importlib.import_module(f"lnbits.extensions.admin").db - except: - return False - - async with ext_db.connect() as conn: - - if conn.type == SQLITE: - exists = await conn.fetchone( - "SELECT * FROM sqlite_master WHERE type='table' AND name='admin'" - ) - elif conn.type in {POSTGRES, COCKROACH}: - exists = await conn.fetchone( - "SELECT * FROM information_schema.tables WHERE table_name = 'admin'" - ) - - if not exists: - return False - - row = await conn.fetchone("SELECT * from admin.admin") - - return Admin(**row) if row else None - - async def migrate_databases(): """Creates the necessary databases if they don't exist already; or migrates them.""" diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index bb1ca0c1c..7fc21a98f 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -8,7 +8,7 @@ from loguru import logger from lnbits import bolt11 from lnbits.db import COCKROACH, POSTGRES, Connection -from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS +from lnbits.settings import settings from . import db from .models import BalanceCheck, Payment, User, Wallet @@ -63,8 +63,8 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[ email=user["email"], extensions=[e[0] for e in extensions], wallets=[Wallet(**w) for w in wallets], - admin=user["id"] in [x.strip() for x in LNBITS_ADMIN_USERS] - if LNBITS_ADMIN_USERS + admin=user["id"] in settings.lnbits_admin_users + if settings.lnbits_admin_users else False, ) @@ -99,7 +99,7 @@ async def create_wallet( """, ( wallet_id, - wallet_name or DEFAULT_WALLET_NAME, + wallet_name or settings.lnbits_default_wallet_name, user_id, uuid4().hex, uuid4().hex, diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 5d993b4c5..23a5b315c 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -21,7 +21,7 @@ from lnbits.decorators import ( ) from lnbits.helpers import url_for, urlsafe_short_hash from lnbits.requestvars import g -from lnbits.settings import FAKE_WALLET, RESERVE_FEE_MIN, RESERVE_FEE_PERCENT, WALLET +from lnbits.settings import FAKE_WALLET, WALLET, settings from lnbits.wallets.base import PaymentResponse, PaymentStatus from . import db @@ -381,4 +381,6 @@ async def check_transaction_status( # WARN: this same value must be used for balance check and passed to WALLET.pay_invoice(), it may cause a vulnerability if the values differ def fee_reserve(amount_msat: int) -> int: - return max(int(RESERVE_FEE_MIN), int(amount_msat * RESERVE_FEE_PERCENT / 100.0)) + reserve_min = settings.lnbits_reserve_fee_min + reserve_percent = settings.lnbits_reserve_fee_percent + return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0)) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 983d5a261..c45c39769 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -30,7 +30,7 @@ from lnbits.decorators import ( require_invoice_key, ) from lnbits.helpers import url_for, urlsafe_short_hash -from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE, WALLET +from lnbits.settings import WALLET, settings from lnbits.utils.exchange_rates import ( currencies, fiat_amount_as_satoshis, @@ -76,7 +76,7 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)): async def api_update_balance( amount: int, wallet: WalletTypeInfo = Depends(get_key_type) ): - if wallet.wallet.user not in LNBITS_ADMIN_USERS: + if wallet.wallet.user not in settings.lnbits_admin_users: raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" ) @@ -178,7 +178,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): else: description_hash = b"" unhashed_description = b"" - memo = data.memo or LNBITS_SITE_TITLE + memo = data.memo or settings.lnbits_site_title if data.unit == "sat": amount = int(data.amount) else: @@ -678,7 +678,7 @@ async def img(request: Request, data): @core_app.get("/api/v1/audit/") async def api_auditor(wallet: WalletTypeInfo = Depends(get_key_type)): - if wallet.wallet.user not in LNBITS_ADMIN_USERS: + if wallet.wallet.user not in settings.lnbits_admin_users: raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" ) diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index db4fac430..d03f89f6a 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -16,14 +16,7 @@ from lnbits.core.models import User from lnbits.decorators import check_user_exists from lnbits.helpers import template_renderer, url_for from lnbits.requestvars import g -from lnbits.settings import ( - LNBITS_ADMIN_UI, - LNBITS_ADMIN_USERS, - LNBITS_ALLOWED_USERS, - LNBITS_CUSTOM_LOGO, - LNBITS_SITE_TITLE, - SERVICE_FEE, -) +from lnbits.settings import settings from ...helpers import get_valid_extensions from ..crud import ( @@ -119,14 +112,6 @@ async def wallet( user_id = usr.hex if usr else None wallet_id = wal.hex if wal else None wallet_name = nme - service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE - - if LNBITS_ADMIN_UI: - LNBITS_ADMIN_USERS = g().admin_conf.admin_users - LNBITS_ALLOWED_USERS = g().admin_conf.allowed_users - else: - LNBITS_ADMIN_USERS = [] - LNBITS_ALLOWED_USERS = [] if not user_id: user = await get_user((await create_account()).id) @@ -137,11 +122,14 @@ async def wallet( return template_renderer().TemplateResponse( "error.html", {"request": request, "err": "User does not exist."} ) - if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS: + if ( + len(settings.lnbits_allowed_users) > 0 + and user_id not in settings.lnbits_allowed_users + ): return template_renderer().TemplateResponse( "error.html", {"request": request, "err": "User not authorized."} ) - if LNBITS_ADMIN_USERS and user_id in LNBITS_ADMIN_USERS: + if user_id in settings.lnbits_admin_users: user.admin = True if not wallet_id: if user.wallets and not wallet_name: # type: ignore @@ -172,7 +160,7 @@ async def wallet( "request": request, "user": user.dict(), # type: ignore "wallet": userwallet.dict(), - "service_fee": service_fee, + "service_fee": settings.lnbits_service_fee, "web_manifest": f"/manifest/{user.id}.webmanifest", # type: ignore }, ) @@ -194,7 +182,7 @@ async def lnurl_full_withdraw(request: Request): "k1": "0", "minWithdrawable": 1000 if wallet.withdrawable_balance else 0, "maxWithdrawable": wallet.withdrawable_balance, - "defaultDescription": f"{LNBITS_SITE_TITLE} balance withdraw from {wallet.id[0:5]}", + "defaultDescription": f"{settings.lnbits_site_title} balance withdraw from {wallet.id[0:5]}", "balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id), } @@ -293,12 +281,12 @@ async def manifest(usr: str): raise HTTPException(status_code=HTTPStatus.NOT_FOUND) return { - "short_name": LNBITS_SITE_TITLE, - "name": LNBITS_SITE_TITLE + " Wallet", + "short_name": settings.lnbits_site_title, + "name": settings.lnbits_site_title + " Wallet", "icons": [ { - "src": LNBITS_CUSTOM_LOGO - if LNBITS_CUSTOM_LOGO + "src": settings.lnbits_custom_logo + if settings.lnbits_custom_logo else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png", "type": "image/png", "sizes": "900x900", diff --git a/lnbits/db.py b/lnbits/db.py index f52b03914..8ae10f720 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -10,7 +10,7 @@ from sqlalchemy import create_engine from sqlalchemy_aio.base import AsyncConnection from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore -from .settings import LNBITS_DATA_FOLDER, LNBITS_DATABASE_URL +from lnbits.settings import settings POSTGRES = "POSTGRES" COCKROACH = "COCKROACH" @@ -91,8 +91,8 @@ class Database(Compat): def __init__(self, db_name: str): self.name = db_name - if LNBITS_DATABASE_URL: - database_uri = LNBITS_DATABASE_URL + if settings.lnbits_database_url: + database_uri = settings.lnbits_database_url if database_uri.startswith("cockroachdb://"): self.type = COCKROACH @@ -137,14 +137,16 @@ class Database(Compat): ) ) else: - if os.path.isdir(LNBITS_DATA_FOLDER): - self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3") + if os.path.isdir(settings.lnbits_data_folder): + self.path = os.path.join( + settings.lnbits_data_folder, f"{self.name}.sqlite3" + ) database_uri = f"sqlite:///{self.path}" self.type = SQLITE else: raise NotADirectoryError( - f"LNBITS_DATA_FOLDER named {LNBITS_DATA_FOLDER} was not created" - f" - please 'mkdir {LNBITS_DATA_FOLDER}' and try again" + f"LNBITS_DATA_FOLDER named {settings.lnbits_data_folder} was not created" + f" - please 'mkdir {settings.lnbits_data_folder}' and try again" ) logger.trace(f"database {self.type} added for {self.name}") self.schema = self.name diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 5a3c0a5c3..8206146cf 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -14,12 +14,7 @@ from starlette.requests import Request from lnbits.core.crud import get_user, get_wallet_for_key from lnbits.core.models import User, Wallet from lnbits.requestvars import g -from lnbits.settings import ( - LNBITS_ADMIN_EXTENSIONS, - LNBITS_ADMIN_UI, - LNBITS_ADMIN_USERS, - LNBITS_ALLOWED_USERS, -) +from lnbits.settings import settings class KeyChecker(SecurityBase): @@ -139,11 +134,6 @@ async def get_key_type( detail="Invoice (or Admin) key required.", ) - if LNBITS_ADMIN_UI: - LNBITS_ADMIN_USERS = g().admin_conf.admin_users - else: - LNBITS_ADMIN_USERS = [] - for typenr, WalletChecker in zip( [0, 1], [WalletAdminKeyChecker, WalletInvoiceKeyChecker] ): @@ -156,8 +146,12 @@ async def get_key_type( status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist." ) if ( - LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS - ) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS): + settings.lnbits_admin_users + and wallet.wallet.user not in settings.lnbits_admin_users + ) and ( + settings.lnbits_admin_extensions + and pathname in settings.lnbits_admin_extensions + ): raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="User not authorized for this extension.", @@ -233,22 +227,33 @@ async def require_invoice_key( async def check_user_exists(usr: UUID4) -> User: g().user = await get_user(usr.hex) + if not g().user: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." + status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." ) - if LNBITS_ADMIN_UI: - LNBITS_ADMIN_USERS = g().admin_conf.admin_users - LNBITS_ALLOWED_USERS = g().admin_conf.allowed_users - else: - LNBITS_ADMIN_USERS = [] - LNBITS_ALLOWED_USERS = [] - - if LNBITS_ALLOWED_USERS and g().user.id not in LNBITS_ALLOWED_USERS: + if ( + len(settings.lnbits_allowed_users) > 0 + and g().user.id not in settings.lnbits_allowed_users + ): raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." ) - if LNBITS_ADMIN_USERS and g().user.id in LNBITS_ADMIN_USERS: + + if g().user.id in settings.lnbits_admin_users: g().user.admin = True + return g().user + + +async def check_admin(usr: UUID4) -> User: + user = await check_user_exists(usr) + + if not user.id in settings.lnbits_admin_users: + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, + detail="User not authorized. No admin privileges.", + ) + + return user diff --git a/lnbits/helpers.py b/lnbits/helpers.py index f4255c866..c967f7070 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -6,9 +6,9 @@ from typing import Any, List, NamedTuple, Optional import jinja2 import shortuuid # type: ignore -import lnbits.settings as settings from lnbits.jinja2_templating import Jinja2Templates from lnbits.requestvars import g +from lnbits.settings import settings class Extension(NamedTuple): @@ -24,15 +24,10 @@ class Extension(NamedTuple): class ExtensionManager: def __init__(self): - if settings.LNBITS_ADMIN_UI: - settings.LNBITS_DISABLED_EXTENSIONS = g().admin_conf.disabled_ext - settings.LNBITS_ADMIN_EXTENSIONS = g().admin_conf.admin_ext - self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS - self._admin_only: List[str] = [ - x.strip(" ") for x in settings.LNBITS_ADMIN_EXTENSIONS - ] + self._disabled: List[str] = settings.lnbits_disabled_extensions + self._admin_only: List[str] = settings.lnbits_admin_extensions self._extension_folders: List[str] = [ - x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions")) + x[1] for x in os.walk(os.path.join(settings.lnbits_path, "extensions")) ][0] @property @@ -48,7 +43,7 @@ class ExtensionManager: try: with open( os.path.join( - settings.LNBITS_PATH, "extensions", extension, "config.json" + settings.lnbits_path, "extensions", extension, "config.json" ) ) as json_file: config = json.load(json_file) @@ -120,7 +115,7 @@ def get_css_vendored(prefer_minified: bool = False) -> List[str]: def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]: paths: List[str] = [] for path in glob.glob( - os.path.join(settings.LNBITS_PATH, "static/vendor/**"), recursive=True + os.path.join(settings.lnbits_path, "static/vendor/**"), recursive=True ): if path.endswith(".min" + ext): # path is minified @@ -146,7 +141,7 @@ def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]: def url_for_vendored(abspath: str) -> str: - return "/" + os.path.relpath(abspath, settings.LNBITS_PATH) + return "/" + os.path.relpath(abspath, settings.lnbits_path) def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str: @@ -163,16 +158,6 @@ def removeEmptyString(arr): def template_renderer(additional_folders: List = []) -> Jinja2Templates: - if settings.LNBITS_ADMIN_UI: - _ = g().admin_conf - settings.LNBITS_AD_SPACE = _.ad_space - settings.LNBITS_HIDE_API = _.hide_api - settings.LNBITS_SITE_TITLE = _.site_title - settings.LNBITS_DENOMINATION = _.denomination - settings.LNBITS_SITE_TAGLINE = _.site_tagline - settings.LNBITS_SITE_DESCRIPTION = _.site_description - settings.LNBITS_THEME_OPTIONS = _.theme - settings.LNBITS_CUSTOM_LOGO = _.custom_logo t = Jinja2Templates( loader=jinja2.FileSystemLoader( @@ -180,20 +165,21 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates: ) ) - if settings.LNBITS_AD_SPACE: - t.env.globals["AD_SPACE"] = settings.LNBITS_AD_SPACE - t.env.globals["HIDE_API"] = settings.LNBITS_HIDE_API - t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE - t.env.globals["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION - t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE - t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION - t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS - t.env.globals["LNBITS_VERSION"] = settings.LNBITS_COMMIT - t.env.globals["EXTENSIONS"] = get_valid_extensions() - if settings.LNBITS_CUSTOM_LOGO: - t.env.globals["USE_CUSTOM_LOGO"] = settings.LNBITS_CUSTOM_LOGO + if settings.lnbits_ad_space: + t.env.globals["AD_SPACE"] = settings.lnbits_ad_space - if settings.DEBUG: + t.env.globals["HIDE_API"] = settings.lnbits_hide_api + t.env.globals["SITE_TITLE"] = settings.lnbits_site_title + t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination + t.env.globals["SITE_TAGLINE"] = settings.lnbits_site_tagline + t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description + t.env.globals["LNBITS_THEME_OPTIONS"] = settings.lnbits_theme_options + t.env.globals["LNBITS_VERSION"] = settings.lnbits_commit + t.env.globals["EXTENSIONS"] = get_valid_extensions() + if settings.lnbits_custom_logo: + t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo + + if settings.debug: t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored()) t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored()) else: diff --git a/lnbits/server.py b/lnbits/server.py index e9849851b..7c033981b 100644 --- a/lnbits/server.py +++ b/lnbits/server.py @@ -1,9 +1,16 @@ +import asyncio +import uvloop +uvloop.install() + +import contextlib +import multiprocessing as mp +import sys import time import click import uvicorn -from lnbits.settings import HOST, PORT +from lnbits.settings import settings @click.command( @@ -12,12 +19,13 @@ from lnbits.settings import HOST, PORT allow_extra_args=True, ) ) -@click.option("--port", default=PORT, help="Port to listen on") -@click.option("--host", default=HOST, help="Host to run LNBits on") +@click.option("--port", default=settings.port, help="Port to listen on") +@click.option("--host", default=settings.host, help="Host to run LNBits on") +@click.option("--reload", is_flag=True, help="Reload LNBits on changes in code") @click.option("--ssl-keyfile", default=None, help="Path to SSL keyfile") @click.option("--ssl-certfile", default=None, help="Path to SSL certificate") @click.pass_context -def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str): +def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str, reload: bool): """Launched with `poetry run lnbits` at root level""" # this beautiful beast parses all command line arguments and passes them to the uvicorn server d = dict() @@ -33,17 +41,31 @@ def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str): else: d[a.strip("--")] = True # argument like --key - config = uvicorn.Config( - "lnbits.__main__:app", - port=port, - host=host, - ssl_keyfile=ssl_keyfile, - ssl_certfile=ssl_certfile, - **d - ) - server = uvicorn.Server(config) - server.run() + while True: + # loop = asyncio.new_event_loop() + config = uvicorn.Config( + "lnbits.__main__:app", + port=port, + host=host, + reload=reload, + # loop=loop, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + **d + ) + server = uvicorn.Server(config=config) + process = mp.Process(target=server.run) + process.start() + server_restart.wait() + server_restart.clear() + server.should_exit = True + server.force_exit = True + process.terminate() + process.join() + time.sleep(3) +server_restart = mp.Event() + if __name__ == "__main__": main() diff --git a/lnbits/settings.py b/lnbits/settings.py index 8e5c321a2..81bd64941 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -1,88 +1,220 @@ import importlib +import json import subprocess from os import path -from typing import List +from sqlite3 import Row +from typing import List, Optional -from environs import Env +from loguru import logger +from pydantic import BaseSettings, Field, validator + + +def list_parse_fallback(v): + try: + return json.loads(v) + except Exception as e: + replaced = v.replace(" ", "") + if replaced: + return replaced.split(",") + else: + return [] + + +read_only_variables = ["host", "port", "lnbits_commit"] + + +class Settings(BaseSettings): + + lnbits_admin_ui: bool = Field(default=False) + + # .env + debug: Optional[bool] + host: Optional[str] + port: Optional[int] + lnbits_path: Optional[str] = path.dirname(path.realpath(__file__)) + lnbits_commit: str = Field(default="unknown") + + # users + 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=[]) + + # Change theme + 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) + lnbits_default_wallet_name: str = Field(default="LNbits wallet") + lnbits_theme_options: List[str] = Field( + default=["classic", "flamingo", "mint", "salvador", "monochrome", "autumn"] + ) + lnbits_custom_logo: str = Field(default=None) + lnbits_ad_space: List[str] + + # ops + lnbits_data_folder: str = Field(default="./data") + lnbits_database_url: str = Field(default=None) + lnbits_force_https: bool = Field(default=True) + lnbits_reserve_fee_min: int = Field(default=4000) + lnbits_reserve_fee_percent: float = Field(default=1.0) + lnbits_service_fee: float = Field(default=0) + lnbits_hide_api: bool = Field(default=False) + lnbits_denomination: str = Field(default="sats") + + # funding sources + lnbits_backend_wallet_class: str = Field(default="VoidWallet") + lnbits_allowed_funding_sources: List[str] = Field( + default=[ + "CLightningWallet", + "LndRestWallet", + "LndWallet", + "LntxbotWallet", + "LNPayWallet", + "LnbitsWallet", + "OpenNodeWallet", + ] + ) + fake_wallet_secret: str = Field(default="ToTheMoon1") + lnbits_endpoint: str = Field(default="https://legend.lnbits.com") + lnbits_key: Optional[str] = Field(default=None) + cliche_endpoint: Optional[str] = Field(default=None) + corelightning_rpc: Optional[str] = Field(default=None) + eclair_url: Optional[str] = Field(default=None) + eclair_pass: Optional[str] = Field(default=None) + lnd_rest_endpoint: Optional[str] = Field(default=None) + lnd_rest_cert: Optional[str] = Field(default=None) + lnd_rest_macaroon: Optional[str] = Field(default=None) + lnpay_api_endpoint: Optional[str] = Field(default=None) + lnpay_api_key: Optional[str] = Field(default=None) + lnpay_wallet_key: Optional[str] = Field(default=None) + lntxbot_api_endpoint: Optional[str] = Field(default=None) + lntxbot_key: Optional[str] = Field(default=None) + opennode_api_endpoint: Optional[str] = Field(default=None) + opennode_key: Optional[str] = Field(default=None) + spark_url: Optional[str] = Field(default=None) + spark_token: Optional[str] = Field(default=None) + + # boltz + 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") + + @validator( + "lnbits_admin_users", + "lnbits_allowed_users", + "lnbits_theme_options", + "lnbits_ad_space", + "lnbits_admin_extensions", + "lnbits_disabled_extensions", + "lnbits_allowed_funding_sources", + pre=True, + ) + def validate(cls, val): + if type(val) == str: + val = val.split(",") if val else [] + return val + + @classmethod + def from_row(cls, row: Row) -> "Settings": + data = dict(row) + return cls(**data) + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + case_sensitive = False + json_loads = list_parse_fallback + + +settings = Settings() + +settings.lnbits_commit = ( + subprocess.check_output( + ["git", "-C", settings.lnbits_path, "rev-parse", "HEAD"], + stderr=subprocess.DEVNULL, + ) + .strip() + .decode("ascii") +) + + +if not settings.lnbits_admin_ui: + logger.debug(f"Enviroment Settings:") + for key, value in settings.dict(exclude_none=True).items(): + logger.debug(f"{key}: {value}") + + +async def check_admin_settings(): + if settings.lnbits_admin_ui: + try: + ext_db = importlib.import_module(f"lnbits.extensions.admin").db + except: + logger.error("could not import module lnbits.extensions.admin database") + raise + + async with ext_db.connect() as db: + try: + row = await db.fetchone("SELECT * FROM admin.settings") + if not row or len(row) == 0: + logger.warning( + "admin.settings empty. inserting new settings and creating admin account" + ) + + from lnbits.core.crud import create_account + + account = await create_account() + settings.lnbits_admin_users.insert(0, account.id) + keys = [] + values = "" + for key, value in settings.dict(exclude_none=True).items(): + keys.append(key) + if type(value) == list: + joined = ",".join(value) + values += f"'{joined}'" + if type(value) == int or type(value) == float: + values += str(value) + if type(value) == bool: + values += "true" if value else "false" + if type(value) == str: + value = value.replace("'", "") + values += f"'{value}'" + values += "," + q = ", ".join(keys) + v = values.rstrip(",") + sql = f"INSERT INTO admin.settings ({q}) VALUES ({v})" + await db.execute(sql) + logger.warning( + "initialized admin.settings from enviroment variables." + ) + + row = await db.fetchone("SELECT * FROM admin.settings") + assert row, "Newly updated settings couldn't be retrieved" + + admin = Settings(**row) + + logger.debug(f"Admin settings:") + for key, value in admin.dict(exclude_none=True).items(): + if not key in read_only_variables: + try: + setattr(settings, key, value) + logger.debug(f"{key}: {value}") + except: + logger.error( + f"error overriding setting: {key}, value: {value}" + ) + + http = "https" if settings.lnbits_force_https else "http" + user = settings.lnbits_admin_users[0] + logger.warning( + f" ✔️ Access admin user account at: {http}://{settings.host}:{settings.port}/wallet?usr={user}" + ) + except: + logger.warning("admin.settings tables does not exist.") + raise -env = Env() -env.read_env() wallets_module = importlib.import_module("lnbits.wallets") -wallet_class = getattr( - wallets_module, env.str("LNBITS_BACKEND_WALLET_CLASS", default="VoidWallet") -) - -DEBUG = env.bool("DEBUG", default=False) - -HOST = env.str("HOST", default="127.0.0.1") -PORT = env.int("PORT", default=5000) - -LNBITS_PATH = path.dirname(path.realpath(__file__)) -LNBITS_DATA_FOLDER = env.str( - "LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data") -) -LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None) - -LNBITS_ALLOWED_USERS: List[str] = [ - x.strip(" ") for x in env.list("LNBITS_ALLOWED_USERS", default=[], subcast=str) -] -LNBITS_ADMIN_UI = env.bool("LNBITS_ADMIN_UI", default=False) -LNBITS_ADMIN_USERS: List[str] = [ - x.strip(" ") for x in env.list("LNBITS_ADMIN_USERS", default=[], subcast=str) -] -LNBITS_ADMIN_EXTENSIONS: List[str] = [ - x.strip(" ") for x in env.list("LNBITS_ADMIN_EXTENSIONS", default=[], subcast=str) -] -LNBITS_DISABLED_EXTENSIONS: List[str] = [ - x.strip(" ") - for x in env.list("LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str) -] - -LNBITS_AD_SPACE = [x.strip(" ") for x in env.list("LNBITS_AD_SPACE", default=[])] -LNBITS_HIDE_API = env.bool("LNBITS_HIDE_API", default=False) -LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits") -LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats") -LNBITS_SITE_TAGLINE = env.str( - "LNBITS_SITE_TAGLINE", default="free and open-source lightning wallet" -) -LNBITS_SITE_DESCRIPTION = env.str("LNBITS_SITE_DESCRIPTION", default="") -LNBITS_THEME_OPTIONS: List[str] = [ - x.strip(" ") - for x in env.list( - "LNBITS_THEME_OPTIONS", - default="classic, flamingo, mint, salvador, monochrome, autumn", - subcast=str, - ) -] -LNBITS_CUSTOM_LOGO = env.str("LNBITS_CUSTOM_LOGO", default="") - +wallet_class = getattr(wallets_module, settings.lnbits_backend_wallet_class) WALLET = wallet_class() FAKE_WALLET = getattr(wallets_module, "FakeWallet")() -DEFAULT_WALLET_NAME = env.str("LNBITS_DEFAULT_WALLET_NAME", default="LNbits wallet") -PREFER_SECURE_URLS = env.bool("LNBITS_FORCE_HTTPS", default=True) - -RESERVE_FEE_MIN = env.int("LNBITS_RESERVE_FEE_MIN", default=2000) -RESERVE_FEE_PERCENT = env.float("LNBITS_RESERVE_FEE_PERCENT", default=1.0) -SERVICE_FEE = env.float("LNBITS_SERVICE_FEE", default=0.0) - -try: - LNBITS_COMMIT = ( - subprocess.check_output( - ["git", "-C", LNBITS_PATH, "rev-parse", "HEAD"], stderr=subprocess.DEVNULL - ) - .strip() - .decode("ascii") - ) -except: - LNBITS_COMMIT = "unknown" - - -BOLTZ_NETWORK = env.str("BOLTZ_NETWORK", default="main") -BOLTZ_URL = env.str("BOLTZ_URL", default="https://boltz.exchange/api") -BOLTZ_MEMPOOL_SPACE_URL = env.str( - "BOLTZ_MEMPOOL_SPACE_URL", default="https://mempool.space" -) -BOLTZ_MEMPOOL_SPACE_URL_WS = env.str( - "BOLTZ_MEMPOOL_SPACE_URL_WS", default="wss://mempool.space" -) diff --git a/lnbits/wallets/fake.py b/lnbits/wallets/fake.py index a07ef4d89..5dc266c58 100644 --- a/lnbits/wallets/fake.py +++ b/lnbits/wallets/fake.py @@ -2,12 +2,12 @@ import asyncio import hashlib import random from datetime import datetime -from os import getenv from typing import AsyncGenerator, Dict, Optional -from environs import Env # type: ignore from loguru import logger +from lnbits.settings import settings + from ..bolt11 import Invoice, decode, encode from .base import ( InvoiceResponse, @@ -17,9 +17,6 @@ from .base import ( Wallet, ) -env = Env() -env.read_env() - class FakeWallet(Wallet): queue: asyncio.Queue = asyncio.Queue(0) @@ -47,7 +44,7 @@ class FakeWallet(Wallet): ) -> InvoiceResponse: # we set a default secret since FakeWallet is used for internal=True invoices # and the user might not have configured a secret yet - + secret = settings.fake_wallet_secret data: Dict = { "out": False, "amount": amount,