diff --git a/README.md b/README.md index 61fa7965..bc700bfd 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ LNbits is a very simple Python server that sits on top of any funding source, an * Fallback wallet for the LNURL scheme * Instant wallet for LN demonstrations -LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularily. +LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly. See [lnbits.org](https://lnbits.org) for more detailed documentation. diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 4f1bb853..eabef16e 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -13,6 +13,7 @@ Download this repo and install the dependencies: ```sh git clone https://github.com/lnbits/lnbits.git cd lnbits/ +# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work python3 -m venv venv ./venv/bin/pip install -r requirements.txt cp .env.example .env diff --git a/lnbits/app.py b/lnbits/app.py index cd700f5c..6137fdaf 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -1,6 +1,7 @@ import sys -import importlib import warnings +import importlib +import traceback from quart import g from quart_trio import QuartTrio @@ -23,7 +24,6 @@ from .tasks import ( invoice_listener, internal_invoice_listener, webhook_handler, - grab_app_for_later, ) from .settings import WALLET @@ -48,7 +48,7 @@ def create_app(config_object="lnbits.settings") -> QuartTrio: register_commands(app) register_request_hooks(app) register_async_tasks(app) - grab_app_for_later(app) + register_exception_handlers(app) return app @@ -114,10 +114,6 @@ def register_filters(app: QuartTrio): def register_request_hooks(app: QuartTrio): """Open the core db for each request so everything happens in a big transaction""" - @app.before_request - async def before_request(): - g.nursery = app.nursery - @app.after_request async def set_secure_headers(response): secure_headers.quart(response) @@ -139,3 +135,21 @@ def register_async_tasks(app): @app.after_serving async def stop_listeners(): pass + + +def register_exception_handlers(app): + @app.errorhandler(Exception) + async def basic_error(err): + etype, value, tb = sys.exc_info() + traceback.print_exception(etype, err, tb) + exc = traceback.format_exc() + return ( + "\n\n".join( + [ + "LNbits internal error!", + exc, + "If you believe this shouldn't be an error please bring it up on https://t.me/lnbits", + ] + ), + 500, + ) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index b9f02070..47623cc2 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -267,12 +267,23 @@ async def get_payments( async def delete_expired_invoices( conn: Optional[Connection] = None, ) -> None: + # first we delete all invoices older than one month + await (conn or db).execute( + """ + DELETE FROM apipayments + WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 2592000 + """ + ) + + # then we delete all expired invoices, checking one by one rows = await (conn or db).fetchall( """ SELECT bolt11 FROM apipayments - WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 86400 - """ + WHERE pending = 1 + AND bolt11 IS NOT NULL + AND amount > 0 AND time < strftime('%s', 'now') - 86400 + """ ) for (payment_request,) in rows: try: diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index c6851793..b3f7b1c1 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -129,7 +129,7 @@ #{{ props.row.tag }} diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 89330ab3..2547435e 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -3,7 +3,7 @@ import json import lnurl # type: ignore import httpx from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult -from quart import g, jsonify, make_response, url_for +from quart import g, current_app, jsonify, make_response, url_for from http import HTTPStatus from binascii import unhexlify from typing import Dict, Union @@ -310,8 +310,8 @@ async def api_payments_sse(): await send_event.send(("keepalive", "")) await trio.sleep(25) - g.nursery.start_soon(payment_received) - g.nursery.start_soon(repeat_keepalive) + current_app.nursery.start_soon(payment_received) + current_app.nursery.start_soon(repeat_keepalive) async def send_events(): try: diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index e17d71b3..f48b054f 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -2,6 +2,7 @@ from os import path from http import HTTPStatus from quart import ( g, + current_app, abort, jsonify, request, @@ -154,7 +155,7 @@ async def lnurl_full_withdraw_callback(): async def pay(): await pay_invoice(wallet_id=wallet.id, payment_request=pr) - g.nursery.start_soon(pay) + current_app.nursery.start_soon(pay) balance_notify = request.args.get("balanceNotify") if balance_notify: @@ -197,7 +198,7 @@ async def lnurlwallet(): user = await get_user(account.id, conn=conn) wallet = await create_wallet(user_id=user.id, conn=conn) - g.nursery.start_soon( + current_app.nursery.start_soon( redeem_lnurl_withdraw, wallet.id, request.args.get("lightning"), diff --git a/lnbits/extensions/livestream/lnurl.py b/lnbits/extensions/livestream/lnurl.py index 1e021f85..3b9e7e31 100644 --- a/lnbits/extensions/livestream/lnurl.py +++ b/lnbits/extensions/livestream/lnurl.py @@ -61,7 +61,7 @@ async def lnurl_callback(track_id): if not track: return jsonify({"status": "ERROR", "reason": "Couldn't find track."}) - amount_received = int(request.args.get("amount")) + amount_received = int(request.args.get("amount") or 0) if amount_received < track.min_sendable: return ( diff --git a/lnbits/extensions/lnurlp/lnurl.py b/lnbits/extensions/lnurlp/lnurl.py index 79076564..5d8bcf08 100644 --- a/lnbits/extensions/lnurlp/lnurl.py +++ b/lnbits/extensions/lnurlp/lnurl.py @@ -54,7 +54,7 @@ async def api_lnurl_callback(link_id): min = link.min * 1000 max = link.max * 1000 - amount_received = int(request.args.get("amount")) + amount_received = int(request.args.get("amount") or 0) if amount_received < min: return ( jsonify( diff --git a/lnbits/extensions/lnurlp/views_api.py b/lnbits/extensions/lnurlp/views_api.py index de36345a..cbe1bc4e 100644 --- a/lnbits/extensions/lnurlp/views_api.py +++ b/lnbits/extensions/lnurlp/views_api.py @@ -87,6 +87,9 @@ async def api_link_create_or_update(link_id=None): round(g.data["min"]) != g.data["min"] or round(g.data["max"]) != g.data["max"] ): return jsonify({"message": "Must use full satoshis."}), HTTPStatus.BAD_REQUEST + + if g.data["success_url"][:8] != "https://": + return jsonify({"message": "Success URL must be secure https://..."}), HTTPStatus.BAD_REQUEST if link_id: link = await get_pay_link(link_id) diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py index 13944a29..d99e4cea 100644 --- a/lnbits/extensions/offlineshop/lnurl.py +++ b/lnbits/extensions/offlineshop/lnurl.py @@ -49,7 +49,7 @@ async def lnurl_callback(item_id): min = price * 995 max = price * 1010 - amount_received = int(request.args.get("amount")) + amount_received = int(request.args.get("amount") or 0) if amount_received < min: return jsonify( LnurlErrorResponse( diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html index d62f8f38..74fd4ade 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -452,7 +452,10 @@ id: this.domainDialog.data.wallet }) var data = this.domainDialog.data - data.allowed_record_types = data.allowed_record_types.join(', ') + data.allowed_record_types = + typeof data.allowed_record_types === 'string' + ? data.allowed_record_types + : data.allowed_record_types.join(', ') console.log(this.domainDialog) if (data.id) { this.updateDomain(wallet, data) diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py index 6470c413..dbee287c 100644 --- a/lnbits/extensions/usermanager/crud.py +++ b/lnbits/extensions/usermanager/crud.py @@ -17,7 +17,11 @@ from .models import Users, Wallets async def create_usermanager_user( - user_name: str, wallet_name: str, admin_id: str + user_name: str, + wallet_name: str, + admin_id: str, + email: Optional[str] = None, + password: Optional[str] = None, ) -> Users: account = await create_account() user = await get_user(account.id) @@ -27,10 +31,10 @@ async def create_usermanager_user( await db.execute( """ - INSERT INTO users (id, name, admin) - VALUES (?, ?, ?) + INSERT INTO users (id, name, admin, email, password) + VALUES (?, ?, ?, ?, ?) """, - (user.id, user_name, admin_id), + (user.id, user_name, admin_id, email, password), ) await db.execute( diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html index fbd13e72..2f516b28 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html +++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html @@ -48,6 +48,26 @@ + + + + GET + /usermanager/api/v1/users/<user_id> +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON list of users +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/users/<user_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
@@ -114,7 +134,8 @@ {"admin_id": <string>, "user_name": <string>, - "wallet_name": <string>}
Returns 201 CREATED (application/json) @@ -128,7 +149,8 @@ curl -X POST {{ request.url_root }}api/v1/users -d '{"admin_id": "{{ g.user.id }}", "wallet_name": <string>, "user_name": - <string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H + <string>, "email": <Optional string>, "password": < + Optional string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H "Content-type: application/json" diff --git a/lnbits/extensions/usermanager/templates/usermanager/index.html b/lnbits/extensions/usermanager/templates/usermanager/index.html index 44837a54..a7514089 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/index.html +++ b/lnbits/extensions/usermanager/templates/usermanager/index.html @@ -157,6 +157,18 @@ v-model.trim="userDialog.data.walname" label="Initial wallet name" > + + ", methods=["GET"]) +@api_check_wallet_key(key_type="invoice") +async def api_usermanager_user(user_id): + user = await get_usermanager_user(user_id) + return ( + jsonify(user._asdict()), + HTTPStatus.OK, + ) + + @usermanager_ext.route("/api/v1/users", methods=["POST"]) @api_check_wallet_key(key_type="invoice") @api_validate_post_request( schema={ - "admin_id": {"type": "string", "empty": False, "required": True}, "user_name": {"type": "string", "empty": False, "required": True}, "wallet_name": {"type": "string", "empty": False, "required": True}, + "admin_id": {"type": "string", "empty": False, "required": True}, + "email": {"type": "string", "required": False}, + "password": {"type": "string", "required": False}, } ) async def api_usermanager_users_create(): - user = await create_usermanager_user( - g.data["user_name"], g.data["wallet_name"], g.data["admin_id"] - ) + user = await create_usermanager_user(**g.data) return jsonify(user._asdict()), HTTPStatus.CREATED diff --git a/lnbits/tasks.py b/lnbits/tasks.py index 0e2ff98d..756b1142 100644 --- a/lnbits/tasks.py +++ b/lnbits/tasks.py @@ -13,13 +13,6 @@ from lnbits.core.crud import ( ) from lnbits.core.services import redeem_lnurl_withdraw -main_app: Optional[QuartTrio] = None - - -def grab_app_for_later(app: QuartTrio): - global main_app - main_app = app - deferred_async: List[Callable] = []