Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa1fec87c7 | ||
|
|
74a1dd5ea6 | ||
|
|
ef47f1660d | ||
|
|
4341c329fd | ||
|
|
fdf3ab2688 | ||
|
|
99e4f33142 | ||
|
|
f4f43ad361 | ||
|
|
f14ea6c577 | ||
|
|
07428ecf94 | ||
|
|
867e3d06f6 | ||
|
|
4b4b6d0bcd | ||
|
|
07b1521dad | ||
|
|
7a2ddd9826 | ||
|
|
0eb4b477b7 | ||
|
|
06a0ba58ce | ||
|
|
5399b36027 |
+5
-1
@@ -7,9 +7,13 @@
|
|||||||
# They are NOT managed by the Admin UI and are not stored in the database.
|
# They are NOT managed by the Admin UI and are not stored in the database.
|
||||||
|
|
||||||
# === First Install Token ===
|
# === 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"
|
# 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 ===
|
# === Logging and Development ===
|
||||||
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
<source media="(prefers-color-scheme: dark)" srcset="docs/logos/lnbits-full-inverse.svg">
|
||||||
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
|
<img src="docs/logos/lnbits-full.svg" alt="LNbits" style="width:300px">
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](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)
|
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](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 width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
<img alt="lnbits_head" src="docs/assets/header.jpg" />
|
||||||
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||||
|
|
||||||
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
# 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.
|
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 src="https://i.imgur.com/w8jdGpF.png" style="width:800px">
|
<img alt="lnbits_wallet" src="docs/assets/wallet.jpg" />
|
||||||
|
|
||||||
## LNbits extension universe
|
## 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.
|
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 src="https://i.imgur.com/aEBpwJF.png" style="width:800px">
|
<img alt="lnbits_extensions" src="docs/assets/extensions.jpg" />
|
||||||
|
|
||||||
## LNbits API
|
## LNbits API
|
||||||
|
|
||||||
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
|
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/V742sb9.png" style="width:800px">
|
<img alt="lnbits_api" src="docs/assets/api.jpg" />
|
||||||
|
|
||||||
## LNbits node manager
|
## LNbits node manager
|
||||||
|
|
||||||
LNbits comes packaged with a light node management UI, to make running your node that much easier.
|
LNbits comes packaged with a light node management UI, to make running your node that much easier.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/TYqIK60.png" style="width:800px">
|
<img alt="lnbits_api" src="docs/assets/lightning_node.jpg" />
|
||||||
|
|
||||||
## LNbits across all your devices
|
## LNbits merchant tools
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/J96EbRf.png" style="width:800px">
|
<img alt="lnbits_merchants" src="docs/assets/merchants_small.webp" />
|
||||||
|
|
||||||
## Powered by LNbits
|
## Powered by LNbits
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 317 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
@@ -43,6 +43,8 @@ class ExplicitRelease(BaseModel):
|
|||||||
details_link: str | None
|
details_link: str | None
|
||||||
paid_features: str | None
|
paid_features: str | None
|
||||||
pay_link: str | None
|
pay_link: str | None
|
||||||
|
admin_only: bool = False
|
||||||
|
super_user_only: bool = False
|
||||||
|
|
||||||
def is_version_compatible(self):
|
def is_version_compatible(self):
|
||||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
@@ -52,6 +54,8 @@ class GitHubRelease(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
organisation: str
|
organisation: str
|
||||||
repository: str
|
repository: str
|
||||||
|
admin_only: bool = False
|
||||||
|
super_user_only: bool = False
|
||||||
|
|
||||||
|
|
||||||
class Manifest(BaseModel):
|
class Manifest(BaseModel):
|
||||||
@@ -83,6 +87,8 @@ class ExtensionConfig(BaseModel):
|
|||||||
warning: str | None = ""
|
warning: str | None = ""
|
||||||
min_lnbits_version: str | None
|
min_lnbits_version: str | None
|
||||||
max_lnbits_version: str | None
|
max_lnbits_version: str | None
|
||||||
|
admin_only: bool = False
|
||||||
|
super_user_only: bool = False
|
||||||
|
|
||||||
def is_version_compatible(self) -> bool:
|
def is_version_compatible(self) -> bool:
|
||||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
@@ -195,6 +201,8 @@ class ExtensionRelease(BaseModel):
|
|||||||
cost_sats: int | None = None
|
cost_sats: int | None = None
|
||||||
paid_sats: int | None = 0
|
paid_sats: int | None = 0
|
||||||
payment_hash: str | None = None
|
payment_hash: str | None = None
|
||||||
|
admin_only: bool = False
|
||||||
|
super_user_only: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def archive_url(self) -> str:
|
def archive_url(self) -> str:
|
||||||
@@ -263,6 +271,8 @@ class ExtensionRelease(BaseModel):
|
|||||||
paid_features=e.paid_features,
|
paid_features=e.paid_features,
|
||||||
repo=e.repo,
|
repo=e.repo,
|
||||||
icon=e.icon,
|
icon=e.icon,
|
||||||
|
admin_only=e.admin_only,
|
||||||
|
super_user_only=e.super_user_only,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -289,6 +299,8 @@ class ExtensionRelease(BaseModel):
|
|||||||
release.min_lnbits_version = config.min_lnbits_version
|
release.min_lnbits_version = config.min_lnbits_version
|
||||||
release.max_lnbits_version = config.max_lnbits_version
|
release.max_lnbits_version = config.max_lnbits_version
|
||||||
release.is_version_compatible = config.is_version_compatible()
|
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)
|
release.icon = icon_to_github_url(f"{org}/{repo}", config.tile)
|
||||||
|
|
||||||
@@ -336,6 +348,8 @@ class ExtensionMeta(BaseModel):
|
|||||||
paid_features: str | None = None
|
paid_features: str | None = None
|
||||||
has_paid_release: bool = False
|
has_paid_release: bool = False
|
||||||
has_free_release: bool = False
|
has_free_release: bool = False
|
||||||
|
admin_only: bool = False
|
||||||
|
super_user_only: bool = False
|
||||||
|
|
||||||
|
|
||||||
class InstallableExtension(BaseModel):
|
class InstallableExtension(BaseModel):
|
||||||
@@ -399,6 +413,16 @@ class InstallableExtension(BaseModel):
|
|||||||
return False
|
return False
|
||||||
return self.meta.pay_to_enable.required is True
|
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):
|
async def download_archive(self):
|
||||||
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
|
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
|
||||||
ext_zip_file = self.zip_path
|
ext_zip_file = self.zip_path
|
||||||
@@ -454,6 +478,16 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
self.name = config_json.get("name")
|
self.name = config_json.get("name")
|
||||||
self.short_description = config_json.get("short_description")
|
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 (
|
if (
|
||||||
self.meta
|
self.meta
|
||||||
@@ -496,6 +530,10 @@ class InstallableExtension(BaseModel):
|
|||||||
return
|
return
|
||||||
if not release.is_version_compatible:
|
if not release.is_version_compatible:
|
||||||
return
|
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:
|
if not self.meta or not self.meta.latest_release:
|
||||||
meta = self.meta or ExtensionMeta()
|
meta = self.meta or ExtensionMeta()
|
||||||
meta.latest_release = release
|
meta.latest_release = release
|
||||||
@@ -558,6 +596,13 @@ class InstallableExtension(BaseModel):
|
|||||||
github_release.organisation, github_release.repository
|
github_release.organisation, github_release.repository
|
||||||
)
|
)
|
||||||
source_repo = f"{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(
|
return InstallableExtension(
|
||||||
id=github_release.id,
|
id=github_release.id,
|
||||||
name=config.name,
|
name=config.name,
|
||||||
@@ -569,9 +614,9 @@ class InstallableExtension(BaseModel):
|
|||||||
config.tile,
|
config.tile,
|
||||||
),
|
),
|
||||||
meta=ExtensionMeta(
|
meta=ExtensionMeta(
|
||||||
latest_release=ExtensionRelease.from_github_release(
|
admin_only=admin_only,
|
||||||
source_repo, latest_release
|
super_user_only=super_user_only,
|
||||||
),
|
latest_release=latest_extension_release,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -580,7 +625,12 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension:
|
def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension:
|
||||||
meta = ExtensionMeta(archive=e.archive, dependencies=e.dependencies)
|
meta = ExtensionMeta(
|
||||||
|
archive=e.archive,
|
||||||
|
dependencies=e.dependencies,
|
||||||
|
admin_only=e.admin_only,
|
||||||
|
super_user_only=e.super_user_only,
|
||||||
|
)
|
||||||
return InstallableExtension(
|
return InstallableExtension(
|
||||||
id=e.id,
|
id=e.id,
|
||||||
name=e.name,
|
name=e.name,
|
||||||
@@ -610,6 +660,8 @@ class InstallableExtension(BaseModel):
|
|||||||
short_description=config_json.get("short_description"),
|
short_description=config_json.get("short_description"),
|
||||||
icon=config_json.get("tile"),
|
icon=config_json.get("tile"),
|
||||||
meta=ExtensionMeta(
|
meta=ExtensionMeta(
|
||||||
|
admin_only=config_json.get("admin_only", False),
|
||||||
|
super_user_only=config_json.get("super_user_only", False),
|
||||||
installed_release=ExtensionRelease(
|
installed_release=ExtensionRelease(
|
||||||
name=ext_id,
|
name=ext_id,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -617,7 +669,9 @@ class InstallableExtension(BaseModel):
|
|||||||
source_repo=f"{conf_path}",
|
source_repo=f"{conf_path}",
|
||||||
min_lnbits_version=config_json.get("min_lnbits_version"),
|
min_lnbits_version=config_json.get("min_lnbits_version"),
|
||||||
max_lnbits_version=config_json.get("max_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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -719,6 +773,13 @@ class InstallableExtension(BaseModel):
|
|||||||
repo_releases = await ExtensionRelease.get_github_releases(
|
repo_releases = await ExtensionRelease.get_github_releases(
|
||||||
r.organisation, r.repository
|
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
|
extension_releases += repo_releases
|
||||||
|
|
||||||
for e in manifest.extensions:
|
for e in manifest.extensions:
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ async def api_install_extension(data: CreateExtension):
|
|||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Incompatible extension version.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Incompatible extension version.")
|
||||||
|
|
||||||
release.payment_hash = data.payment_hash
|
release.payment_hash = data.payment_hash
|
||||||
ext_meta = ExtensionMeta(installed_release=release)
|
ext_meta = ExtensionMeta(
|
||||||
|
installed_release=release,
|
||||||
|
admin_only=release.admin_only,
|
||||||
|
super_user_only=release.super_user_only,
|
||||||
|
)
|
||||||
ext_info = InstallableExtension(
|
ext_info = InstallableExtension(
|
||||||
id=data.ext_id,
|
id=data.ext_id,
|
||||||
name=data.ext_id,
|
name=data.ext_id,
|
||||||
@@ -176,6 +180,19 @@ 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")
|
@extension_router.put("/{ext_id}/enable")
|
||||||
async def api_enable_extension(
|
async def api_enable_extension(
|
||||||
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||||
@@ -191,6 +208,7 @@ async def api_enable_extension(
|
|||||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||||
if not ext.active:
|
if not ext.active:
|
||||||
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
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)
|
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||||
if not user_ext:
|
if not user_ext:
|
||||||
@@ -464,6 +482,8 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
|
|||||||
"min_lnbits_version": config.min_lnbits_version,
|
"min_lnbits_version": config.min_lnbits_version,
|
||||||
"is_version_compatible": config.is_version_compatible(),
|
"is_version_compatible": config.is_version_compatible(),
|
||||||
"warning": config.warning,
|
"warning": config.warning,
|
||||||
|
"admin_only": config.admin_only,
|
||||||
|
"super_user_only": config.super_user_only,
|
||||||
}
|
}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -573,7 +593,8 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
(True for version in db_versions if version.db == ext.id), False
|
(True for version in db_versions if version.db == ext.id), False
|
||||||
),
|
),
|
||||||
"isAvailable": ext.id in all_ext_ids,
|
"isAvailable": ext.id in all_ext_ids,
|
||||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
"isAdminOnly": ext.is_admin_only,
|
||||||
|
"isSuperUserOnly": ext.is_super_user_only,
|
||||||
"isActive": ext.id not in inactive_extensions,
|
"isActive": ext.id not in inactive_extensions,
|
||||||
"latestRelease": (
|
"latestRelease": (
|
||||||
dict(ext.meta.latest_release)
|
dict(ext.meta.latest_release)
|
||||||
|
|||||||
+14
-1
@@ -14,6 +14,7 @@ from lnbits.core.crud import (
|
|||||||
get_account,
|
get_account,
|
||||||
get_account_by_email,
|
get_account_by_email,
|
||||||
get_account_by_username,
|
get_account_by_username,
|
||||||
|
get_installed_extension,
|
||||||
get_user_active_extensions_ids,
|
get_user_active_extensions_ids,
|
||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
@@ -424,7 +425,19 @@ async def check_user_extension_access(
|
|||||||
Check if the user has access to a particular extension.
|
Check if the user has access to a particular extension.
|
||||||
Raises HTTP Forbidden if the user is not allowed.
|
Raises HTTP Forbidden if the user is not allowed.
|
||||||
"""
|
"""
|
||||||
if settings.is_admin_extension(ext_id) and not settings.is_admin_user(user_id):
|
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):
|
||||||
return SimpleStatus(
|
return SimpleStatus(
|
||||||
success=False, message=f"User not authorized for extension '{ext_id}'."
|
success=False, message=f"User not authorized for extension '{ext_id}'."
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-4
@@ -324,7 +324,8 @@ class AssetSettings(LNbitsSettings):
|
|||||||
"heif",
|
"heif",
|
||||||
"heics",
|
"heics",
|
||||||
"text/plain",
|
"text/plain",
|
||||||
"text/json" "text/xml",
|
"text/json",
|
||||||
|
"text/xml",
|
||||||
"application/json",
|
"application/json",
|
||||||
"application/pdf",
|
"application/pdf",
|
||||||
]
|
]
|
||||||
@@ -588,6 +589,8 @@ class ZBDFundingSource(LNbitsSettings):
|
|||||||
class PhoenixdFundingSource(LNbitsSettings):
|
class PhoenixdFundingSource(LNbitsSettings):
|
||||||
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
|
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
|
||||||
phoenixd_api_password: str | None = Field(default=None)
|
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):
|
class AlbyFundingSource(LNbitsSettings):
|
||||||
@@ -1007,9 +1010,7 @@ class EnvSettings(LNbitsSettings):
|
|||||||
debug_database: bool = Field(default=False)
|
debug_database: bool = Field(default=False)
|
||||||
bundle_assets: bool = Field(default=True)
|
bundle_assets: bool = Field(default=True)
|
||||||
# When enabled, auth cookies require HTTPS and SSO will reject insecure HTTP.
|
# When enabled, auth cookies require HTTPS and SSO will reject insecure HTTP.
|
||||||
# Keep disabled by default so local HTTP installs continue to work unless
|
auth_https_only: bool = Field(default=True)
|
||||||
# operators explicitly opt into HTTPS-only auth cookies.
|
|
||||||
auth_https_only: 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, gt=0)
|
port: int = Field(default=5000, gt=0)
|
||||||
forwarded_allow_ips: str = Field(default="*")
|
forwarded_allow_ips: str = Field(default="*")
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+11
-11
File diff suppressed because one or more lines are too long
@@ -191,6 +191,7 @@ window.localisation.br = {
|
|||||||
only_admins_can_create_extensions:
|
only_admins_can_create_extensions:
|
||||||
'Apenas contas de administrador podem criar extensões',
|
'Apenas contas de administrador podem criar extensões',
|
||||||
admin_only: 'Apenas para Administração',
|
admin_only: 'Apenas para Administração',
|
||||||
|
super_user_only: 'Apenas para superusuário',
|
||||||
make_user_admin: 'Tornar usuário administrador',
|
make_user_admin: 'Tornar usuário administrador',
|
||||||
revoke_admin: 'Revogar Admin',
|
revoke_admin: 'Revogar Admin',
|
||||||
new_version: 'Nova Versão',
|
new_version: 'Nova Versão',
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ window.localisation.cn = {
|
|||||||
all: '全部',
|
all: '全部',
|
||||||
only_admins_can_install: '(只有管理员账户可以安装扩展)',
|
only_admins_can_install: '(只有管理员账户可以安装扩展)',
|
||||||
admin_only: '仅限管理员',
|
admin_only: '仅限管理员',
|
||||||
|
super_user_only: '仅限超级用户',
|
||||||
new_version: '新版本',
|
new_version: '新版本',
|
||||||
extension_depends_on: '依赖于:',
|
extension_depends_on: '依赖于:',
|
||||||
extension_rating_soon: '即将推出评分',
|
extension_rating_soon: '即将推出评分',
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ window.localisation.cs = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'(Pouze administrátorské účty mohou instalovat rozšíření)',
|
'(Pouze administrátorské účty mohou instalovat rozšíření)',
|
||||||
admin_only: 'Pouze pro adminy',
|
admin_only: 'Pouze pro adminy',
|
||||||
|
super_user_only: 'Pouze pro superuživatele',
|
||||||
new_version: 'Nová verze',
|
new_version: 'Nová verze',
|
||||||
extension_depends_on: 'Závisí na:',
|
extension_depends_on: 'Závisí na:',
|
||||||
extension_rating_soon: 'Hodnocení brzy dostupné',
|
extension_rating_soon: 'Hodnocení brzy dostupné',
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ window.localisation.de = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'(Nur Administratorkonten können Erweiterungen installieren)',
|
'(Nur Administratorkonten können Erweiterungen installieren)',
|
||||||
admin_only: 'Nur für Admins',
|
admin_only: 'Nur für Admins',
|
||||||
|
super_user_only: 'Nur für Superuser',
|
||||||
new_version: 'Neue Version',
|
new_version: 'Neue Version',
|
||||||
extension_depends_on: 'Hängt ab von:',
|
extension_depends_on: 'Hängt ab von:',
|
||||||
extension_rating_soon: 'Bewertungen sind bald verfügbar',
|
extension_rating_soon: 'Bewertungen sind bald verfügbar',
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ window.localisation.en = {
|
|||||||
only_admins_can_create_extensions:
|
only_admins_can_create_extensions:
|
||||||
'Only admin accounts can create extensions',
|
'Only admin accounts can create extensions',
|
||||||
admin_only: 'Admin Only',
|
admin_only: 'Admin Only',
|
||||||
|
super_user_only: 'Super User Only',
|
||||||
make_user_admin: 'Make User Admin',
|
make_user_admin: 'Make User Admin',
|
||||||
revoke_admin: 'Revoke Admin',
|
revoke_admin: 'Revoke Admin',
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
@@ -275,6 +276,8 @@ window.localisation.en = {
|
|||||||
requires_server_restart:
|
requires_server_restart:
|
||||||
'Changing these settings requires a server restart to take effect.',
|
'Changing these settings requires a server restart to take effect.',
|
||||||
funding_source_info: 'Select the active funding wallet',
|
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}.',
|
latest_update: 'You are on the latest version {version}.',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
notifications_configure: 'Configure Notifications',
|
notifications_configure: 'Configure Notifications',
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ window.localisation.es = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'(Solo las cuentas de administrador pueden instalar extensiones)',
|
'(Solo las cuentas de administrador pueden instalar extensiones)',
|
||||||
admin_only: 'Solo administradores',
|
admin_only: 'Solo administradores',
|
||||||
|
super_user_only: 'Solo superusuario',
|
||||||
new_version: 'Nueva Versión',
|
new_version: 'Nueva Versión',
|
||||||
extension_depends_on: 'Depende de:',
|
extension_depends_on: 'Depende de:',
|
||||||
extension_rating_soon: 'Calificaciones próximamente',
|
extension_rating_soon: 'Calificaciones próximamente',
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ window.localisation.fi = {
|
|||||||
all: 'Kaikki',
|
all: 'Kaikki',
|
||||||
only_admins_can_install: '(Vain pääkäyttäjät voivat asentaa laajennuksia)',
|
only_admins_can_install: '(Vain pääkäyttäjät voivat asentaa laajennuksia)',
|
||||||
admin_only: 'Pääkäyttäjille',
|
admin_only: 'Pääkäyttäjille',
|
||||||
|
super_user_only: 'Vain superkäyttäjälle',
|
||||||
new_version: 'Uusi versio',
|
new_version: 'Uusi versio',
|
||||||
extension_depends_on: 'Edellyttää:',
|
extension_depends_on: 'Edellyttää:',
|
||||||
extension_rating_soon: 'Arvostelut on tulossa pian',
|
extension_rating_soon: 'Arvostelut on tulossa pian',
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ window.localisation.fr = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Seuls les comptes administrateurs peuvent installer des extensions',
|
'Seuls les comptes administrateurs peuvent installer des extensions',
|
||||||
admin_only: 'Réservé aux administrateurs',
|
admin_only: 'Réservé aux administrateurs',
|
||||||
|
super_user_only: 'Réservé au super utilisateur',
|
||||||
new_version: 'Nouvelle version',
|
new_version: 'Nouvelle version',
|
||||||
extension_depends_on: 'Dépend de :',
|
extension_depends_on: 'Dépend de :',
|
||||||
extension_rating_soon: 'Notes des utilisateurs à venir bientôt',
|
extension_rating_soon: 'Notes des utilisateurs à venir bientôt',
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ window.localisation.it = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Solo gli account amministratore possono installare estensioni.',
|
'Solo gli account amministratore possono installare estensioni.',
|
||||||
admin_only: 'Solo amministratore',
|
admin_only: 'Solo amministratore',
|
||||||
|
super_user_only: 'Solo superutente',
|
||||||
new_version: 'Nuova Versione',
|
new_version: 'Nuova Versione',
|
||||||
extension_depends_on: 'Dipende da:',
|
extension_depends_on: 'Dipende da:',
|
||||||
extension_rating_soon: 'Valutazioni in arrivo',
|
extension_rating_soon: 'Valutazioni in arrivo',
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ window.localisation.jp = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'(管理者アカウントのみが拡張機能をインストールできます)',
|
'(管理者アカウントのみが拡張機能をインストールできます)',
|
||||||
admin_only: '管理者のみ',
|
admin_only: '管理者のみ',
|
||||||
|
super_user_only: 'スーパー ユーザーのみ',
|
||||||
new_version: '新しいバージョン',
|
new_version: '新しいバージョン',
|
||||||
extension_depends_on: '依存先:',
|
extension_depends_on: '依存先:',
|
||||||
extension_rating_soon: '評価は近日公開',
|
extension_rating_soon: '評価は近日公開',
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ window.localisation.kr = {
|
|||||||
all: '전체',
|
all: '전체',
|
||||||
only_admins_can_install: '(관리자 계정만이 확장 기능을 설치할 수 있습니다)',
|
only_admins_can_install: '(관리자 계정만이 확장 기능을 설치할 수 있습니다)',
|
||||||
admin_only: '관리자 전용',
|
admin_only: '관리자 전용',
|
||||||
|
super_user_only: '슈퍼유저 전용',
|
||||||
new_version: '새로운 버전',
|
new_version: '새로운 버전',
|
||||||
extension_depends_on: '의존성 존재:',
|
extension_depends_on: '의존성 존재:',
|
||||||
extension_rating_soon: '평점 기능도 곧 구현됩니다',
|
extension_rating_soon: '평점 기능도 곧 구현됩니다',
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ window.localisation.nl = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Alleen beheerdersaccounts kunnen extensies installeren',
|
'Alleen beheerdersaccounts kunnen extensies installeren',
|
||||||
admin_only: 'Alleen beheerder',
|
admin_only: 'Alleen beheerder',
|
||||||
|
super_user_only: 'Alleen supergebruiker',
|
||||||
new_version: 'Nieuwe Versie',
|
new_version: 'Nieuwe Versie',
|
||||||
extension_depends_on: 'Afhankelijk van:',
|
extension_depends_on: 'Afhankelijk van:',
|
||||||
extension_rating_soon: 'Beoordelingen binnenkort beschikbaar',
|
extension_rating_soon: 'Beoordelingen binnenkort beschikbaar',
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ window.localisation.pi = {
|
|||||||
all: 'Arr',
|
all: 'Arr',
|
||||||
only_admins_can_install: '(Only admin accounts can install extensions)',
|
only_admins_can_install: '(Only admin accounts can install extensions)',
|
||||||
admin_only: "Cap'n Only",
|
admin_only: "Cap'n Only",
|
||||||
|
super_user_only: "Big Cap'n Only",
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
extension_depends_on: 'Depends on:',
|
extension_depends_on: 'Depends on:',
|
||||||
extension_rating_soon: "Ratings a'comin' soon",
|
extension_rating_soon: "Ratings a'comin' soon",
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ window.localisation.pl = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Tylko konta administratorów mogą instalować rozszerzenia',
|
'Tylko konta administratorów mogą instalować rozszerzenia',
|
||||||
admin_only: 'Tylko dla administratora',
|
admin_only: 'Tylko dla administratora',
|
||||||
|
super_user_only: 'Tylko dla superużytkownika',
|
||||||
new_version: 'Nowa wersja',
|
new_version: 'Nowa wersja',
|
||||||
extension_depends_on: 'Zależy od:',
|
extension_depends_on: 'Zależy od:',
|
||||||
extension_rating_soon: 'Oceny będą dostępne wkrótce',
|
extension_rating_soon: 'Oceny będą dostępne wkrótce',
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ window.localisation.pt = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Apenas contas de administrador podem instalar extensões.',
|
'Apenas contas de administrador podem instalar extensões.',
|
||||||
admin_only: 'Apenas para administradores',
|
admin_only: 'Apenas para administradores',
|
||||||
|
super_user_only: 'Apenas para superusuário',
|
||||||
new_version: 'Nova Versão',
|
new_version: 'Nova Versão',
|
||||||
extension_depends_on: 'Depende de:',
|
extension_depends_on: 'Depende de:',
|
||||||
extension_rating_soon: 'Avaliações em breve',
|
extension_rating_soon: 'Avaliações em breve',
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ window.localisation.sk = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'(Iba administrátorské účty môžu inštalovať rozšírenia)',
|
'(Iba administrátorské účty môžu inštalovať rozšírenia)',
|
||||||
admin_only: 'Iba pre administrátorov',
|
admin_only: 'Iba pre administrátorov',
|
||||||
|
super_user_only: 'Iba pre superužívateľa',
|
||||||
new_version: 'Nová verzia',
|
new_version: 'Nová verzia',
|
||||||
extension_depends_on: 'Závisí na:',
|
extension_depends_on: 'Závisí na:',
|
||||||
extension_rating_soon: 'Hodnotenia budú čoskoro dostupné',
|
extension_rating_soon: 'Hodnotenia budú čoskoro dostupné',
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ window.localisation.we = {
|
|||||||
all: 'Pob',
|
all: 'Pob',
|
||||||
only_admins_can_install: 'Dim ond cyfrifon gweinyddwr all osod estyniadau',
|
only_admins_can_install: 'Dim ond cyfrifon gweinyddwr all osod estyniadau',
|
||||||
admin_only: 'Dim ond Gweinyddwr',
|
admin_only: 'Dim ond Gweinyddwr',
|
||||||
|
super_user_only: 'Dim ond Uwchddefnyddiwr',
|
||||||
new_version: 'Fersiwn Newydd',
|
new_version: 'Fersiwn Newydd',
|
||||||
extension_depends_on: 'Dibynnu ar:',
|
extension_depends_on: 'Dibynnu ar:',
|
||||||
extension_rating_soon: 'Sgôr yn dod yn fuan',
|
extension_rating_soon: 'Sgôr yn dod yn fuan',
|
||||||
|
|||||||
@@ -202,7 +202,18 @@ window.app.component('lnbits-admin-funding-sources', {
|
|||||||
'Phoenixd',
|
'Phoenixd',
|
||||||
{
|
{
|
||||||
phoenixd_api_endpoint: 'Endpoint',
|
phoenixd_api_endpoint: 'Endpoint',
|
||||||
phoenixd_api_password: 'Key'
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -41,5 +41,37 @@ window.app.component('lnbits-admin-site-customisation', {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {}
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -545,31 +545,56 @@ window.PageAccount = {
|
|||||||
if (file) {
|
if (file) {
|
||||||
this.uploadAsset(file)
|
this.uploadAsset(file)
|
||||||
}
|
}
|
||||||
|
e.target.value = null
|
||||||
},
|
},
|
||||||
async uploadAsset(file) {
|
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} = {}
|
||||||
|
) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
try {
|
try {
|
||||||
await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'POST',
|
'POST',
|
||||||
`/api/v1/assets?public_asset=${this.assetsUploadToPublic}`,
|
`/api/v1/assets?public_asset=${isPublic}`,
|
||||||
null,
|
null,
|
||||||
formData,
|
formData,
|
||||||
{
|
{
|
||||||
headers: {'Content-Type': 'multipart/form-data'}
|
headers: {'Content-Type': 'multipart/form-data'}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
this.$q.notify({
|
if (notifySuccess) {
|
||||||
type: 'positive',
|
this.$q.notify({
|
||||||
message: 'Upload successful!',
|
type: 'positive',
|
||||||
icon: null
|
message: 'Upload successful!',
|
||||||
})
|
icon: null
|
||||||
|
})
|
||||||
|
}
|
||||||
await this.getUserAssets()
|
await this.getUserAssets()
|
||||||
|
return data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
LNbits.utils.notifyApiError(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) {
|
async deleteAsset(asset) {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this asset?')
|
.confirmDialog('Are you sure you want to delete this asset?')
|
||||||
|
|||||||
Vendored
+290
-149
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,7 @@
|
|||||||
:label="prop.label"
|
:label="prop.label"
|
||||||
:hint="prop.hint"
|
:hint="prop.hint"
|
||||||
:value="prop.value"
|
:value="prop.value"
|
||||||
|
:readonly="prop.readonly || false"
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="prop.copy"
|
v-if="prop.copy"
|
||||||
@@ -73,6 +74,15 @@
|
|||||||
></q-btn>
|
></q-btn>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="
|
v-if="
|
||||||
|
|||||||
@@ -251,10 +251,27 @@
|
|||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_default_bgimage"
|
v-model="formData.lnbits_default_bgimage"
|
||||||
label="Background Image"
|
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."
|
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>
|
</q-input>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="adminBackgroundImageInput"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
@change="onBackgroundImageInput"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
<div class="row q-col-gutter-md q-mb-md">
|
||||||
|
|||||||
@@ -492,10 +492,28 @@
|
|||||||
siteCustomisationChanged({bgimageChoice: $event})
|
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
|
<q-tooltip
|
||||||
><span v-text="$t('background_image')"></span
|
><span v-text="$t('background_image')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="backgroundImageInput"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
@change="onBackgroundImageInput"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
@@ -1108,82 +1126,130 @@
|
|||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-btn-dropdown
|
<div
|
||||||
color="grey"
|
class="row items-center no-wrap q-col-gutter-sm"
|
||||||
dense
|
|
||||||
outline
|
|
||||||
no-caps
|
|
||||||
:label="props.row.name"
|
|
||||||
:icon="props.row.is_public ? 'public' : ''"
|
|
||||||
>
|
>
|
||||||
<q-list>
|
<div class="col">
|
||||||
<q-item
|
<q-btn-dropdown
|
||||||
clickable
|
color="grey"
|
||||||
v-close-popup
|
dense
|
||||||
@click="copyAssetLinkToClipboard(props.row)"
|
outline
|
||||||
|
no-caps
|
||||||
|
class="full-width"
|
||||||
|
:label="props.row.name"
|
||||||
|
:icon="props.row.is_public ? 'public' : ''"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-list>
|
||||||
<q-avatar icon="content_copy" />
|
<q-item
|
||||||
</q-item-section>
|
clickable
|
||||||
<q-item-section>
|
v-close-popup
|
||||||
<q-item-label>Copy Link</q-item-label>
|
@click="
|
||||||
<q-item-label caption
|
copyAssetLinkToClipboard(props.row)
|
||||||
>Copy asset link to
|
|
||||||
clipboard</q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<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>
|
<q-item-section avatar>
|
||||||
<q-item-section v-else>
|
<q-avatar icon="content_copy" />
|
||||||
<q-item-label>Publish</q-item-label>
|
</q-item-section>
|
||||||
<q-item-label caption
|
<q-item-section>
|
||||||
>Make this asset public</q-item-label
|
<q-item-label>Copy Link</q-item-label>
|
||||||
>
|
<q-item-label caption
|
||||||
</q-item-section>
|
>Copy asset link to
|
||||||
</q-item>
|
clipboard</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
<q-item
|
<q-item
|
||||||
clickable
|
clickable
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="deleteAsset(props.row)"
|
@click="
|
||||||
>
|
toggleAssetPublicAccess(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>
|
<q-item-section avatar>
|
||||||
</q-item>
|
<q-avatar
|
||||||
</q-list>
|
:icon="
|
||||||
</q-btn-dropdown>
|
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
|
||||||
|
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>
|
||||||
|
</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-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -307,7 +307,13 @@
|
|||||||
:label="$t('disable')"
|
:label="$t('disable')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="extension.isAdminOnly && !g.user.admin"
|
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-text="$t('admin_only')"
|
v-text="$t('admin_only')"
|
||||||
>
|
>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
@@ -316,7 +322,9 @@
|
|||||||
v-else-if="
|
v-else-if="
|
||||||
extension.isInstalled &&
|
extension.isInstalled &&
|
||||||
extension.isActive &&
|
extension.isActive &&
|
||||||
!g.user.extensions.includes(extension.id)
|
!g.user.extensions.includes(extension.id) &&
|
||||||
|
(!extension.isAdminOnly || g.user.admin) &&
|
||||||
|
(!extension.isSuperUserOnly || g.user.super_user)
|
||||||
"
|
"
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from embit.bip39 import mnemonic_is_valid
|
||||||
from httpx import RequestError, TimeoutException
|
from httpx import RequestError, TimeoutException
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets import connect
|
from websockets import connect
|
||||||
@@ -61,6 +63,8 @@ class PhoenixdWallet(Wallet):
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
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):
|
async def cleanup(self):
|
||||||
try:
|
try:
|
||||||
@@ -69,6 +73,7 @@ class PhoenixdWallet(Wallet):
|
|||||||
logger.warning(f"Error closing wallet connection: {e}")
|
logger.warning(f"Error closing wallet connection: {e}")
|
||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
|
await self._persist_loaded_mnemonic()
|
||||||
try:
|
try:
|
||||||
r = await self.client.get("/getinfo", timeout=10)
|
r = await self.client.get("/getinfo", timeout=10)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -101,7 +106,6 @@ class PhoenixdWallet(Wallet):
|
|||||||
unhashed_description: bytes | None = None,
|
unhashed_description: bytes | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msats_amount = amount
|
msats_amount = amount
|
||||||
data: dict[str, Any] = {
|
data: dict[str, Any] = {
|
||||||
@@ -309,7 +313,7 @@ class PhoenixdWallet(Wallet):
|
|||||||
and message_json.get("type") == "payment_received"
|
and message_json.get("type") == "payment_received"
|
||||||
):
|
):
|
||||||
logger.info(
|
logger.info(
|
||||||
f'payment-received: {message_json["paymentHash"]}'
|
f"payment-received: {message_json['paymentHash']}"
|
||||||
)
|
)
|
||||||
yield message_json["paymentHash"]
|
yield message_json["paymentHash"]
|
||||||
|
|
||||||
@@ -319,3 +323,49 @@ class PhoenixdWallet(Wallet):
|
|||||||
"retrying in 5 seconds"
|
"retrying in 5 seconds"
|
||||||
)
|
)
|
||||||
await asyncio.sleep(5)
|
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}")
|
||||||
|
|||||||
Generated
+12
-8
@@ -6,7 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "lnbits",
|
"name": "lnbits",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.15.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nostr-tools": "^2.18.2",
|
"nostr-tools": "^2.18.2",
|
||||||
@@ -734,14 +734,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.5",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.15.11",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
@@ -1555,9 +1555,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pyright": {
|
"node_modules/pyright": {
|
||||||
"version": "1.1.289",
|
"version": "1.1.289",
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
"terser": "^5.44.1"
|
"terser": "^5.44.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.15.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nostr-tools": "^2.18.2",
|
"nostr-tools": "^2.18.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.5.3"
|
version = "1.5.4"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ from lnbits.core.models.extensions import (
|
|||||||
Extension,
|
Extension,
|
||||||
ExtensionConfig,
|
ExtensionConfig,
|
||||||
ExtensionRelease,
|
ExtensionRelease,
|
||||||
|
GitHubRelease,
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
|
Manifest,
|
||||||
PayToEnableInfo,
|
PayToEnableInfo,
|
||||||
ReleasePaymentInfo,
|
ReleasePaymentInfo,
|
||||||
UserExtensionInfo,
|
UserExtensionInfo,
|
||||||
@@ -152,6 +154,8 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
|
|||||||
short_description="Config",
|
short_description="Config",
|
||||||
min_lnbits_version="0.1.0",
|
min_lnbits_version="0.1.0",
|
||||||
max_lnbits_version=None,
|
max_lnbits_version=None,
|
||||||
|
admin_only=True,
|
||||||
|
super_user_only=True,
|
||||||
)
|
)
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
ExtensionConfig,
|
ExtensionConfig,
|
||||||
@@ -160,6 +164,8 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
|
|||||||
)
|
)
|
||||||
release_info = await get_extension_release("org", ext_id, "v1.0.0")
|
release_info = await get_extension_release("org", ext_id, "v1.0.0")
|
||||||
assert release_info["is_version_compatible"] is True
|
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
|
@pytest.mark.anyio
|
||||||
@@ -258,6 +264,8 @@ async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user)
|
|||||||
catalog_entry = make_installable_extension(
|
catalog_entry = make_installable_extension(
|
||||||
ext_id,
|
ext_id,
|
||||||
pay_to_enable=PayToEnableInfo(required=True, amount=21, wallet=admin_wallet.id),
|
pay_to_enable=PayToEnableInfo(required=True, amount=21, wallet=admin_wallet.id),
|
||||||
|
admin_only=True,
|
||||||
|
super_user_only=True,
|
||||||
)
|
)
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
@@ -267,6 +275,8 @@ async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user)
|
|||||||
catalog = await extensions(AccountId(id=regular_user.id))
|
catalog = await extensions(AccountId(id=regular_user.id))
|
||||||
catalog_item = next(item for item in catalog if item["id"] == ext_id)
|
catalog_item = next(item for item in catalog if item["id"] == ext_id)
|
||||||
assert catalog_item["payToEnable"]["wallet"] is None
|
assert catalog_item["payToEnable"]["wallet"] is None
|
||||||
|
assert catalog_item["isAdminOnly"] is True
|
||||||
|
assert catalog_item["isSuperUserOnly"] is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@@ -428,3 +438,64 @@ async def test_extension_api_review_endpoints(mocker):
|
|||||||
CreateExtensionReview(tag=ext_id, name="Alice", rating=900, comment="Great")
|
CreateExtensionReview(tag=ext_id, name="Alice", rating=900, comment="Great")
|
||||||
)
|
)
|
||||||
assert payment_request.payment_hash.startswith("hash_")
|
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
|
||||||
|
|||||||
@@ -141,9 +141,13 @@ def make_installable_extension(
|
|||||||
pay_to_enable: PayToEnableInfo | None = None,
|
pay_to_enable: PayToEnableInfo | None = None,
|
||||||
dependencies: list[str] | None = None,
|
dependencies: list[str] | None = None,
|
||||||
payments: list[ReleasePaymentInfo] | None = None,
|
payments: list[ReleasePaymentInfo] | None = None,
|
||||||
|
admin_only: bool = False,
|
||||||
|
super_user_only: bool = False,
|
||||||
) -> InstallableExtension:
|
) -> InstallableExtension:
|
||||||
release = make_extension_release(ext_id, version)
|
release = make_extension_release(ext_id, version)
|
||||||
release.is_version_compatible = compatible
|
release.is_version_compatible = compatible
|
||||||
|
release.admin_only = admin_only
|
||||||
|
release.super_user_only = super_user_only
|
||||||
return InstallableExtension(
|
return InstallableExtension(
|
||||||
id=ext_id,
|
id=ext_id,
|
||||||
name=f"Extension {ext_id}",
|
name=f"Extension {ext_id}",
|
||||||
@@ -156,6 +160,8 @@ def make_installable_extension(
|
|||||||
pay_to_enable=pay_to_enable,
|
pay_to_enable=pay_to_enable,
|
||||||
dependencies=dependencies or [],
|
dependencies=dependencies or [],
|
||||||
payments=payments or [],
|
payments=payments or [],
|
||||||
|
admin_only=admin_only,
|
||||||
|
super_user_only=super_user_only,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from fastapi.exceptions import HTTPException
|
|||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
from pydantic.types import UUID4
|
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.crud.users import delete_account
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.core.models.users import AccessTokenPayload
|
from lnbits.core.models.users import AccessTokenPayload
|
||||||
@@ -18,10 +19,12 @@ from lnbits.decorators import (
|
|||||||
check_extension_builder,
|
check_extension_builder,
|
||||||
check_first_install,
|
check_first_install,
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
|
check_user_extension_access,
|
||||||
optional_user_id,
|
optional_user_id,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import create_access_token
|
from lnbits.helpers import create_access_token
|
||||||
from lnbits.settings import AuthMethods, Settings, settings
|
from lnbits.settings import AuthMethods, Settings, settings
|
||||||
|
from tests.helpers import make_installable_extension
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@@ -225,3 +228,37 @@ async def test_check_extension_builder_requires_admin_when_disabled_for_users(
|
|||||||
admin_user = user_alan.copy(deep=True)
|
admin_user = user_alan.copy(deep=True)
|
||||||
admin_user.admin = True
|
admin_user.admin = True
|
||||||
await check_extension_builder(admin_user)
|
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
|
||||||
|
|||||||
@@ -1263,7 +1263,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.5.3"
|
version = "1.5.4"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
@@ -2114,7 +2114,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.0.2"
|
version = "9.0.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
@@ -2125,9 +2125,9 @@ dependencies = [
|
|||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
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" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2213,11 +2213,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.22"
|
version = "0.0.26"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
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" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user