[feat] Pay to enable extension (#2516)

* feat: add payment tab

* feat: add buttons

* feat: persist `pay to enable` changes

* fix: do not disable extension on upgrade

* fix: show releases tab first

* feat: extract `enableExtension` logic

* refactor: rename routes

* feat: show dialog for paying extension

* feat: create invoice to enable

* refactor: extract enable/disable extension logic

* feat: add extra info to UserExtensions

* feat: check payment for extension enable

* fix: parsing

* feat: admins must not pay

* fix: code checks

* fix: test

* refactor: extract extension activate/deactivate to the `api` side

* feat: add `get_user_extensions `

* feat: return explicit `requiresPayment`

* feat: add `isPaymentRequired` to extension list

* fix: `paid_to_enable` status

* fix: ui layout

* feat: show QR Code

* feat: wait for invoice to be paid

* test: removed deprecated test and dead code

* feat: add re-check button

* refactor: rename paths for endpoints

* feat: i18n

* feat: add `{"success": True}`

* test: fix listener

* fix: rebase errors

* chore: update bundle

* fix: return error status code for the HTML error pages

* fix: active extension loading from file system

* chore: temp commit

* fix: premature optimisation

* chore: make check

* refactor: remove extracted logic

* chore: code format

* fix: enable by default after install

* fix: use `discard` instead of `remove` for `set`

* chore: code format

* fix: better error code

* fix: check for stop function before invoking

* feat: check if the wallet belongs to the admin user

* refactor: return 402 Requires Payment

* chore: more typing

* chore: temp checkout different branch for tests

* fix: too much typing

* fix: remove try-except

* fix: typo

* fix: manual format

* fix: merge issue

* remove this line

---------

Co-authored-by: dni  <office@dnilabs.com>
This commit is contained in:
Vlad Stan
2024-05-28 12:07:33 +01:00
committed by GitHub
co-authored by dni ⚡
parent 7c68a02eee
commit d72cf40439
16 changed files with 785 additions and 189 deletions
+14 -65
View File
@@ -1,4 +1,3 @@
import sys
from http import HTTPStatus
from pathlib import Path
from typing import Annotated, List, Optional, Union
@@ -11,7 +10,6 @@ from fastapi.routing import APIRouter
from loguru import logger
from pydantic.types import UUID4
from lnbits.core.db import core_app_extra
from lnbits.core.helpers import to_valid_user_id
from lnbits.core.models import User
from lnbits.decorators import check_admin, check_user_exists
@@ -24,11 +22,8 @@ from ...utils.exchange_rates import allowed_currencies, currencies
from ..crud import (
create_wallet,
get_dbversions,
get_inactive_extensions,
get_installed_extensions,
get_user,
update_installed_extension_state,
update_user_extension,
)
generic_router = APIRouter(
@@ -73,19 +68,8 @@ async def robots():
return HTMLResponse(content=data, media_type="text/plain")
@generic_router.get(
"/extensions", name="install.extensions", response_class=HTMLResponse
)
async def extensions_install(
request: Request,
user: User = Depends(check_user_exists),
activate: str = Query(None),
deactivate: str = Query(None),
enable: str = Query(None),
disable: str = Query(None),
):
await toggle_extension(enable, disable, user.id)
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
async def extensions(request: Request, user: User = Depends(check_user_exists)):
try:
installed_exts: List["InstallableExtension"] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts]
@@ -100,6 +84,11 @@ async def extensions_install(
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
if installed_ext:
e.installed_release = installed_ext.installed_release
if installed_ext.pay_to_enable and not user.admin:
# not a security leak, but better not to share the wallet id
installed_ext.pay_to_enable.wallet = None
e.pay_to_enable = installed_ext.pay_to_enable
# use the installed extension values
e.name = installed_ext.name
e.short_description = installed_ext.short_description
@@ -111,26 +100,10 @@ async def extensions_install(
installed_exts_ids = []
try:
ext_id = activate or deactivate
all_extensions = get_valid_extensions()
ext = next((e for e in all_extensions if e.code == ext_id), None)
if ext_id and user.admin:
if deactivate:
settings.lnbits_deactivated_extensions.add(deactivate)
elif activate:
# if extension never loaded (was deactivated on server startup)
if ext_id not in sys.modules.keys():
# run extension start-up routine
core_app_extra.register_new_ext_routes(ext)
settings.lnbits_deactivated_extensions.remove(activate)
await update_installed_extension_state(
ext_id=ext_id, active=activate is not None
)
all_ext_ids = [ext.code for ext in all_extensions]
inactive_extensions = await get_inactive_extensions()
all_ext_ids = [ext.code for ext in get_valid_extensions()]
inactive_extensions = [
e.id for e in await get_installed_extensions(active=False)
]
db_version = await get_dbversions()
extensions = [
{
@@ -152,6 +125,8 @@ async def extensions_install(
"installedRelease": (
dict(ext.installed_release) if ext.installed_release else None
),
"payToEnable": (dict(ext.pay_to_enable) if ext.pay_to_enable else {}),
"isPaymentRequired": ext.requires_payment,
}
for ext in installable_exts
]
@@ -199,7 +174,7 @@ async def wallet(
user_wallet = user.get_wallet(wallet_id)
if not user_wallet or user_wallet.deleted:
return template_renderer().TemplateResponse(
request, "error.html", {"err": "Wallet not found"}
request, "error.html", {"err": "Wallet not found"}, HTTPStatus.NOT_FOUND
)
resp = template_renderer().TemplateResponse(
@@ -414,29 +389,3 @@ async def hex_to_uuid4(hex_value: str):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
) from exc
async def toggle_extension(extension_to_enable, extension_to_disable, user_id):
if extension_to_enable and extension_to_disable:
raise HTTPException(
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
)
# check if extension exists
if extension_to_enable or extension_to_disable:
ext = extension_to_enable or extension_to_disable
if ext not in [e.code for e in get_valid_extensions()]:
raise HTTPException(
HTTPStatus.BAD_REQUEST, f"Extension '{ext}' doesn't exist."
)
if extension_to_enable:
logger.info(f"Enabling extension: {extension_to_enable} for user {user_id}")
await update_user_extension(
user_id=user_id, extension=extension_to_enable, active=True
)
elif extension_to_disable:
logger.info(f"Disabling extension: {extension_to_disable} for user {user_id}")
await update_user_extension(
user_id=user_id, extension=extension_to_disable, active=False
)