Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a6ff7ed02 | ||
|
|
335c056a4b |
@@ -306,7 +306,7 @@ async def update_payment_checking_id(
|
|||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
UPDATE apipayments
|
UPDATE apipayments
|
||||||
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder("now")}
|
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder('now')}
|
||||||
WHERE checking_id = :old_id
|
WHERE checking_id = :old_id
|
||||||
""", # noqa: S608
|
""", # noqa: S608
|
||||||
{
|
{
|
||||||
@@ -399,6 +399,7 @@ async def get_payment_count_stats(
|
|||||||
user_id: str | None = None,
|
user_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> list[PaymentCountStat]:
|
) -> list[PaymentCountStat]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
extra_stmts = []
|
extra_stmts = []
|
||||||
@@ -431,6 +432,7 @@ async def get_daily_stats(
|
|||||||
user_id: str | None = None,
|
user_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|
||||||
@@ -480,6 +482,7 @@ async def get_wallets_stats(
|
|||||||
user_id: str | None = None,
|
user_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> list[PaymentWalletStats]:
|
) -> list[PaymentWalletStats]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from .payments import (
|
|||||||
PaymentState,
|
PaymentState,
|
||||||
PaymentWalletStats,
|
PaymentWalletStats,
|
||||||
SettleInvoice,
|
SettleInvoice,
|
||||||
UpdatePaymentExtra,
|
|
||||||
)
|
)
|
||||||
from .tinyurl import TinyURL
|
from .tinyurl import TinyURL
|
||||||
from .users import (
|
from .users import (
|
||||||
@@ -91,7 +90,6 @@ __all__ = [
|
|||||||
"SimpleStatus",
|
"SimpleStatus",
|
||||||
"TinyURL",
|
"TinyURL",
|
||||||
"UpdateBalance",
|
"UpdateBalance",
|
||||||
"UpdatePaymentExtra",
|
|
||||||
"UpdateSuperuserPassword",
|
"UpdateSuperuserPassword",
|
||||||
"UpdateUser",
|
"UpdateUser",
|
||||||
"UpdateUserPassword",
|
"UpdateUserPassword",
|
||||||
|
|||||||
@@ -35,11 +35,6 @@ class PaymentExtra(BaseModel):
|
|||||||
lnurl_response: str | None = None
|
lnurl_response: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class UpdatePaymentExtra(BaseModel):
|
|
||||||
payment_hash: str
|
|
||||||
extra: dict = Field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
class PayInvoice(BaseModel):
|
class PayInvoice(BaseModel):
|
||||||
payment_request: str
|
payment_request: str
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from lnbits.decorators import (
|
|||||||
check_first_install,
|
check_first_install,
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import check_callback_url, template_renderer
|
from lnbits.helpers import check_callback_url, extension_id_from_path, template_renderer
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from ..crud import get_user
|
from ..crud import get_user
|
||||||
@@ -198,7 +198,9 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
|||||||
async def index(
|
async def index(
|
||||||
request: Request, user: User = Depends(check_user_exists)
|
request: Request, user: User = Depends(check_user_exists)
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer(
|
||||||
|
extension_id=extension_id_from_path(request.url.path)
|
||||||
|
).TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"base.html",
|
"base.html",
|
||||||
{
|
{
|
||||||
@@ -211,7 +213,9 @@ async def index(
|
|||||||
@generic_router.get("/node/public")
|
@generic_router.get("/node/public")
|
||||||
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
||||||
async def index_public(request: Request) -> HTMLResponse:
|
async def index_public(request: Request) -> HTMLResponse:
|
||||||
return template_renderer().TemplateResponse(request, "base.html", {"public": True})
|
return template_renderer(
|
||||||
|
extension_id=extension_id_from_path(request.url.path)
|
||||||
|
).TemplateResponse(request, "base.html", {"public": True})
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/uuidv4/{hex_value}")
|
@generic_router.get("/uuidv4/{hex_value}")
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ from lnbits.core.models import (
|
|||||||
PaymentWalletStats,
|
PaymentWalletStats,
|
||||||
SettleInvoice,
|
SettleInvoice,
|
||||||
SimpleStatus,
|
SimpleStatus,
|
||||||
UpdatePaymentExtra,
|
|
||||||
)
|
)
|
||||||
from lnbits.core.models.payments import UpdatePaymentLabels
|
from lnbits.core.models.payments import UpdatePaymentLabels
|
||||||
from lnbits.core.models.users import AccountId
|
from lnbits.core.models.users import AccountId
|
||||||
@@ -298,38 +297,6 @@ async def api_update_payment_labels(
|
|||||||
return SimpleStatus(success=True, message="Payment labels updated.")
|
return SimpleStatus(success=True, message="Payment labels updated.")
|
||||||
|
|
||||||
|
|
||||||
@payment_router.patch(
|
|
||||||
"/extra",
|
|
||||||
name="Update payment extra",
|
|
||||||
description="Append new extra metadata to a payment.",
|
|
||||||
response_model=Payment,
|
|
||||||
)
|
|
||||||
async def api_update_payment_extra(
|
|
||||||
data: UpdatePaymentExtra,
|
|
||||||
key_type: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
) -> Payment:
|
|
||||||
payment = await get_standalone_payment(
|
|
||||||
data.payment_hash, wallet_id=key_type.wallet.id
|
|
||||||
)
|
|
||||||
if payment is None:
|
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Payment does not exist.")
|
|
||||||
if not payment.success:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.BAD_REQUEST, "Payment extra can only be updated after success."
|
|
||||||
)
|
|
||||||
|
|
||||||
duplicate_keys = sorted(set(payment.extra).intersection(data.extra))
|
|
||||||
if duplicate_keys:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.BAD_REQUEST,
|
|
||||||
f"Extra keys already exist: {', '.join(duplicate_keys)}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
payment.extra.update(data.extra)
|
|
||||||
await update_payment(payment)
|
|
||||||
return payment
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get("/fee-reserve")
|
@payment_router.get("/fee-reserve")
|
||||||
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||||
invoice_obj = bolt11.decode(invoice)
|
invoice_obj = bolt11.decode(invoice)
|
||||||
|
|||||||
+47
-1
@@ -52,7 +52,45 @@ def static_url_for(static: str, path: str) -> str:
|
|||||||
return f"/{static}/{path}?v={settings.server_startup_time}"
|
return f"/{static}/{path}?v={settings.server_startup_time}"
|
||||||
|
|
||||||
|
|
||||||
def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
|
def extension_id_from_path(path: str) -> str | None:
|
||||||
|
parts = [part for part in path.split("/") if part]
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(parts) >= 3 and parts[0] == "upgrades":
|
||||||
|
return parts[2]
|
||||||
|
|
||||||
|
ext_id = parts[0]
|
||||||
|
ext_i18n_dir = Path(
|
||||||
|
settings.lnbits_extensions_path, "extensions", ext_id, "static", "i18n"
|
||||||
|
)
|
||||||
|
if ext_i18n_dir.is_dir():
|
||||||
|
return ext_id
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extension_i18n_urls(extension_id: str | None) -> list[str]:
|
||||||
|
if not extension_id:
|
||||||
|
return []
|
||||||
|
|
||||||
|
i18n_dir = Path(
|
||||||
|
settings.lnbits_extensions_path, "extensions", extension_id, "static", "i18n"
|
||||||
|
)
|
||||||
|
if not i18n_dir.is_dir():
|
||||||
|
return []
|
||||||
|
|
||||||
|
files = [file.name for file in i18n_dir.glob("*.js") if file.is_file()]
|
||||||
|
return [
|
||||||
|
static_url_for(f"{extension_id}/static", f"i18n/{filename}")
|
||||||
|
for filename in sorted(files, key=lambda name: (name != "en.js", name))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def template_renderer(
|
||||||
|
additional_folders: list | None = None,
|
||||||
|
extension_id: str | None = None,
|
||||||
|
) -> Jinja2Templates:
|
||||||
folders = [
|
folders = [
|
||||||
"lnbits/templates",
|
"lnbits/templates",
|
||||||
settings.extension_builder_working_dir_path.as_posix(),
|
settings.extension_builder_working_dir_path.as_posix(),
|
||||||
@@ -86,6 +124,14 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
||||||
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
||||||
|
|
||||||
|
if not extension_id and additional_folders:
|
||||||
|
for folder in additional_folders:
|
||||||
|
parts = Path(folder).parts
|
||||||
|
if parts and parts[-1] == "templates" and len(parts) >= 2:
|
||||||
|
extension_id = parts[-2]
|
||||||
|
break
|
||||||
|
t.env.globals["INCLUDED_EXTENSION_I18N"] = extension_i18n_urls(extension_id)
|
||||||
|
|
||||||
# backwards compatibility for extensions (tpos)
|
# backwards compatibility for extensions (tpos)
|
||||||
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
|
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,9 @@
|
|||||||
window.g.isPublicPage = false
|
window.g.isPublicPage = false
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %} {% for url in INCLUDED_EXTENSION_I18N %}
|
||||||
|
<script src="{{ url }}"></script>
|
||||||
|
{% endfor %}
|
||||||
<!-- app init -->
|
<!-- app init -->
|
||||||
<script>
|
<script>
|
||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.5.5-rc1"
|
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" }]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pytest
|
|||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from lnbits.core.crud.payments import create_payment, get_payment, get_payments
|
from lnbits.core.crud.payments import create_payment, get_payments
|
||||||
from lnbits.core.models import Account, CreateInvoice, PaymentFilters, PaymentState
|
from lnbits.core.models import Account, CreateInvoice, PaymentFilters, PaymentState
|
||||||
from lnbits.core.models.payments import CancelInvoice, CreatePayment, SettleInvoice
|
from lnbits.core.models.payments import CancelInvoice, CreatePayment, SettleInvoice
|
||||||
from lnbits.core.models.users import AccountId
|
from lnbits.core.models.users import AccountId
|
||||||
@@ -218,164 +218,6 @@ async def test_payment_api_fee_reserve_and_hold_invoice_actions(mocker):
|
|||||||
cancel_mock.assert_awaited_once()
|
cancel_mock.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_payment_extra_update_appends_new_keys(
|
|
||||||
client,
|
|
||||||
to_wallet,
|
|
||||||
adminkey_headers_to,
|
|
||||||
):
|
|
||||||
payment_hash = uuid4().hex
|
|
||||||
checking_id = await _create_payment(
|
|
||||||
to_wallet.id,
|
|
||||||
amount_msat=1_000,
|
|
||||||
payment_hash=payment_hash,
|
|
||||||
tag="splitpayments",
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await client.patch(
|
|
||||||
"/api/v1/payments/extra",
|
|
||||||
headers=adminkey_headers_to,
|
|
||||||
json={
|
|
||||||
"payment_hash": payment_hash,
|
|
||||||
"extra": {"child": "daughter", "compliance_note": "reviewed"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
extra = response.json()["extra"]
|
|
||||||
assert extra["tag"] == "splitpayments"
|
|
||||||
assert extra["child"] == "daughter"
|
|
||||||
assert extra["compliance_note"] == "reviewed"
|
|
||||||
|
|
||||||
payment = await get_payment(checking_id)
|
|
||||||
assert payment.extra == extra
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_payment_extra_update_creates_extra_when_missing(
|
|
||||||
client,
|
|
||||||
to_wallet,
|
|
||||||
adminkey_headers_to,
|
|
||||||
):
|
|
||||||
payment_hash = uuid4().hex
|
|
||||||
checking_id = await _create_payment(
|
|
||||||
to_wallet.id,
|
|
||||||
amount_msat=1_000,
|
|
||||||
payment_hash=payment_hash,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await client.patch(
|
|
||||||
"/api/v1/payments/extra",
|
|
||||||
headers=adminkey_headers_to,
|
|
||||||
json={"payment_hash": payment_hash, "extra": {"note": "reviewed"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json()["extra"] == {"note": "reviewed"}
|
|
||||||
|
|
||||||
payment = await get_payment(checking_id)
|
|
||||||
assert payment.extra == {"note": "reviewed"}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_payment_extra_update_rejects_existing_keys(
|
|
||||||
client,
|
|
||||||
to_wallet,
|
|
||||||
adminkey_headers_to,
|
|
||||||
):
|
|
||||||
payment_hash = uuid4().hex
|
|
||||||
checking_id = await _create_payment(
|
|
||||||
to_wallet.id,
|
|
||||||
amount_msat=1_000,
|
|
||||||
payment_hash=payment_hash,
|
|
||||||
tag="original",
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await client.patch(
|
|
||||||
"/api/v1/payments/extra",
|
|
||||||
headers=adminkey_headers_to,
|
|
||||||
json={"payment_hash": payment_hash, "extra": {"tag": "overwritten"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.json()["detail"] == "Extra keys already exist: tag."
|
|
||||||
|
|
||||||
payment = await get_payment(checking_id)
|
|
||||||
assert payment.extra == {"tag": "original"}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_payment_extra_update_requires_admin_key(
|
|
||||||
client,
|
|
||||||
to_wallet,
|
|
||||||
inkey_headers_to,
|
|
||||||
):
|
|
||||||
payment_hash = uuid4().hex
|
|
||||||
await _create_payment(
|
|
||||||
to_wallet.id,
|
|
||||||
amount_msat=1_000,
|
|
||||||
payment_hash=payment_hash,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await client.patch(
|
|
||||||
"/api/v1/payments/extra",
|
|
||||||
headers=inkey_headers_to,
|
|
||||||
json={"payment_hash": payment_hash, "extra": {"note": "invoice key"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json()["detail"] == "Invalid adminkey."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_payment_extra_update_is_wallet_scoped(
|
|
||||||
client,
|
|
||||||
from_wallet,
|
|
||||||
adminkey_headers_to,
|
|
||||||
):
|
|
||||||
payment_hash = uuid4().hex
|
|
||||||
await _create_payment(
|
|
||||||
from_wallet.id,
|
|
||||||
amount_msat=1_000,
|
|
||||||
payment_hash=payment_hash,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await client.patch(
|
|
||||||
"/api/v1/payments/extra",
|
|
||||||
headers=adminkey_headers_to,
|
|
||||||
json={"payment_hash": payment_hash, "extra": {"note": "wrong wallet"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert response.json()["detail"] == "Payment does not exist."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_payment_extra_update_requires_successful_payment(
|
|
||||||
client,
|
|
||||||
to_wallet,
|
|
||||||
adminkey_headers_to,
|
|
||||||
):
|
|
||||||
payment_hash = uuid4().hex
|
|
||||||
await _create_payment(
|
|
||||||
to_wallet.id,
|
|
||||||
amount_msat=1_000,
|
|
||||||
payment_hash=payment_hash,
|
|
||||||
status=PaymentState.PENDING,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await client.patch(
|
|
||||||
"/api/v1/payments/extra",
|
|
||||||
headers=adminkey_headers_to,
|
|
||||||
json={"payment_hash": payment_hash, "extra": {"note": "too early"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert (
|
|
||||||
response.json()["detail"] == "Payment extra can only be updated after success."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _create_payment(
|
async def _create_payment(
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
*,
|
*,
|
||||||
|
|||||||
Reference in New Issue
Block a user