Compare commits

..
Author SHA1 Message Date
Vlad Stan 4744ef9bba Merge branch 'more_unit_tests' of https://github.com/lnbits/lnbits into more_unit_tests 2026-03-30 16:00:12 +03:00
Vlad Stan fdf061c08d chore: make regtest wait 2026-03-30 15:32:42 +03:00
Vlad Stan d348234cad fix: aaaaa 2026-03-30 15:32:42 +03:00
Vlad Stan fc5261132e fix: regtests 2026-03-30 15:32:42 +03:00
Vlad Stan 4e18c423de chore: check log 2026-03-30 15:32:42 +03:00
Vlad Stan 37e5601207 fix: try clean-up 2026-03-30 15:32:42 +03:00
Vlad Stan 231f6fc01c fix: is boltz wallet 2026-03-30 15:32:42 +03:00
Vlad Stan 811957f5e1 chore: logs 2026-03-30 15:32:42 +03:00
Vlad Stan c63a53cb14 chore: some logs 2026-03-30 15:32:42 +03:00
Vlad Stan 78b8f7f16e fix: hope for regtest to work 2026-03-30 15:32:42 +03:00
Vlad Stan 4642878c05 fix: test 2026-03-30 15:32:42 +03:00
Vlad Stan d7c850ccbb fix: test funding source 2026-03-30 15:32:42 +03:00
Vlad Stan 4fbb4b5eef chore: fix lint 2026-03-30 15:32:42 +03:00
Vlad Stan db2829a5f6 chore: postpone test 2026-03-30 15:31:59 +03:00
Vlad Stan 13ce1ac172 chore: fix lint 2026-03-30 15:08:42 +03:00
Vlad Stan 512697ae53 test: more tests 2026-03-30 14:58:04 +03:00
Vlad Stan dfa89ca063 test: some tests 2026-03-30 13:18:36 +03:00
Vlad Stan 182694d2e5 fix: bad config 2026-03-30 13:12:47 +03:00
Vlad Stan 2f10c57a10 fix: timeout 2026-03-30 13:12:46 +03:00
Vlad Stan d685824596 fix: test funding source 2026-03-30 13:12:46 +03:00
Vlad Stan e816eb2dac fix: date 2026-03-30 13:12:46 +03:00
Vlad Stan d3c099350a chore: fix lint 2026-03-30 13:12:46 +03:00
Vlad Stan 691ac969dc chore: postpone test 2026-03-30 13:12:46 +03:00
Vlad Stan 85e8ac6057 refactor: extract helpers, move private functions to the bottom 2026-03-30 13:12:46 +03:00
Vlad Stan 99da300f6d chore: code lint 2026-03-30 13:12:46 +03:00
Vlad Stan cd413d85ed fix: tests 2026-03-30 13:12:46 +03:00
Vlad Stan 9245e2c268 fix: lint 2026-03-30 13:12:46 +03:00
Vlad Stan 0b5c8cdee2 test: more api tests 2026-03-30 13:12:46 +03:00
Vlad Stan c94cef07c2 chore: fix lint 2026-03-30 13:12:46 +03:00
Vlad Stan 22b4ab779f test: more tests 2026-03-30 13:12:46 +03:00
Vlad Stan 42061d0cc8 test: some tests 2026-03-30 13:12:46 +03:00
Vlad Stan d0ea42fd70 chore: make regtest wait 2026-03-26 12:28:12 +02:00
Vlad Stan cd58a0dc9b fix: aaaaa 2026-03-26 12:12:13 +02:00
Vlad Stan cac4e309f4 fix: regtests 2026-03-26 11:57:25 +02:00
Vlad Stan 632cd257d7 chore: check log 2026-03-26 11:50:30 +02:00
Vlad Stan a11274593c fix: try clean-up 2026-03-26 11:49:57 +02:00
Vlad Stan ae92965b69 fix: is boltz wallet 2026-03-26 11:45:16 +02:00
Vlad Stan 3e1f75c4cc chore: logs 2026-03-26 11:32:02 +02:00
Vlad Stan d668747ca2 chore: some logs 2026-03-26 11:24:18 +02:00
Vlad Stan dedfcd4818 fix: hope for regtest to work 2026-03-25 11:37:21 +02:00
Vlad Stan f66f2fe6c2 fix: test 2026-03-25 11:24:25 +02:00
Vlad Stan f3ca37e846 Merge branch 'more_unit_tests' of https://github.com/lnbits/lnbits into more_unit_tests 2026-03-25 10:54:23 +02:00
Vlad Stan d45709a80f fix: bad config 2026-03-25 10:53:25 +02:00
Vlad Stan e4b6dc84b5 fix: timeout 2026-03-25 10:49:52 +02:00
Vlad Stan 67d1b4be3e fix: test funding source 2026-03-25 10:49:52 +02:00
Vlad Stan bd514f1a46 fix: date 2026-03-25 10:49:52 +02:00
Vlad Stan 03b302da69 chore: fix lint 2026-03-25 10:49:52 +02:00
Vlad Stan b373e35915 chore: postpone test 2026-03-25 10:49:52 +02:00
Vlad Stan 0fd116e7ee refactor: extract helpers, move private functions to the bottom 2026-03-25 10:49:52 +02:00
Vlad Stan 2c84f9781d chore: code lint 2026-03-25 10:49:52 +02:00
Vlad Stan 9fcf3121ac fix: tests 2026-03-25 10:49:52 +02:00
Vlad Stan 1df9fa831d fix: lint 2026-03-25 10:49:52 +02:00
Vlad Stan 42039070b9 test: more api tests 2026-03-25 10:49:52 +02:00
Vlad Stan 2f9146fff3 chore: fix lint 2026-03-25 10:49:52 +02:00
Vlad Stan 5738795c8c test: more tests 2026-03-25 10:49:52 +02:00
Vlad Stan 3c77acf266 test: some tests 2026-03-25 10:49:52 +02:00
Vlad Stan a4f2f55279 fix: timeout 2026-03-23 14:01:55 +02:00
Vlad Stan 2ab7c17ceb fix: test funding source 2026-03-23 12:33:57 +02:00
Vlad Stan 7a2c509cd3 fix: date 2026-03-23 12:32:39 +02:00
Vlad Stan 8a51ea53c8 chore: fix lint 2026-03-23 12:32:39 +02:00
Vlad Stan 19d3e9e64e chore: postpone test 2026-03-23 12:32:39 +02:00
Vlad Stan 9f9f14ead0 refactor: extract helpers, move private functions to the bottom 2026-03-23 12:32:39 +02:00
Vlad Stan 0a093d82ae chore: code lint 2026-03-23 12:32:39 +02:00
Vlad Stan 17fd459fa7 fix: tests 2026-03-23 12:32:39 +02:00
Vlad Stan fe9aa4d8c9 fix: lint 2026-03-23 12:32:39 +02:00
Vlad Stan 1af3b62739 test: more api tests 2026-03-23 12:32:39 +02:00
Vlad Stan e9cbc3492f chore: fix lint 2026-03-23 12:32:39 +02:00
Vlad Stan ca2f78d25b test: more tests 2026-03-23 12:32:39 +02:00
Vlad Stan 9d9ab48345 test: some tests 2026-03-23 12:32:39 +02:00
63 changed files with 339 additions and 1034 deletions
+1 -9
View File
@@ -7,13 +7,9 @@
# They are NOT managed by the Admin UI and are not stored in the database.
# === First Install Token ===
# If set the user is required to enter this token on the /first_install page
# if set the user is required to enter this token on the /first_install page
# FIRST_INSTALL_TOKEN="myaccesstoken"
# === Security ===
# When enabled (recommended), auth cookies require HTTPS and SSO will reject insecure HTTP.
AUTH_HTTPS_ONLY=true
# === Logging and Development ===
DEBUG=False
@@ -28,10 +24,6 @@ LOG_ROTATION="100 MB"
LOG_RETENTION="3 months"
# for database cleanup commands
# 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 ===
-3
View File
@@ -69,10 +69,7 @@ jobs:
--onefile \
--name lnbits \
--hidden-import=embit \
--hidden-import=bitstring.bitstore_bitarray \
--collect-all embit \
--collect-all bitstring \
--collect-all bitarray \
--collect-all lnbits \
--collect-all sqlalchemy \
--collect-all breez_sdk \
+1 -1
View File
@@ -65,7 +65,7 @@ jobs:
make: openapi
regtest:
needs: [ lint ]
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi ]
uses: ./.github/workflows/regtest.yml
strategy:
matrix:
-1
View File
@@ -6,7 +6,6 @@ __pycache__
*$py.class
.mypy_cache
.vscode
.codex
*-lock.json
.python-version
+10 -10
View File
@@ -1,12 +1,12 @@
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/logos/lnbits-full-inverse.svg">
<img src="docs/logos/lnbits-full.svg" alt="LNbits" style="width:300px">
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
</picture>
</a>
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043) [![license-badge]](LICENSE) [![docs-badge]][docs] ![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow) [![explore: LNbits extensions](https://img.shields.io/badge/explore-LNbits%20extensions-10B981)](https://extensions.lnbits.com/) [![hardware: LNBitsShop](https://img.shields.io/badge/hardware-LNBitsShop-7C3AED)](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
<img alt="lnbits_head" src="docs/assets/header.jpg" />
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
[![tip-hero](https://img.shields.io/badge/TipJar-LNBits%20Hero-9b5cff?labelColor=6b7280&logo=lightning&logoColor=white)](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
# LNbits — The most powerful Bitcoin & Lightning toolkit
@@ -46,7 +46,7 @@ Get yourself familiar and test on our demo server [demo.lnbits.com](https://demo
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv. Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
<img alt="lnbits_wallet" src="docs/assets/wallet.jpg" />
<img src="https://i.imgur.com/w8jdGpF.png" style="width:800px">
## LNbits extension universe
@@ -54,25 +54,25 @@ Extend YOUR LNbits to meet YOUR needs.
All non-core features are installed as extensions, reducing your code base and making your LNbits unique to you. Extend your LNbits install in any direction, and even create and share your own extensions.
<img alt="lnbits_extensions" src="docs/assets/extensions.jpg" />
<img src="https://i.imgur.com/aEBpwJF.png" style="width:800px">
## LNbits API
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
<img alt="lnbits_api" src="docs/assets/api.jpg" />
<img src="https://i.imgur.com/V742sb9.png" style="width:800px">
## LNbits node manager
LNbits comes packaged with a light node management UI, to make running your node that much easier.
<img alt="lnbits_api" src="docs/assets/lightning_node.jpg" />
<img src="https://i.imgur.com/TYqIK60.png" style="width:800px">
## LNbits merchant tools
## LNbits across all your devices
The LNbits stack can process both bitcoin and fiat payments, making it a turnkey, all-in-one solution for merchants. With orders and inventory shared across extensions, and built-in notifications for Nostr, Telegram, and email, LNbits keeps everything in sync, freeing merchants to focus on their business.
As well as working great in a browser, LNbits has native IoS and Android apps as well as a chrome extension. So you can enjoy the same UI across ALL your devices.
<img alt="lnbits_merchants" src="docs/assets/merchants_small.webp" />
<img src="https://i.imgur.com/J96EbRf.png" style="width:800px">
## Powered by LNbits
Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

-4
View File
@@ -12,7 +12,6 @@ from .extensions import (
drop_extension_db,
get_installed_extension,
get_installed_extensions,
get_installed_extensions_count,
get_user_active_extensions_ids,
get_user_extension,
get_user_extensions,
@@ -59,7 +58,6 @@ from .users import (
get_account_by_username,
get_account_by_username_or_email,
get_accounts,
get_accounts_count,
get_user,
get_user_access_control_lists,
get_user_from_account,
@@ -119,13 +117,11 @@ __all__ = [
"get_account_by_username",
"get_account_by_username_or_email",
"get_accounts",
"get_accounts_count",
"get_admin_settings",
"get_db_version",
"get_db_versions",
"get_installed_extension",
"get_installed_extensions",
"get_installed_extensions_count",
"get_latest_payments_by_extension",
"get_payment",
"get_payments",
-7
View File
@@ -90,13 +90,6 @@ async def get_installed_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(
user_id: str, extension: str, conn: Connection | None = None
) -> UserExtension | None:
-7
View File
@@ -37,13 +37,6 @@ async def create_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:
account.updated_at = datetime.now(timezone.utc)
await (conn or db).update("accounts", account)
+5 -66
View File
@@ -43,8 +43,6 @@ class ExplicitRelease(BaseModel):
details_link: str | None
paid_features: str | None
pay_link: str | None
admin_only: bool = False
super_user_only: bool = False
def is_version_compatible(self):
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
@@ -54,8 +52,6 @@ class GitHubRelease(BaseModel):
id: str
organisation: str
repository: str
admin_only: bool = False
super_user_only: bool = False
class Manifest(BaseModel):
@@ -87,8 +83,6 @@ class ExtensionConfig(BaseModel):
warning: str | None = ""
min_lnbits_version: str | None
max_lnbits_version: str | None
admin_only: bool = False
super_user_only: bool = False
def is_version_compatible(self) -> bool:
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
@@ -201,8 +195,6 @@ class ExtensionRelease(BaseModel):
cost_sats: int | None = None
paid_sats: int | None = 0
payment_hash: str | None = None
admin_only: bool = False
super_user_only: bool = False
@property
def archive_url(self) -> str:
@@ -271,8 +263,6 @@ class ExtensionRelease(BaseModel):
paid_features=e.paid_features,
repo=e.repo,
icon=e.icon,
admin_only=e.admin_only,
super_user_only=e.super_user_only,
)
@classmethod
@@ -299,8 +289,6 @@ class ExtensionRelease(BaseModel):
release.min_lnbits_version = config.min_lnbits_version
release.max_lnbits_version = config.max_lnbits_version
release.is_version_compatible = config.is_version_compatible()
release.admin_only = config.admin_only
release.super_user_only = config.super_user_only
release.icon = icon_to_github_url(f"{org}/{repo}", config.tile)
@@ -348,8 +336,6 @@ class ExtensionMeta(BaseModel):
paid_features: str | None = None
has_paid_release: bool = False
has_free_release: bool = False
admin_only: bool = False
super_user_only: bool = False
class InstallableExtension(BaseModel):
@@ -413,16 +399,6 @@ class InstallableExtension(BaseModel):
return False
return self.meta.pay_to_enable.required is True
@property
def is_admin_only(self) -> bool:
return settings.is_admin_extension(self.id) or bool(
self.meta and self.meta.admin_only
)
@property
def is_super_user_only(self) -> bool:
return bool(self.meta and self.meta.super_user_only)
async def download_archive(self):
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
ext_zip_file = self.zip_path
@@ -478,16 +454,6 @@ class InstallableExtension(BaseModel):
self.name = config_json.get("name")
self.short_description = config_json.get("short_description")
if self.meta:
self.meta.admin_only = config_json.get("admin_only", False)
self.meta.super_user_only = config_json.get("super_user_only", False)
if self.meta.installed_release:
self.meta.installed_release.admin_only = config_json.get(
"admin_only", False
)
self.meta.installed_release.super_user_only = config_json.get(
"super_user_only", False
)
if (
self.meta
@@ -530,10 +496,6 @@ class InstallableExtension(BaseModel):
return
if not release.is_version_compatible:
return
if not self.meta:
self.meta = ExtensionMeta()
self.meta.admin_only = release.admin_only
self.meta.super_user_only = release.super_user_only
if not self.meta or not self.meta.latest_release:
meta = self.meta or ExtensionMeta()
meta.latest_release = release
@@ -596,13 +558,6 @@ class InstallableExtension(BaseModel):
github_release.organisation, github_release.repository
)
source_repo = f"{github_release.organisation}/{github_release.repository}"
admin_only = github_release.admin_only or config.admin_only
super_user_only = github_release.super_user_only or config.super_user_only
latest_extension_release = ExtensionRelease.from_github_release(
source_repo, latest_release
)
latest_extension_release.admin_only = admin_only
latest_extension_release.super_user_only = super_user_only
return InstallableExtension(
id=github_release.id,
name=config.name,
@@ -614,9 +569,9 @@ class InstallableExtension(BaseModel):
config.tile,
),
meta=ExtensionMeta(
admin_only=admin_only,
super_user_only=super_user_only,
latest_release=latest_extension_release,
latest_release=ExtensionRelease.from_github_release(
source_repo, latest_release
),
),
)
except Exception as e:
@@ -625,12 +580,7 @@ class InstallableExtension(BaseModel):
@classmethod
def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension:
meta = ExtensionMeta(
archive=e.archive,
dependencies=e.dependencies,
admin_only=e.admin_only,
super_user_only=e.super_user_only,
)
meta = ExtensionMeta(archive=e.archive, dependencies=e.dependencies)
return InstallableExtension(
id=e.id,
name=e.name,
@@ -660,8 +610,6 @@ class InstallableExtension(BaseModel):
short_description=config_json.get("short_description"),
icon=config_json.get("tile"),
meta=ExtensionMeta(
admin_only=config_json.get("admin_only", False),
super_user_only=config_json.get("super_user_only", False),
installed_release=ExtensionRelease(
name=ext_id,
version=version,
@@ -669,9 +617,7 @@ class InstallableExtension(BaseModel):
source_repo=f"{conf_path}",
min_lnbits_version=config_json.get("min_lnbits_version"),
max_lnbits_version=config_json.get("max_lnbits_version"),
admin_only=config_json.get("admin_only", False),
super_user_only=config_json.get("super_user_only", False),
),
)
),
)
@@ -773,13 +719,6 @@ class InstallableExtension(BaseModel):
repo_releases = await ExtensionRelease.get_github_releases(
r.organisation, r.repository
)
for repo_release in repo_releases:
repo_release.admin_only = (
repo_release.admin_only or r.admin_only
)
repo_release.super_user_only = (
repo_release.super_user_only or r.super_user_only
)
extension_releases += repo_releases
for e in manifest.extensions:
-12
View File
@@ -9,7 +9,6 @@ from lnbits.core.crud import (
delete_installed_extension,
get_db_version,
get_installed_extension,
get_installed_extensions_count,
update_installed_extension_state,
)
from lnbits.core.crud.extensions import (
@@ -39,8 +38,6 @@ async def install_extension(
if installed_ext and installed_ext.meta:
ext_info.meta.payments = installed_ext.meta.payments
await check_extensions_limit(installed_ext)
if not skip_download:
await ext_info.download_archive()
@@ -66,15 +63,6 @@ async def install_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):
await stop_extension_background_work(ext_id)
-12
View File
@@ -23,7 +23,6 @@ from ..crud import (
get_account_by_email,
get_account_by_pubkey,
get_account_by_username,
get_accounts_count,
get_super_settings,
get_user_extensions,
get_user_from_account,
@@ -56,8 +55,6 @@ async def create_user_account_no_ckeck(
conn: Connection | None = None,
) -> User:
async with db.reuse_conn(conn) if conn else db.connect() as conn:
await check_users_limit(conn)
if account:
account.validate_fields()
if account.username and await get_account_by_username(
@@ -98,15 +95,6 @@ async def create_user_account_no_ckeck(
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:
account.validate_fields()
+4 -29
View File
@@ -79,11 +79,7 @@ async def api_install_extension(data: CreateExtension):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Incompatible extension version.")
release.payment_hash = data.payment_hash
ext_meta = ExtensionMeta(
installed_release=release,
admin_only=release.admin_only,
super_user_only=release.super_user_only,
)
ext_meta = ExtensionMeta(installed_release=release)
ext_info = InstallableExtension(
id=data.ext_id,
name=data.ext_id,
@@ -102,16 +98,12 @@ async def api_install_extension(data: CreateExtension):
ext_info.clean_extension_files()
detail = (
str(exc)
if isinstance(exc, (AssertionError, ValueError))
if isinstance(exc, AssertionError)
else f"Failed to install extension '{ext_info.id}'."
f"({ext_info.installed_version})."
)
raise HTTPException(
status_code=(
HTTPStatus.BAD_REQUEST
if isinstance(exc, (AssertionError, ValueError))
else HTTPStatus.INTERNAL_SERVER_ERROR
),
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=detail,
) from exc
@@ -180,19 +172,6 @@ async def api_update_pay_to_enable(
)
def _check_enable_extension_access(
ext_id: str, ext: InstallableExtension, account_id: AccountId
) -> None:
if ext.is_super_user_only and not settings.is_super_user(account_id.id):
raise HTTPException(
HTTPStatus.FORBIDDEN, f"User not authorized for extension '{ext_id}'."
)
if ext.is_admin_only and not settings.is_admin_user(account_id.id):
raise HTTPException(
HTTPStatus.FORBIDDEN, f"User not authorized for extension '{ext_id}'."
)
@extension_router.put("/{ext_id}/enable")
async def api_enable_extension(
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
@@ -208,7 +187,6 @@ async def api_enable_extension(
raise ValueError(f"Extension '{ext_id}' is not installed.")
if not ext.active:
raise ValueError(f"Extension '{ext_id}' is not activated.")
_check_enable_extension_access(ext_id, ext, account_id)
user_ext = await get_user_extension(account_id.id, ext_id)
if not user_ext:
@@ -482,8 +460,6 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
"min_lnbits_version": config.min_lnbits_version,
"is_version_compatible": config.is_version_compatible(),
"warning": config.warning,
"admin_only": config.admin_only,
"super_user_only": config.super_user_only,
}
except Exception as exc:
raise HTTPException(
@@ -593,8 +569,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
(True for version in db_versions if version.db == ext.id), False
),
"isAvailable": ext.id in all_ext_ids,
"isAdminOnly": ext.is_admin_only,
"isSuperUserOnly": ext.is_super_user_only,
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
"isActive": ext.id not in inactive_extensions,
"latestRelease": (
dict(ext.meta.latest_release)
+1 -14
View File
@@ -14,7 +14,6 @@ from lnbits.core.crud import (
get_account,
get_account_by_email,
get_account_by_username,
get_installed_extension,
get_user_active_extensions_ids,
get_user_from_account,
get_wallet_for_key,
@@ -425,19 +424,7 @@ async def check_user_extension_access(
Check if the user has access to a particular extension.
Raises HTTP Forbidden if the user is not allowed.
"""
ext = await get_installed_extension(ext_id, conn=conn)
is_admin_only = settings.is_admin_extension(ext_id) or bool(
ext and ext.meta and ext.meta.admin_only
)
is_super_user_only = bool(ext and ext.meta and ext.meta.super_user_only)
if is_super_user_only and not settings.is_super_user(user_id):
return SimpleStatus(
success=False,
message=f"User not authorized for extension '{ext_id}'.",
)
if is_admin_only and not settings.is_admin_user(user_id):
if settings.is_admin_extension(ext_id) and not settings.is_admin_user(user_id):
return SimpleStatus(
success=False, message=f"User not authorized for extension '{ext_id}'."
)
+2 -6
View File
@@ -324,8 +324,7 @@ class AssetSettings(LNbitsSettings):
"heif",
"heics",
"text/plain",
"text/json",
"text/xml",
"text/json" "text/xml",
"application/json",
"application/pdf",
]
@@ -589,8 +588,6 @@ class ZBDFundingSource(LNbitsSettings):
class PhoenixdFundingSource(LNbitsSettings):
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
phoenixd_api_password: str | None = Field(default=None)
phoenixd_data_dir: str | None = Field(default=None)
phoenixd_mnemonic: str | None = Field(default=None)
class AlbyFundingSource(LNbitsSettings):
@@ -1010,6 +1007,7 @@ class EnvSettings(LNbitsSettings):
debug_database: bool = Field(default=False)
bundle_assets: bool = Field(default=True)
# When enabled, auth cookies require HTTPS and SSO will reject insecure HTTP.
# Set to false for local/dev environments that run without TLS.
auth_https_only: bool = Field(default=True)
host: str = Field(default="127.0.0.1")
port: int = Field(default=5000, gt=0)
@@ -1028,8 +1026,6 @@ class EnvSettings(LNbitsSettings):
cleanup_wallets_days: int = Field(default=90, 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
def has_default_extension_path(self) -> bool:
File diff suppressed because one or more lines are too long
+11 -11
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -191,7 +191,6 @@ window.localisation.br = {
only_admins_can_create_extensions:
'Apenas contas de administrador podem criar extensões',
admin_only: 'Apenas para Administração',
super_user_only: 'Apenas para superusuário',
make_user_admin: 'Tornar usuário administrador',
revoke_admin: 'Revogar Admin',
new_version: 'Nova Versão',
-1
View File
@@ -135,7 +135,6 @@ window.localisation.cn = {
all: '全部',
only_admins_can_install: '(只有管理员账户可以安装扩展)',
admin_only: '仅限管理员',
super_user_only: '仅限超级用户',
new_version: '新版本',
extension_depends_on: '依赖于:',
extension_rating_soon: '即将推出评分',
-1
View File
@@ -140,7 +140,6 @@ window.localisation.cs = {
only_admins_can_install:
'(Pouze administrátorské účty mohou instalovat rozšíření)',
admin_only: 'Pouze pro adminy',
super_user_only: 'Pouze pro superuživatele',
new_version: 'Nová verze',
extension_depends_on: 'Závisí na:',
extension_rating_soon: 'Hodnocení brzy dostupné',
-1
View File
@@ -143,7 +143,6 @@ window.localisation.de = {
only_admins_can_install:
'(Nur Administratorkonten können Erweiterungen installieren)',
admin_only: 'Nur für Admins',
super_user_only: 'Nur für Superuser',
new_version: 'Neue Version',
extension_depends_on: 'Hängt ab von:',
extension_rating_soon: 'Bewertungen sind bald verfügbar',
-3
View File
@@ -189,7 +189,6 @@ window.localisation.en = {
only_admins_can_create_extensions:
'Only admin accounts can create extensions',
admin_only: 'Admin Only',
super_user_only: 'Super User Only',
make_user_admin: 'Make User Admin',
revoke_admin: 'Revoke Admin',
new_version: 'New Version',
@@ -276,8 +275,6 @@ window.localisation.en = {
requires_server_restart:
'Changing these settings requires a server restart to take effect.',
funding_source_info: 'Select the active funding wallet',
phoenixd_warning:
"Phoenixd mnemonic is only available if phoenixd data-dir is specified and is readable by LNbits. It's not indicative of phoenixd not running. It just means LNbits cannot access the mnemonic to display it here.",
latest_update: 'You are on the latest version {version}.',
notifications: 'Notifications',
notifications_configure: 'Configure Notifications',
-1
View File
@@ -142,7 +142,6 @@ window.localisation.es = {
only_admins_can_install:
'(Solo las cuentas de administrador pueden instalar extensiones)',
admin_only: 'Solo administradores',
super_user_only: 'Solo superusuario',
new_version: 'Nueva Versión',
extension_depends_on: 'Depende de:',
extension_rating_soon: 'Calificaciones próximamente',
-1
View File
@@ -154,7 +154,6 @@ window.localisation.fi = {
all: 'Kaikki',
only_admins_can_install: '(Vain pääkäyttäjät voivat asentaa laajennuksia)',
admin_only: 'Pääkäyttäjille',
super_user_only: 'Vain superkäyttäjälle',
new_version: 'Uusi versio',
extension_depends_on: 'Edellyttää:',
extension_rating_soon: 'Arvostelut on tulossa pian',
-1
View File
@@ -145,7 +145,6 @@ window.localisation.fr = {
only_admins_can_install:
'Seuls les comptes administrateurs peuvent installer des extensions',
admin_only: 'Réservé aux administrateurs',
super_user_only: 'Réservé au super utilisateur',
new_version: 'Nouvelle version',
extension_depends_on: 'Dépend de :',
extension_rating_soon: 'Notes des utilisateurs à venir bientôt',
-1
View File
@@ -142,7 +142,6 @@ window.localisation.it = {
only_admins_can_install:
'Solo gli account amministratore possono installare estensioni.',
admin_only: 'Solo amministratore',
super_user_only: 'Solo superutente',
new_version: 'Nuova Versione',
extension_depends_on: 'Dipende da:',
extension_rating_soon: 'Valutazioni in arrivo',
-1
View File
@@ -137,7 +137,6 @@ window.localisation.jp = {
only_admins_can_install:
'(管理者アカウントのみが拡張機能をインストールできます)',
admin_only: '管理者のみ',
super_user_only: 'スーパー ユーザーのみ',
new_version: '新しいバージョン',
extension_depends_on: '依存先:',
extension_rating_soon: '評価は近日公開',
-1
View File
@@ -139,7 +139,6 @@ window.localisation.kr = {
all: '전체',
only_admins_can_install: '(관리자 계정만이 확장 기능을 설치할 수 있습니다)',
admin_only: '관리자 전용',
super_user_only: '슈퍼유저 전용',
new_version: '새로운 버전',
extension_depends_on: '의존성 존재:',
extension_rating_soon: '평점 기능도 곧 구현됩니다',
-1
View File
@@ -143,7 +143,6 @@ window.localisation.nl = {
only_admins_can_install:
'Alleen beheerdersaccounts kunnen extensies installeren',
admin_only: 'Alleen beheerder',
super_user_only: 'Alleen supergebruiker',
new_version: 'Nieuwe Versie',
extension_depends_on: 'Afhankelijk van:',
extension_rating_soon: 'Beoordelingen binnenkort beschikbaar',
-1
View File
@@ -141,7 +141,6 @@ window.localisation.pi = {
all: 'Arr',
only_admins_can_install: '(Only admin accounts can install extensions)',
admin_only: "Cap'n Only",
super_user_only: "Big Cap'n Only",
new_version: 'New Version',
extension_depends_on: 'Depends on:',
extension_rating_soon: "Ratings a'comin' soon",
-1
View File
@@ -140,7 +140,6 @@ window.localisation.pl = {
only_admins_can_install:
'Tylko konta administratorów mogą instalować rozszerzenia',
admin_only: 'Tylko dla administratora',
super_user_only: 'Tylko dla superużytkownika',
new_version: 'Nowa wersja',
extension_depends_on: 'Zależy od:',
extension_rating_soon: 'Oceny będą dostępne wkrótce',
-1
View File
@@ -141,7 +141,6 @@ window.localisation.pt = {
only_admins_can_install:
'Apenas contas de administrador podem instalar extensões.',
admin_only: 'Apenas para administradores',
super_user_only: 'Apenas para superusuário',
new_version: 'Nova Versão',
extension_depends_on: 'Depende de:',
extension_rating_soon: 'Avaliações em breve',
-1
View File
@@ -138,7 +138,6 @@ window.localisation.sk = {
only_admins_can_install:
'(Iba administrátorské účty môžu inštalovať rozšírenia)',
admin_only: 'Iba pre administrátorov',
super_user_only: 'Iba pre superužívateľa',
new_version: 'Nová verzia',
extension_depends_on: 'Závisí na:',
extension_rating_soon: 'Hodnotenia budú čoskoro dostupné',
-1
View File
@@ -139,7 +139,6 @@ window.localisation.we = {
all: 'Pob',
only_admins_can_install: 'Dim ond cyfrifon gweinyddwr all osod estyniadau',
admin_only: 'Dim ond Gweinyddwr',
super_user_only: 'Dim ond Uwchddefnyddiwr',
new_version: 'Fersiwn Newydd',
extension_depends_on: 'Dibynnu ar:',
extension_rating_soon: 'Sgôr yn dod yn fuan',
@@ -202,18 +202,7 @@ window.app.component('lnbits-admin-funding-sources', {
'Phoenixd',
{
phoenixd_api_endpoint: 'Endpoint',
phoenixd_api_password: 'Key',
phoenixd_data_dir: {
label: 'Data Directory',
hint: 'Directory where phoenixd stores its data, including the seed phrase.'
},
phoenixd_mnemonic: {
label: 'Phoenixd Seed Phrase',
hint: 'Only available if phoenixd data-dir is specified',
readonly: true,
copy: true,
qrcode: true
}
phoenixd_api_password: 'Key'
}
],
[
@@ -41,37 +41,5 @@ window.app.component('lnbits-admin-site-customisation', {
]
}
},
methods: {
onBackgroundImageInput(e) {
const file = e.target.files[0]
if (file) {
this.uploadBackgroundImage(file)
}
e.target.value = null
},
async uploadBackgroundImage(file) {
const formData = new FormData()
formData.append('file', file)
try {
const {data} = await LNbits.api.request(
'POST',
'/api/v1/assets?public_asset=true',
null,
formData,
{
headers: {'Content-Type': 'multipart/form-data'}
}
)
const assetUrl = `${window.location.origin}/api/v1/assets/${data.id}/thumbnail`
this.formData.lnbits_default_bgimage = assetUrl
Quasar.Notify.create({
type: 'positive',
message: 'Background image uploaded.',
icon: null
})
} catch (e) {
LNbits.utils.notifyApiError(e)
}
}
}
methods: {}
})
+8 -33
View File
@@ -545,56 +545,31 @@ window.PageAccount = {
if (file) {
this.uploadAsset(file)
}
e.target.value = null
},
onBackgroundImageInput(e) {
const file = e.target.files[0]
if (file) {
this.uploadBackgroundImage(file)
}
e.target.value = null
},
async uploadAsset(
file,
{isPublic = this.assetsUploadToPublic, notifySuccess = true} = {}
) {
async uploadAsset(file) {
const formData = new FormData()
formData.append('file', file)
try {
const {data} = await LNbits.api.request(
await LNbits.api.request(
'POST',
`/api/v1/assets?public_asset=${isPublic}`,
`/api/v1/assets?public_asset=${this.assetsUploadToPublic}`,
null,
formData,
{
headers: {'Content-Type': 'multipart/form-data'}
}
)
if (notifySuccess) {
this.$q.notify({
type: 'positive',
message: 'Upload successful!',
icon: null
})
}
this.$q.notify({
type: 'positive',
message: 'Upload successful!',
icon: null
})
await this.getUserAssets()
return data
} catch (e) {
console.warn(e)
LNbits.utils.notifyApiError(e)
}
},
async uploadBackgroundImage(file) {
const asset = await this.uploadAsset(file, {
isPublic: false,
notifySuccess: false
})
if (!asset) {
return
}
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/thumbnail`
await this.siteCustomisationChanged({bgimageChoice: assetUrl})
},
async deleteAsset(asset) {
LNbits.utils
.confirmDialog('Are you sure you want to delete this asset?')
+153 -294
View File
File diff suppressed because it is too large Load Diff
+29 -29
View File
@@ -75,35 +75,7 @@
</p>
</div>
</div>
<div v-if="isSuperUser">
<lnbits-admin-funding-sources
:form-data="formData"
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/>
<div class="row q-col-gutter-md q-my-md">
<div class="col-12 col-sm-8">
<q-item tag="div">
<q-item-section>
<q-item-label
v-text="$t('funding_source_retries')"
></q-item-label>
<q-item-label
caption
v-text="$t('funding_source_retries_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.funding_source_max_retries"
type="number"
/>
</q-item-section>
</q-item>
</div>
</div>
</div>
<div class="row q-col-gutter-md q-mt-lg">
<div class="row q-col-gutter-md">
<div class="col-12">
<h6 class="q-my-none">
<span v-text="$t('routing_fee_reserve_calculations')"></span>
@@ -210,6 +182,34 @@
></q-input>
</div>
</div>
<div v-if="isSuperUser">
<lnbits-admin-funding-sources
:form-data="formData"
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/>
<div class="row q-col-gutter-md q-my-md">
<div class="col-12 col-sm-8">
<q-item tag="div">
<q-item-section>
<q-item-label
v-text="$t('funding_source_retries')"
></q-item-label>
<q-item-label
caption
v-text="$t('funding_source_retries_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.funding_source_max_retries"
type="number"
/>
</q-item-section>
</q-item>
</div>
</div>
</div>
<q-separator></q-separator>
<h6 class="q-mt-lg q-mb-sm">
<p v-text="$t('watchdog')"></p>
@@ -52,7 +52,6 @@
:label="prop.label"
:hint="prop.hint"
:value="prop.value"
:readonly="prop.readonly || false"
>
<q-btn
v-if="prop.copy"
@@ -74,15 +73,6 @@
></q-btn>
</q-input>
</div>
<p
v-if="fund === 'PhoenixdWallet' && key === 'phoenixd_mnemonic'"
class="col-12 q-my-md"
>
<span>
<q-icon name="warning" color="orange" size="xs"></q-icon>
<span v-text="$t('phoenixd_warning')"></span>
</span>
</p>
</div>
<q-expansion-item
v-if="
@@ -251,27 +251,10 @@
type="text"
v-model="formData.lnbits_default_bgimage"
label="Background Image"
@update:model-value="applyGlobalBgimage"
hint="This must be a trusted source. It can change the content and it can log your IP address."
>
<template v-slot:append>
<q-btn
dense
flat
round
icon="upload"
@click="$refs.adminBackgroundImageInput.click()"
>
<q-tooltip>Upload background image</q-tooltip>
</q-btn>
</template>
</q-input>
<input
type="file"
ref="adminBackgroundImageInput"
accept="image/*"
style="display: none"
@change="onBackgroundImageInput"
/>
</div>
</div>
<div class="row q-col-gutter-md q-mb-md">
+68 -134
View File
@@ -492,28 +492,10 @@
siteCustomisationChanged({bgimageChoice: $event})
"
>
<template v-slot:append>
<q-btn
dense
flat
round
icon="upload"
@click="$refs.backgroundImageInput.click()"
>
<q-tooltip>Upload background image</q-tooltip>
</q-btn>
</template>
<q-tooltip
><span v-text="$t('background_image')"></span
></q-tooltip>
</q-input>
<input
type="file"
ref="backgroundImageInput"
accept="image/*"
style="display: none"
@change="onBackgroundImageInput"
/>
</div>
</div>
<div class="row q-mb-md">
@@ -1126,130 +1108,82 @@
<q-separator></q-separator>
<q-card-section>
<div
class="row items-center no-wrap q-col-gutter-sm"
<q-btn-dropdown
color="grey"
dense
outline
no-caps
:label="props.row.name"
:icon="props.row.is_public ? 'public' : ''"
>
<div class="col">
<q-btn-dropdown
color="grey"
dense
outline
no-caps
class="full-width"
:label="props.row.name"
:icon="props.row.is_public ? 'public' : ''"
<q-list>
<q-item
clickable
v-close-popup
@click="copyAssetLinkToClipboard(props.row)"
>
<q-list>
<q-item
clickable
v-close-popup
@click="
copyAssetLinkToClipboard(props.row)
"
<q-item-section avatar>
<q-avatar icon="content_copy" />
</q-item-section>
<q-item-section>
<q-item-label>Copy Link</q-item-label>
<q-item-label caption
>Copy asset link to
clipboard</q-item-label
>
<q-item-section avatar>
<q-avatar icon="content_copy" />
</q-item-section>
<q-item-section>
<q-item-label>Copy Link</q-item-label>
<q-item-label caption
>Copy asset link to
clipboard</q-item-label
>
</q-item-section>
</q-item>
</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="
toggleAssetPublicAccess(props.row)
<q-item
clickable
v-close-popup
@click="toggleAssetPublicAccess(props.row)"
>
<q-item-section avatar>
<q-avatar
:icon="
props.row.is_public
? 'public_off'
: 'public'
"
text-color="primary"
/>
</q-item-section>
<q-item-section v-if="props.row.is_public">
<q-item-label>Unpublish</q-item-label>
<q-item-label caption
>Make this asset private</q-item-label
>
<q-item-section avatar>
<q-avatar
:icon="
props.row.is_public
? 'public_off'
: 'public'
"
text-color="primary"
/>
</q-item-section>
<q-item-section
v-if="props.row.is_public"
>
<q-item-label>Unpublish</q-item-label>
<q-item-label caption
>Make this asset
private</q-item-label
>
</q-item-section>
<q-item-section v-else>
<q-item-label>Publish</q-item-label>
<q-item-label caption
>Make this asset
public</q-item-label
>
</q-item-section>
</q-item>
</q-item-section>
<q-item-section v-else>
<q-item-label>Publish</q-item-label>
<q-item-label caption
>Make this asset public</q-item-label
>
</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="deleteAsset(props.row)"
<q-item
clickable
v-close-popup
@click="deleteAsset(props.row)"
>
<q-item-section avatar>
<q-avatar
icon="delete"
text-color="negative"
/>
</q-item-section>
<q-item-section>
<q-item-label>Delete</q-item-label>
<q-item-label caption
>Permanently delete this
asset</q-item-label
>
<q-item-section avatar>
<q-avatar
icon="delete"
text-color="negative"
/>
</q-item-section>
<q-item-section>
<q-item-label>Delete</q-item-label>
<q-item-label caption
>Permanently delete this
asset</q-item-label
>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
<div class="col-auto">
<q-btn
type="a"
target="_blank"
rel="noopener noreferrer"
color="primary"
dense
flat
round
icon="image"
:href="`/api/v1/assets/${props.row.id}/data`"
>
<q-tooltip>Full image</q-tooltip>
</q-btn>
</div>
<div
class="col-auto"
v-if="props.row.thumbnail_base64"
>
<q-btn
type="a"
target="_blank"
rel="noopener noreferrer"
color="secondary"
dense
flat
round
icon="photo_size_select_small"
:href="`/api/v1/assets/${props.row.id}/thumbnail`"
>
<q-tooltip>Thumbnail</q-tooltip>
</q-btn>
</div>
</div>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-card-section>
</q-card>
</div>
+2 -10
View File
@@ -307,13 +307,7 @@
:label="$t('disable')"
></q-btn>
<q-badge
v-if="extension.isSuperUserOnly && !g.user.super_user"
v-text="$t('super_user_only')"
>
</q-badge>
<q-badge
v-else-if="extension.isAdminOnly && !g.user.admin"
v-if="extension.isAdminOnly && !g.user.admin"
v-text="$t('admin_only')"
>
</q-badge>
@@ -322,9 +316,7 @@
v-else-if="
extension.isInstalled &&
extension.isActive &&
!g.user.extensions.includes(extension.id) &&
(!extension.isAdminOnly || g.user.admin) &&
(!extension.isSuperUserOnly || g.user.super_user)
!g.user.extensions.includes(extension.id)
"
flat
color="primary"
+2 -52
View File
@@ -4,11 +4,9 @@ import hashlib
import json
import urllib.parse
from collections.abc import AsyncGenerator
from pathlib import Path
from typing import Any
import httpx
from embit.bip39 import mnemonic_is_valid
from httpx import RequestError, TimeoutException
from loguru import logger
from websockets import connect
@@ -63,8 +61,6 @@ class PhoenixdWallet(Wallet):
}
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
self._seed_mnemonic_to_persist: str | None = None
self._load_mnemonic_from_seed_file()
async def cleanup(self):
try:
@@ -73,7 +69,6 @@ class PhoenixdWallet(Wallet):
logger.warning(f"Error closing wallet connection: {e}")
async def status(self) -> StatusResponse:
await self._persist_loaded_mnemonic()
try:
r = await self.client.get("/getinfo", timeout=10)
r.raise_for_status()
@@ -106,6 +101,7 @@ class PhoenixdWallet(Wallet):
unhashed_description: bytes | None = None,
**kwargs,
) -> InvoiceResponse:
try:
msats_amount = amount
data: dict[str, Any] = {
@@ -313,7 +309,7 @@ class PhoenixdWallet(Wallet):
and message_json.get("type") == "payment_received"
):
logger.info(
f"payment-received: {message_json['paymentHash']}"
f'payment-received: {message_json["paymentHash"]}'
)
yield message_json["paymentHash"]
@@ -323,49 +319,3 @@ class PhoenixdWallet(Wallet):
"retrying in 5 seconds"
)
await asyncio.sleep(5)
def _load_mnemonic_from_seed_file(self):
data_dir = settings.phoenixd_data_dir
if not data_dir:
return
seed_path = Path(data_dir).expanduser() / "seed.dat"
if not seed_path.is_file():
return
try:
mnemonic = seed_path.read_text(encoding="utf-8").strip()
if mnemonic == settings.phoenixd_mnemonic:
return
except OSError as exc:
logger.warning(f"Failed to read Phoenixd seed file '{seed_path}': {exc}")
return
if not mnemonic:
logger.warning(f"Phoenixd seed file '{seed_path}' is empty.")
return
if not mnemonic_is_valid(mnemonic):
logger.warning(
f"Phoenixd seed file '{seed_path}' does not contain a valid "
"BIP39 mnemonic."
)
return
settings.phoenixd_mnemonic = mnemonic
self._seed_mnemonic_to_persist = mnemonic
async def _persist_loaded_mnemonic(self):
if not self._seed_mnemonic_to_persist:
return
logger.info("Updating 'PHOENIXD_MNEMONIC' mnemonic settings.")
try:
from lnbits.core.crud.settings import set_settings_field
await set_settings_field(
"phoenixd_mnemonic", self._seed_mnemonic_to_persist
)
self._seed_mnemonic_to_persist = None
except Exception as exc:
logger.warning(f"Failed to persist Phoenixd mnemonic: {exc}")
+8 -12
View File
@@ -6,7 +6,7 @@
"": {
"name": "lnbits",
"dependencies": {
"axios": "^1.15.0",
"axios": "^1.13.5",
"chart.js": "^4.5.1",
"moment": "^2.30.1",
"nostr-tools": "^2.18.2",
@@ -734,14 +734,14 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
@@ -1555,13 +1555,9 @@
}
},
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pyright": {
"version": "1.1.289",
+1 -1
View File
@@ -21,7 +21,7 @@
"terser": "^5.44.1"
},
"dependencies": {
"axios": "^1.15.0",
"axios": "^1.13.5",
"chart.js": "^4.5.1",
"moment": "^2.30.1",
"nostr-tools": "^2.18.2",
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.5.4"
version = "1.5.3"
requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
-42
View File
@@ -135,8 +135,6 @@ async def test_auth_api_first_install_success_and_validation(
)
assert success.status_code == 200
assert success.json()["access_token"]
assert "cookie_access_token=" in success.headers["set-cookie"]
assert "Secure" not in success.headers["set-cookie"]
updated_superuser = await get_account(settings.super_user, active_only=False)
assert updated_superuser is not None
@@ -161,43 +159,3 @@ async def test_auth_api_first_install_success_and_validation(
await update_account(restored_superuser)
settings.first_install = original_first_install
settings.first_install_token = original_first_install_token
@pytest.mark.anyio
async def test_auth_api_first_install_uses_secure_cookie_when_enabled(
http_client: AsyncClient, settings: Settings
):
superuser = await get_account(settings.super_user, active_only=False)
assert superuser is not None
original_username = superuser.username
original_password_hash = superuser.password_hash
original_first_install = settings.first_install
original_auth_https_only = settings.auth_https_only
new_username = f"secure_{uuid4().hex[:8]}"
try:
settings.first_install = True
settings.auth_https_only = True
success = await http_client.put(
"/api/v1/auth/first_install",
json={
"username": new_username,
"password": "secret1234",
"password_repeat": "secret1234",
},
)
assert success.status_code == 200
assert "cookie_access_token=" in success.headers["set-cookie"]
assert "Secure" in success.headers["set-cookie"]
finally:
restored_superuser = await get_account(settings.super_user, active_only=False)
assert restored_superuser is not None
restored_superuser.username = original_username
restored_superuser.password_hash = original_password_hash
await update_account(restored_superuser)
settings.first_install = original_first_install
settings.auth_https_only = original_auth_https_only
-71
View File
@@ -20,9 +20,7 @@ from lnbits.core.models.extensions import (
Extension,
ExtensionConfig,
ExtensionRelease,
GitHubRelease,
InstallableExtension,
Manifest,
PayToEnableInfo,
ReleasePaymentInfo,
UserExtensionInfo,
@@ -154,8 +152,6 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
short_description="Config",
min_lnbits_version="0.1.0",
max_lnbits_version=None,
admin_only=True,
super_user_only=True,
)
mocker.patch.object(
ExtensionConfig,
@@ -164,8 +160,6 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
)
release_info = await get_extension_release("org", ext_id, "v1.0.0")
assert release_info["is_version_compatible"] is True
assert release_info["admin_only"] is True
assert release_info["super_user_only"] is True
@pytest.mark.anyio
@@ -264,8 +258,6 @@ async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user)
catalog_entry = make_installable_extension(
ext_id,
pay_to_enable=PayToEnableInfo(required=True, amount=21, wallet=admin_wallet.id),
admin_only=True,
super_user_only=True,
)
mocker.patch.object(
InstallableExtension,
@@ -275,8 +267,6 @@ async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user)
catalog = await extensions(AccountId(id=regular_user.id))
catalog_item = next(item for item in catalog if item["id"] == ext_id)
assert catalog_item["payToEnable"]["wallet"] is None
assert catalog_item["isAdminOnly"] is True
assert catalog_item["isSuperUserOnly"] is True
@pytest.mark.anyio
@@ -438,64 +428,3 @@ async def test_extension_api_review_endpoints(mocker):
CreateExtensionReview(tag=ext_id, name="Alice", rating=900, comment="Great")
)
assert payment_request.payment_hash.startswith("hash_")
@pytest.mark.anyio
async def test_extension_api_enable_rejects_admin_and_super_only_extensions(
admin_user,
):
regular_user = await create_user_account(
Account(
id=uuid4().hex,
username=f"user_{uuid4().hex[:8]}",
email=f"user_{uuid4().hex[:8]}@lnbits.com",
)
)
admin_only_ext = f"admin_{uuid4().hex[:8]}"
super_only_ext = f"super_{uuid4().hex[:8]}"
await create_installed_extension(
make_installable_extension(admin_only_ext, admin_only=True)
)
await create_installed_extension(
make_installable_extension(super_only_ext, super_user_only=True)
)
with pytest.raises(HTTPException, match="User not authorized"):
await api_enable_extension(admin_only_ext, AccountId(id=regular_user.id))
with pytest.raises(HTTPException, match="User not authorized"):
await api_enable_extension(super_only_ext, AccountId(id=admin_user.id))
@pytest.mark.anyio
async def test_repo_manifest_flags_apply_to_repo_releases(mocker):
ext_id = f"repo_{uuid4().hex[:8]}"
release = make_extension_release(ext_id)
manifest = Manifest(
repos=[
GitHubRelease(
id=ext_id,
organisation="lnbits",
repository="tunnel_me_out",
admin_only=True,
super_user_only=True,
)
]
)
mocker.patch.object(
InstallableExtension,
"fetch_manifest",
mocker.AsyncMock(return_value=manifest),
)
mocker.patch.object(
ExtensionRelease,
"get_github_releases",
mocker.AsyncMock(return_value=[release]),
)
releases = await InstallableExtension.get_extension_releases(ext_id)
assert len(releases) == 1
assert releases[0].admin_only is True
assert releases[0].super_user_only is True
+1 -3
View File
@@ -40,9 +40,7 @@ from tests.helpers import (
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
ADMIN_USER_ID = uuid4().hex
# Snapshot the initialized module settings instead of a fresh Settings() instance.
# The module settings include runtime-populated values like `version`.
_PURE_SETTINGS = copy.deepcopy(lnbits_settings)
_PURE_SETTINGS = Settings()
_PURE_SETTINGS_FIELDS = tuple(
sorted(
{
+1 -9
View File
@@ -28,7 +28,7 @@ from lnbits.core.models.extensions_builder import (
PublicPageFields,
SettingsFields,
)
from lnbits.wallets import get_funding_source, set_funding_source
from lnbits.wallets import get_funding_source
class DbTestModel(BaseModel):
@@ -141,13 +141,9 @@ def make_installable_extension(
pay_to_enable: PayToEnableInfo | None = None,
dependencies: list[str] | None = None,
payments: list[ReleasePaymentInfo] | None = None,
admin_only: bool = False,
super_user_only: bool = False,
) -> InstallableExtension:
release = make_extension_release(ext_id, version)
release.is_version_compatible = compatible
release.admin_only = admin_only
release.super_user_only = super_user_only
return InstallableExtension(
id=ext_id,
name=f"Extension {ext_id}",
@@ -160,8 +156,6 @@ def make_installable_extension(
pay_to_enable=pay_to_enable,
dependencies=dependencies or [],
payments=payments or [],
admin_only=admin_only,
super_user_only=super_user_only,
),
)
@@ -188,8 +182,6 @@ def make_lnurl_pay_response(
)
set_funding_source()
funding_source = get_funding_source()
is_fake: bool = funding_source.__class__.__name__ == "FakeWallet"
is_regtest: bool = not is_fake
+9 -1
View File
@@ -9,7 +9,15 @@ from loguru import logger
from lnbits.wallets import get_funding_source
funding_source = get_funding_source()
is_boltz_wallet = funding_source.__class__.__name__ == "BoltzWallet"
def is_boltz_wallet():
print(
"### funding_source.__class__.__name__ 2",
get_funding_source().__class__.__name__,
)
return get_funding_source().__class__.__name__ == "BoltzWallet"
docker_lightning_cli = [
"docker",
@@ -21,7 +21,8 @@ async def test_create_invoice(from_wallet):
)
# we cannot know the preimage of the swap yet
if not is_boltz_wallet:
funding_source = get_funding_source()
if not is_boltz_wallet():
assert payment.preimage
invoice = decode(payment.bolt11)
@@ -42,7 +43,8 @@ async def test_create_internal_invoice(from_wallet):
)
# we cannot know the preimage of the swap yet
if not is_boltz_wallet:
funding_source = get_funding_source()
if not is_boltz_wallet():
assert payment.preimage
invoice = decode(payment.bolt11)
+1 -1
View File
@@ -20,7 +20,7 @@ async def test_services_pay_invoice(to_wallet, real_invoice):
)
assert payment
assert payment.memo == description
if not is_boltz_wallet:
if not is_boltz_wallet():
assert payment.status == PaymentState.SUCCESS
assert payment.preimage
else:
-37
View File
@@ -8,7 +8,6 @@ from fastapi.exceptions import HTTPException
from httpx import AsyncClient
from pydantic.types import UUID4
from lnbits.core.crud.extensions import create_installed_extension
from lnbits.core.crud.users import delete_account
from lnbits.core.models import User
from lnbits.core.models.users import AccessTokenPayload
@@ -19,12 +18,10 @@ from lnbits.decorators import (
check_extension_builder,
check_first_install,
check_user_exists,
check_user_extension_access,
optional_user_id,
)
from lnbits.helpers import create_access_token
from lnbits.settings import AuthMethods, Settings, settings
from tests.helpers import make_installable_extension
@pytest.mark.anyio
@@ -228,37 +225,3 @@ async def test_check_extension_builder_requires_admin_when_disabled_for_users(
admin_user = user_alan.copy(deep=True)
admin_user.admin = True
await check_extension_builder(admin_user)
@pytest.mark.anyio
async def test_check_user_extension_access_honors_extension_metadata(
settings: Settings, user_alan: User, admin_user: User
):
admin_only_ext = f"admin_{uuid4().hex[:8]}"
super_only_ext = f"super_{uuid4().hex[:8]}"
await create_installed_extension(
make_installable_extension(admin_only_ext, admin_only=True)
)
await create_installed_extension(
make_installable_extension(super_only_ext, super_user_only=True)
)
regular_status = await check_user_extension_access(user_alan.id, admin_only_ext)
assert regular_status.success is False
admin_status = await check_user_extension_access(admin_user.id, admin_only_ext)
assert admin_status.success is True
admin_super_status = await check_user_extension_access(
admin_user.id, super_only_ext
)
assert admin_super_status.success is False
previous_super_user = settings.super_user
settings.super_user = admin_user.id
try:
super_status = await check_user_extension_access(admin_user.id, super_only_ext)
assert super_status.success is True
finally:
settings.super_user = previous_super_user
+1 -6
View File
@@ -252,12 +252,7 @@ async def test_notification_for_internal_payment(
assert _payment.bolt11 == payment.bolt11
assert _payment.amount == 123_000
updated_payment = await get_payment(_payment.checking_id)
assert (
updated_payment.webhook_status is not None
), "Webhook should have been called."
assert (
int(updated_payment.webhook_status) >= 400
), "Webhook should have been called and failed."
assert updated_payment.webhook_status == "404"
@pytest.mark.anyio
+1 -1
View File
@@ -197,7 +197,7 @@ async def test_update_wallet_balance_validates_credit_and_debit(
settings.lnbits_wallet_limit_max_balance = 0
queue_mock = mocker.patch(
"lnbits.tasks.internal_invoice_queue_put",
"lnbits.core.services.payments.internal_invoice_queue_put",
mocker.AsyncMock(),
)
Generated
+10 -10
View File
@@ -1263,7 +1263,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.5.4"
version = "1.5.3"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -2022,11 +2022,11 @@ email = [
[[package]]
name = "pygments"
version = "2.20.0"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
@@ -2114,7 +2114,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.0.3"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -2125,9 +2125,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
@@ -2213,11 +2213,11 @@ wheels = [
[[package]]
name = "python-multipart"
version = "0.0.26"
version = "0.0.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
{ url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
]
[[package]]