feat: permissions
This commit is contained in:
+3
-3
@@ -112,7 +112,7 @@ async def startup(app: FastAPI):
|
|||||||
create_llms_txt_route(app)
|
create_llms_txt_route(app)
|
||||||
|
|
||||||
# initialize tasks
|
# initialize tasks
|
||||||
register_async_tasks()
|
register_async_tasks(app)
|
||||||
|
|
||||||
enqueue_admin_notification(
|
enqueue_admin_notification(
|
||||||
NotificationType.server_start_stop,
|
NotificationType.server_start_stop,
|
||||||
@@ -481,7 +481,7 @@ async def check_and_register_extensions(app: FastAPI) -> None:
|
|||||||
await update_installed_extension_state(ext_id=ext.code, active=False)
|
await update_installed_extension_state(ext_id=ext.code, active=False)
|
||||||
|
|
||||||
|
|
||||||
def register_async_tasks() -> None:
|
def register_async_tasks(app: FastAPI) -> None:
|
||||||
|
|
||||||
create_permanent_task(wait_for_audit_data)
|
create_permanent_task(wait_for_audit_data)
|
||||||
create_permanent_task(wait_notification_messages)
|
create_permanent_task(wait_notification_messages)
|
||||||
@@ -499,7 +499,7 @@ def register_async_tasks() -> None:
|
|||||||
# core invoice listener
|
# core invoice listener
|
||||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||||
register_invoice_listener(invoice_queue, "core")
|
register_invoice_listener(invoice_queue, "core")
|
||||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue, app))
|
||||||
|
|
||||||
create_permanent_task(run_by_the_minute_tasks)
|
create_permanent_task(run_by_the_minute_tasks)
|
||||||
create_permanent_task(purge_audit_data)
|
create_permanent_task(purge_audit_data)
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ class InMemoryExtensionAPI(ExtensionAPI):
|
|||||||
unit=request.currency or "sat",
|
unit=request.currency or "sat",
|
||||||
memo=request.memo,
|
memo=request.memo,
|
||||||
extra=request.extra,
|
extra=request.extra,
|
||||||
|
extension=self.extension_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return CreateInvoiceResponse(
|
return CreateInvoiceResponse(
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from typing import Any
|
|||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from lnbits.core.crud.extensions import get_installed_extension
|
||||||
|
|
||||||
from .api import list_extension_api_methods
|
from .api import list_extension_api_methods
|
||||||
from .loader import WasmExtension, register_wasm_extension
|
from .loader import WasmExtension, register_wasm_extension
|
||||||
from .models import UserWalletSummary
|
from .models import UserWalletSummary
|
||||||
@@ -27,7 +29,7 @@ async def invoke_wasm_extension_export(
|
|||||||
extension = _get_registered_extension(app, ext_id)
|
extension = _get_registered_extension(app, ext_id)
|
||||||
state = _get_extension_state(app)
|
state = _get_extension_state(app)
|
||||||
state.user_wallets[extension.id] = _user_wallet_summaries(user)
|
state.user_wallets[extension.id] = _user_wallet_summaries(user)
|
||||||
permissions = _extension_permissions(extension)
|
permissions = await _extension_permissions(extension)
|
||||||
api = InMemoryExtensionAPI(
|
api = InMemoryExtensionAPI(
|
||||||
extension.id,
|
extension.id,
|
||||||
permissions,
|
permissions,
|
||||||
@@ -204,12 +206,11 @@ def _get_extension_state(app: FastAPI) -> InMemoryExtensionState:
|
|||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
def _extension_permissions(extension: WasmExtension) -> set[str]:
|
async def _extension_permissions(extension: WasmExtension) -> set[str]:
|
||||||
permissions = set()
|
installed_extension = await get_installed_extension(extension.id)
|
||||||
for permission in extension.config.get("permissions") or []:
|
if not installed_extension:
|
||||||
if isinstance(permission, Mapping) and isinstance(permission.get("id"), str):
|
return set()
|
||||||
permissions.add(permission["id"])
|
return {permission.id for permission in installed_extension.permissions}
|
||||||
return permissions
|
|
||||||
|
|
||||||
|
|
||||||
def _user_id(user: Any | None) -> str | None:
|
def _user_id(user: Any | None) -> str | None:
|
||||||
|
|||||||
@@ -815,3 +815,12 @@ async def m045_add_external_id_to_payments(db: Connection):
|
|||||||
CREATE INDEX IF NOT EXISTS idx_payments_external_id
|
CREATE INDEX IF NOT EXISTS idx_payments_external_id
|
||||||
ON apipayments (external_id);
|
ON apipayments (external_id);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
async def m046_add_permissions_to_installed_extensions(db: Connection):
|
||||||
|
"""
|
||||||
|
Adds granted permissions to installed extensions.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE installed_extensions ADD COLUMN permissions TEXT DEFAULT '[]'"
|
||||||
|
)
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ class GitHubRepo(BaseModel):
|
|||||||
default_branch: str
|
default_branch: str
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionPermission(BaseModel):
|
||||||
|
id: str
|
||||||
|
label: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
policy: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
class ExtensionConfig(BaseModel):
|
class ExtensionConfig(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
short_description: str
|
short_description: str
|
||||||
@@ -84,6 +91,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
|
||||||
|
extension_type: str | None = None
|
||||||
|
permissions: list[ExtensionPermission] = []
|
||||||
|
|
||||||
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)
|
||||||
@@ -350,6 +359,7 @@ class InstallableExtension(BaseModel):
|
|||||||
icon: str | None = None
|
icon: str | None = None
|
||||||
stars: int = 0
|
stars: int = 0
|
||||||
meta: ExtensionMeta | None = None
|
meta: ExtensionMeta | None = None
|
||||||
|
permissions: list[ExtensionPermission] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hash(self) -> str:
|
def hash(self) -> str:
|
||||||
@@ -624,6 +634,11 @@ class InstallableExtension(BaseModel):
|
|||||||
version=version,
|
version=version,
|
||||||
short_description=config_json.get("short_description"),
|
short_description=config_json.get("short_description"),
|
||||||
icon=config_json.get("tile"),
|
icon=config_json.get("tile"),
|
||||||
|
permissions=[
|
||||||
|
ExtensionPermission.parse_obj(permission)
|
||||||
|
for permission in config_json.get("permissions") or []
|
||||||
|
if isinstance(permission, dict) and permission.get("id")
|
||||||
|
],
|
||||||
meta=ExtensionMeta(
|
meta=ExtensionMeta(
|
||||||
installed_release=ExtensionRelease(
|
installed_release=ExtensionRelease(
|
||||||
name=ext_id,
|
name=ext_id,
|
||||||
@@ -814,6 +829,7 @@ class CreateExtension(BaseModel):
|
|||||||
version: str
|
version: str
|
||||||
cost_sats: int | None = 0
|
cost_sats: int | None = 0
|
||||||
payment_hash: str | None = None
|
payment_hash: str | None = None
|
||||||
|
permissions: list[ExtensionPermission] = []
|
||||||
|
|
||||||
|
|
||||||
class ExtensionDetailsRequest(BaseModel):
|
class ExtensionDetailsRequest(BaseModel):
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import importlib
|
import importlib
|
||||||
|
import json
|
||||||
|
import zipfile
|
||||||
|
from pathlib import PurePosixPath
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -20,11 +24,18 @@ from lnbits.core.helpers import migrate_extension_database
|
|||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
from ..models.extensions import (
|
||||||
|
Extension,
|
||||||
|
ExtensionMeta,
|
||||||
|
ExtensionPermission,
|
||||||
|
InstallableExtension,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def install_extension(
|
async def install_extension(
|
||||||
ext_info: InstallableExtension, skip_download: bool | None = False
|
ext_info: InstallableExtension,
|
||||||
|
skip_download: bool | None = False,
|
||||||
|
granted_permissions: list[ExtensionPermission] | None = None,
|
||||||
) -> Extension:
|
) -> Extension:
|
||||||
|
|
||||||
ext_info.meta = ext_info.meta or ExtensionMeta()
|
ext_info.meta = ext_info.meta or ExtensionMeta()
|
||||||
@@ -44,6 +55,11 @@ async def install_extension(
|
|||||||
if not skip_download:
|
if not skip_download:
|
||||||
await ext_info.download_archive()
|
await ext_info.download_archive()
|
||||||
|
|
||||||
|
extension_config = _load_extension_archive_config(ext_info)
|
||||||
|
ext_info.permissions = _validate_extension_permissions(
|
||||||
|
ext_info, granted_permissions, extension_config
|
||||||
|
)
|
||||||
|
|
||||||
ext_info.extract_archive()
|
ext_info.extract_archive()
|
||||||
|
|
||||||
db_version = await get_db_version(ext_info.id)
|
db_version = await get_db_version(ext_info.id)
|
||||||
@@ -66,6 +82,63 @@ async def install_extension(
|
|||||||
return extension
|
return extension
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_extension_permissions(
|
||||||
|
ext_info: InstallableExtension,
|
||||||
|
granted_permissions: list[ExtensionPermission] | None,
|
||||||
|
extension_config: dict[str, Any],
|
||||||
|
) -> list[ExtensionPermission]:
|
||||||
|
if extension_config.get("extension_type") != "wasm":
|
||||||
|
return []
|
||||||
|
|
||||||
|
requested_permissions = [
|
||||||
|
ExtensionPermission.parse_obj(permission)
|
||||||
|
for permission in extension_config.get("permissions") or []
|
||||||
|
if isinstance(permission, dict) and permission.get("id")
|
||||||
|
]
|
||||||
|
if not requested_permissions:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if granted_permissions is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"WASM extension '{ext_info.id}' requires permission approval."
|
||||||
|
)
|
||||||
|
|
||||||
|
requested_ids = {permission.id for permission in requested_permissions}
|
||||||
|
granted_ids = {permission.id for permission in granted_permissions}
|
||||||
|
if requested_ids != granted_ids:
|
||||||
|
raise ValueError(
|
||||||
|
f"WASM extension '{ext_info.id}' was not granted all requested "
|
||||||
|
"permissions."
|
||||||
|
)
|
||||||
|
|
||||||
|
return requested_permissions
|
||||||
|
|
||||||
|
|
||||||
|
def _load_extension_archive_config(ext_info: InstallableExtension) -> dict[str, Any]:
|
||||||
|
if not ext_info.zip_path.is_file():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(ext_info.zip_path, "r") as archive:
|
||||||
|
config_name = _archive_config_name(archive.namelist())
|
||||||
|
if not config_name:
|
||||||
|
return {}
|
||||||
|
with archive.open(config_name) as config_file:
|
||||||
|
config = json.load(config_file)
|
||||||
|
except Exception as exc:
|
||||||
|
raise ValueError(f"Cannot read extension config for '{ext_info.id}'.") from exc
|
||||||
|
|
||||||
|
return config if isinstance(config, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _archive_config_name(names: list[str]) -> str | None:
|
||||||
|
for name in names:
|
||||||
|
path = PurePosixPath(name)
|
||||||
|
if len(path.parts) == 2 and path.name == "config.json":
|
||||||
|
return name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def check_extensions_limit(installed_ext: InstallableExtension | None = None):
|
async def check_extensions_limit(installed_ext: InstallableExtension | None = None):
|
||||||
if settings.lnbits_max_extensions == 0 or installed_ext:
|
if settings.lnbits_max_extensions == 0 or installed_ext:
|
||||||
return
|
return
|
||||||
|
|||||||
+85
-1
@@ -1,5 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
from lnbits.core.crud import (
|
||||||
@@ -89,7 +92,9 @@ async def _notify_server_status() -> None:
|
|||||||
enqueue_admin_notification(NotificationType.server_status, values)
|
enqueue_admin_notification(NotificationType.server_status, values)
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue) -> None:
|
async def wait_for_paid_invoices(
|
||||||
|
invoice_paid_queue: asyncio.Queue, app: FastAPI | None = None
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
This worker dispatches events to all extensions and dispatches webhooks.
|
This worker dispatches events to all extensions and dispatches webhooks.
|
||||||
"""
|
"""
|
||||||
@@ -100,6 +105,85 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue) -> None:
|
|||||||
wallet = await get_wallet(payment.wallet_id)
|
wallet = await get_wallet(payment.wallet_id)
|
||||||
if wallet:
|
if wallet:
|
||||||
await send_payment_notification(wallet, payment)
|
await send_payment_notification(wallet, payment)
|
||||||
|
if app:
|
||||||
|
await dispatch_wasm_invoice_paid(app, payment)
|
||||||
|
|
||||||
|
|
||||||
|
async def dispatch_wasm_invoice_paid(app: FastAPI, payment: Any) -> None:
|
||||||
|
extension_id = _payment_extension_id(payment)
|
||||||
|
if not extension_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
extensions = getattr(app.state, "lnbits_wasm_extensions", {})
|
||||||
|
extension = extensions.get(extension_id)
|
||||||
|
if not extension:
|
||||||
|
return
|
||||||
|
|
||||||
|
export_name = _wasm_invoice_paid_export(extension.config)
|
||||||
|
if not export_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not _is_wasm_event_export(extension, export_name):
|
||||||
|
logger.warning(
|
||||||
|
f"WASM extension '{extension.id}' declares invalid onInvoicePaid "
|
||||||
|
f"export '{export_name}'."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from lnbits.core.extensions.wasm import invoke_wasm_extension_export
|
||||||
|
|
||||||
|
await invoke_wasm_extension_export(
|
||||||
|
app,
|
||||||
|
extension.id,
|
||||||
|
export_name,
|
||||||
|
_wasm_invoice_paid_payload(payment),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
f"WASM extension '{extension.id}' failed to handle paid invoice "
|
||||||
|
f"'{payment.payment_hash}': {exc!s}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _payment_extension_id(payment: Any) -> str | None:
|
||||||
|
if isinstance(payment.extension, str) and payment.extension:
|
||||||
|
return payment.extension
|
||||||
|
|
||||||
|
extra = payment.extra or {}
|
||||||
|
tag = extra.get("tag") or payment.tag
|
||||||
|
return tag if isinstance(tag, str) and tag else None
|
||||||
|
|
||||||
|
|
||||||
|
def _wasm_invoice_paid_export(config: dict[str, Any]) -> str | None:
|
||||||
|
events = config.get("events") or {}
|
||||||
|
export_name = events.get("onInvoicePaid")
|
||||||
|
return export_name if isinstance(export_name, str) and export_name else None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_wasm_event_export(extension: Any, export_name: str) -> bool:
|
||||||
|
for export in extension.exports:
|
||||||
|
if export.get("name") == export_name:
|
||||||
|
return export.get("visibility") == "event"
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _wasm_invoice_paid_payload(payment: Any) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"checkingId": payment.checking_id,
|
||||||
|
"paymentHash": payment.payment_hash,
|
||||||
|
"walletId": payment.wallet_id,
|
||||||
|
"amount": payment.amount,
|
||||||
|
"fee": payment.fee,
|
||||||
|
"bolt11": payment.bolt11,
|
||||||
|
"memo": payment.memo,
|
||||||
|
"pending": payment.pending,
|
||||||
|
"status": payment.status,
|
||||||
|
"tag": payment.tag,
|
||||||
|
"extension": payment.extension,
|
||||||
|
"extra": payment.extra or {},
|
||||||
|
"payment": json.loads(payment.json()),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_audit_data() -> None:
|
async def wait_for_audit_data() -> None:
|
||||||
|
|||||||
@@ -89,7 +89,9 @@ async def api_install_extension(data: CreateExtension):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
extension = await install_extension(ext_info)
|
extension = await install_extension(
|
||||||
|
ext_info, granted_permissions=data.permissions
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
@@ -463,6 +465,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,
|
||||||
|
"extension_type": config.extension_type,
|
||||||
|
"permissions": [dict(permission) for permission in config.permissions],
|
||||||
}
|
}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -535,9 +539,10 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
)
|
)
|
||||||
installable_exts_ids = [e.id for e in installable_exts]
|
installable_exts_ids = [e.id for e in installable_exts]
|
||||||
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
|
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
|
||||||
|
installed_exts_by_id = {e.id: e for e in installed_exts}
|
||||||
|
|
||||||
for e in installable_exts:
|
for e in installable_exts:
|
||||||
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
|
installed_ext = installed_exts_by_id.get(e.id)
|
||||||
if installed_ext and installed_ext.meta:
|
if installed_ext and installed_ext.meta:
|
||||||
installed_release = installed_ext.meta.installed_release
|
installed_release = installed_ext.meta.installed_release
|
||||||
if installed_ext.meta.pay_to_enable and not account_id.is_admin_id:
|
if installed_ext.meta.pay_to_enable and not account_id.is_admin_id:
|
||||||
@@ -558,47 +563,55 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
e.short_description = installed_ext.short_description
|
e.short_description = installed_ext.short_description
|
||||||
e.icon = installed_ext.icon
|
e.icon = installed_ext.icon
|
||||||
|
|
||||||
extension_data = [
|
extension_data = []
|
||||||
{
|
for ext in installable_exts:
|
||||||
"id": ext.id,
|
installed_ext = installed_exts_by_id.get(ext.id)
|
||||||
"name": ext.name,
|
extension_data.append(
|
||||||
"icon": ext.icon,
|
{
|
||||||
"shortDescription": ext.short_description,
|
"id": ext.id,
|
||||||
"stars": ext.stars,
|
"name": ext.name,
|
||||||
"isFeatured": ext.meta.featured if ext.meta else False,
|
"icon": ext.icon,
|
||||||
"categories": ext.meta.categories if ext.meta else [],
|
"shortDescription": ext.short_description,
|
||||||
"dependencies": ext.meta.dependencies if ext.meta else "",
|
"stars": ext.stars,
|
||||||
"isInstalled": ext.id in installed_exts_ids,
|
"isFeatured": ext.meta.featured if ext.meta else False,
|
||||||
"hasDatabaseTables": next(
|
"categories": ext.meta.categories if ext.meta else [],
|
||||||
(True for version in db_versions if version.db == ext.id), False
|
"dependencies": ext.meta.dependencies if ext.meta else "",
|
||||||
),
|
"isInstalled": ext.id in installed_exts_ids,
|
||||||
"isAvailable": ext.id in all_ext_ids,
|
"hasDatabaseTables": next(
|
||||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
(True for version in db_versions if version.db == ext.id), False
|
||||||
"isActive": ext.id not in inactive_extensions,
|
),
|
||||||
"latestRelease": (
|
"isAvailable": ext.id in all_ext_ids,
|
||||||
dict(ext.meta.latest_release)
|
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||||
if ext.meta and ext.meta.latest_release
|
"isActive": ext.id not in inactive_extensions,
|
||||||
else None
|
"latestRelease": (
|
||||||
),
|
dict(ext.meta.latest_release)
|
||||||
"hasPaidRelease": ext.meta.has_paid_release if ext.meta else False,
|
if ext.meta and ext.meta.latest_release
|
||||||
"hasFreeRelease": ext.meta.has_free_release if ext.meta else False,
|
else None
|
||||||
"paidFeatures": ext.meta.paid_features if ext.meta else False,
|
),
|
||||||
"installedRelease": (
|
"hasPaidRelease": ext.meta.has_paid_release if ext.meta else False,
|
||||||
dict(ext.meta.installed_release)
|
"hasFreeRelease": ext.meta.has_free_release if ext.meta else False,
|
||||||
if ext.meta and ext.meta.installed_release
|
"paidFeatures": ext.meta.paid_features if ext.meta else False,
|
||||||
else None
|
"installedRelease": (
|
||||||
),
|
dict(ext.meta.installed_release)
|
||||||
"payToEnable": (
|
if ext.meta and ext.meta.installed_release
|
||||||
dict(ext.meta.pay_to_enable)
|
else None
|
||||||
if ext.meta and ext.meta.pay_to_enable
|
),
|
||||||
else {}
|
"payToEnable": (
|
||||||
),
|
dict(ext.meta.pay_to_enable)
|
||||||
"isPaymentRequired": ext.requires_payment,
|
if ext.meta and ext.meta.pay_to_enable
|
||||||
"inProgress": False,
|
else {}
|
||||||
"selectedForUpdate": False,
|
),
|
||||||
}
|
"isPaymentRequired": ext.requires_payment,
|
||||||
for ext in installable_exts
|
"isWasm": installed_ext.is_wasm if installed_ext else ext.is_wasm,
|
||||||
]
|
"permissions": (
|
||||||
|
[dict(permission) for permission in installed_ext.permissions]
|
||||||
|
if installed_ext
|
||||||
|
else []
|
||||||
|
),
|
||||||
|
"inProgress": False,
|
||||||
|
"selectedForUpdate": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
return extension_data
|
return extension_data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,12 @@ window.PageExtensions = {
|
|||||||
// the install logic has been triggered one way or another
|
// the install logic has been triggered one way or another
|
||||||
this.unsubscribeFromPaylinkWs()
|
this.unsubscribeFromPaylinkWs()
|
||||||
|
|
||||||
|
const grantedPermissions =
|
||||||
|
await this.resolveExtensionPermissionGrant(release)
|
||||||
|
if (grantedPermissions === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.selectedExtension.inProgress = true
|
this.selectedExtension.inProgress = true
|
||||||
this.showManageExtensionDialog = false
|
this.showManageExtensionDialog = false
|
||||||
release.payment_hash =
|
release.payment_hash =
|
||||||
@@ -143,7 +149,8 @@ window.PageExtensions = {
|
|||||||
archive: release.archive,
|
archive: release.archive,
|
||||||
source_repo: release.source_repo,
|
source_repo: release.source_repo,
|
||||||
payment_hash: release.payment_hash,
|
payment_hash: release.payment_hash,
|
||||||
version: release.version
|
version: release.version,
|
||||||
|
permissions: grantedPermissions
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.selectedExtension.inProgress = false
|
this.selectedExtension.inProgress = false
|
||||||
@@ -406,6 +413,10 @@ window.PageExtensions = {
|
|||||||
},
|
},
|
||||||
async payAndInstall(release) {
|
async payAndInstall(release) {
|
||||||
try {
|
try {
|
||||||
|
if ((await this.resolveExtensionPermissionGrant(release)) === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.selectedExtension.inProgress = true
|
this.selectedExtension.inProgress = true
|
||||||
this.showManageExtensionDialog = false
|
this.showManageExtensionDialog = false
|
||||||
const paymentInfo = await this.requestPaymentForInstall(
|
const paymentInfo = await this.requestPaymentForInstall(
|
||||||
@@ -451,6 +462,10 @@ window.PageExtensions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async showInstallQRCode(release) {
|
async showInstallQRCode(release) {
|
||||||
|
if ((await this.resolveExtensionPermissionGrant(release)) === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.selectedRelease = release
|
this.selectedRelease = release
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -628,6 +643,8 @@ window.PageExtensions = {
|
|||||||
release.is_version_compatible = data.is_version_compatible
|
release.is_version_compatible = data.is_version_compatible
|
||||||
release.min_lnbits_version = data.min_lnbits_version
|
release.min_lnbits_version = data.min_lnbits_version
|
||||||
release.warning = data.warning
|
release.warning = data.warning
|
||||||
|
release.extension_type = data.extension_type
|
||||||
|
release.permissions = data.permissions || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
release.error = error
|
release.error = error
|
||||||
@@ -636,6 +653,77 @@ window.PageExtensions = {
|
|||||||
release.inProgress = false
|
release.inProgress = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async resolveExtensionPermissionGrant(release) {
|
||||||
|
const permissions = this.extensionPermissionsForRelease(release)
|
||||||
|
if (
|
||||||
|
!this.releaseRequiresPermissionGrant(release) ||
|
||||||
|
!permissions.length
|
||||||
|
) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (release.grantedPermissions) {
|
||||||
|
return release.grantedPermissions
|
||||||
|
}
|
||||||
|
const granted = await this.confirmExtensionPermissions(permissions)
|
||||||
|
if (!granted) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
release.grantedPermissions = permissions
|
||||||
|
return permissions
|
||||||
|
},
|
||||||
|
extensionPermissionsForRelease(release) {
|
||||||
|
return release.permissions || this.selectedExtension?.permissions || []
|
||||||
|
},
|
||||||
|
releaseRequiresPermissionGrant(release) {
|
||||||
|
return (
|
||||||
|
release.extension_type === 'wasm' ||
|
||||||
|
this.selectedExtension?.isWasm === true
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmExtensionPermissions(permissions) {
|
||||||
|
const permissionItems = permissions
|
||||||
|
.map(permission => {
|
||||||
|
const label = this.escapeHtml(permission.label || permission.id)
|
||||||
|
const description = permission.description
|
||||||
|
? `<div class="text-caption text-grey-7">${this.escapeHtml(
|
||||||
|
permission.description
|
||||||
|
)}</div>`
|
||||||
|
: ''
|
||||||
|
return `<li><strong>${label}</strong>${description}</li>`
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.$q
|
||||||
|
.dialog({
|
||||||
|
title: 'Grant extension permissions',
|
||||||
|
message: `<p>This WASM extension requests these permissions:</p><ul>${permissionItems}</ul>`,
|
||||||
|
html: true,
|
||||||
|
persistent: true,
|
||||||
|
ok: {
|
||||||
|
label: 'Grant and install',
|
||||||
|
color: 'primary',
|
||||||
|
flat: true
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: this.$t('cancel'),
|
||||||
|
color: 'grey',
|
||||||
|
flat: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onOk(() => resolve(true))
|
||||||
|
.onCancel(() => resolve(false))
|
||||||
|
.onDismiss(() => resolve(false))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
},
|
||||||
async selectAllUpdatableExtensionss() {
|
async selectAllUpdatableExtensionss() {
|
||||||
this.updatableExtensions.forEach(e => (e.selectedForUpdate = true))
|
this.updatableExtensions.forEach(e => (e.selectedForUpdate = true))
|
||||||
},
|
},
|
||||||
@@ -646,6 +734,13 @@ window.PageExtensions = {
|
|||||||
if (!ext.selectedForUpdate) {
|
if (!ext.selectedForUpdate) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (ext.isWasm) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: `Skipping ${ext.id}; WASM updates require permission approval.`
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
ext.inProgress = true
|
ext.inProgress = true
|
||||||
await LNbits.api.request('POST', `/api/v1/extension`, null, {
|
await LNbits.api.request('POST', `/api/v1/extension`, null, {
|
||||||
ext_id: ext.id,
|
ext_id: ext.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user