Compare commits

...
Author SHA1 Message Date
Arc fa1fec87c7 fundle 2026-05-01 12:32:44 +01:00
Arc 74a1dd5ea6 trans 2026-05-01 12:31:03 +01:00
Arc ef47f1660d make 2026-05-01 12:28:43 +01:00
Arc 4341c329fd extend to repo manifests 2026-05-01 12:16:39 +01:00
Arc fdf3ab2688 init 2026-05-01 12:10:11 +01:00
dni ⚡andGitHub 99e4f33142 chore: update to version v1.5.4 (#3939) 2026-04-23 11:12:06 +02:00
ArcandGitHub f4f43ad361 fix: Switching images to local (#3938) 2026-04-21 11:41:45 +01:00
ArcandGitHub f14ea6c577 fix: add auth_https_only to env.example (#3937) 2026-04-17 14:50:26 +03:00
35 changed files with 262 additions and 24 deletions
+5 -1
View File
@@ -7,9 +7,13 @@
# 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
+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="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
<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">
</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 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" />
[![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 src="https://i.imgur.com/w8jdGpF.png" style="width:800px">
<img alt="lnbits_wallet" src="docs/assets/wallet.jpg" />
## 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 src="https://i.imgur.com/aEBpwJF.png" style="width:800px">
<img alt="lnbits_extensions" src="docs/assets/extensions.jpg" />
## LNbits API
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 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
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

+66 -5
View File
@@ -43,6 +43,8 @@ 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)
@@ -52,6 +54,8 @@ class GitHubRelease(BaseModel):
id: str
organisation: str
repository: str
admin_only: bool = False
super_user_only: bool = False
class Manifest(BaseModel):
@@ -83,6 +87,8 @@ 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)
@@ -195,6 +201,8 @@ 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:
@@ -263,6 +271,8 @@ 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
@@ -289,6 +299,8 @@ 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)
@@ -336,6 +348,8 @@ 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):
@@ -399,6 +413,16 @@ 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
@@ -454,6 +478,16 @@ 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
@@ -496,6 +530,10 @@ 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
@@ -558,6 +596,13 @@ 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,
@@ -569,9 +614,9 @@ class InstallableExtension(BaseModel):
config.tile,
),
meta=ExtensionMeta(
latest_release=ExtensionRelease.from_github_release(
source_repo, latest_release
),
admin_only=admin_only,
super_user_only=super_user_only,
latest_release=latest_extension_release,
),
)
except Exception as e:
@@ -580,7 +625,12 @@ class InstallableExtension(BaseModel):
@classmethod
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(
id=e.id,
name=e.name,
@@ -610,6 +660,8 @@ 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,
@@ -617,7 +669,9 @@ 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),
),
),
)
@@ -719,6 +773,13 @@ 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:
+23 -2
View File
@@ -79,7 +79,11 @@ 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)
ext_meta = ExtensionMeta(
installed_release=release,
admin_only=release.admin_only,
super_user_only=release.super_user_only,
)
ext_info = InstallableExtension(
id=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")
async def api_enable_extension(
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.")
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:
@@ -464,6 +482,8 @@ 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(
@@ -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
),
"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,
"latestRelease": (
dict(ext.meta.latest_release)
+14 -1
View File
@@ -14,6 +14,7 @@ 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,
@@ -424,7 +425,19 @@ 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.
"""
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(
success=False, message=f"User not authorized for extension '{ext_id}'."
)
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -191,6 +191,7 @@ 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,6 +135,7 @@ 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,6 +140,7 @@ 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,6 +143,7 @@ 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',
+1
View File
@@ -189,6 +189,7 @@ 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',
+1
View File
@@ -142,6 +142,7 @@ 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,6 +154,7 @@ 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,6 +145,7 @@ 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,6 +142,7 @@ 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,6 +137,7 @@ window.localisation.jp = {
only_admins_can_install:
'(管理者アカウントのみが拡張機能をインストールできます)',
admin_only: '管理者のみ',
super_user_only: 'スーパー ユーザーのみ',
new_version: '新しいバージョン',
extension_depends_on: '依存先:',
extension_rating_soon: '評価は近日公開',
+1
View File
@@ -139,6 +139,7 @@ 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,6 +143,7 @@ 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,6 +141,7 @@ 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,6 +140,7 @@ 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,6 +141,7 @@ 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,6 +138,7 @@ 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,6 +139,7 @@ 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',
+10 -2
View File
@@ -307,7 +307,13 @@
:label="$t('disable')"
></q-btn>
<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')"
>
</q-badge>
@@ -316,7 +322,9 @@
v-else-if="
extension.isInstalled &&
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
color="primary"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.5.4-rc1"
version = "1.5.4"
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" }]
+71
View File
@@ -20,7 +20,9 @@ from lnbits.core.models.extensions import (
Extension,
ExtensionConfig,
ExtensionRelease,
GitHubRelease,
InstallableExtension,
Manifest,
PayToEnableInfo,
ReleasePaymentInfo,
UserExtensionInfo,
@@ -152,6 +154,8 @@ 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,
@@ -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")
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
@@ -258,6 +264,8 @@ 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,
@@ -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_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
@@ -428,3 +438,64 @@ 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
+6
View File
@@ -141,9 +141,13 @@ 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}",
@@ -156,6 +160,8 @@ 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,
),
)
+37
View File
@@ -8,6 +8,7 @@ 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
@@ -18,10 +19,12 @@ 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
@@ -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.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
Generated
+1 -1
View File
@@ -1263,7 +1263,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.5.4rc1"
version = "1.5.4"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },