Merge branch 'master' into Jukebox

This commit is contained in:
Ben Arc 2021-05-31 09:13:30 +01:00
commit 3a8a8f95e9
17 changed files with 122 additions and 39 deletions

View file

@ -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 * Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations * 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. See [lnbits.org](https://lnbits.org) for more detailed documentation.

View file

@ -13,6 +13,7 @@ Download this repo and install the dependencies:
```sh ```sh
git clone https://github.com/lnbits/lnbits.git git clone https://github.com/lnbits/lnbits.git
cd lnbits/ cd lnbits/
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
python3 -m venv venv python3 -m venv venv
./venv/bin/pip install -r requirements.txt ./venv/bin/pip install -r requirements.txt
cp .env.example .env cp .env.example .env

View file

@ -1,6 +1,7 @@
import sys import sys
import importlib
import warnings import warnings
import importlib
import traceback
from quart import g from quart import g
from quart_trio import QuartTrio from quart_trio import QuartTrio
@ -23,7 +24,6 @@ from .tasks import (
invoice_listener, invoice_listener,
internal_invoice_listener, internal_invoice_listener,
webhook_handler, webhook_handler,
grab_app_for_later,
) )
from .settings import WALLET from .settings import WALLET
@ -48,7 +48,7 @@ def create_app(config_object="lnbits.settings") -> QuartTrio:
register_commands(app) register_commands(app)
register_request_hooks(app) register_request_hooks(app)
register_async_tasks(app) register_async_tasks(app)
grab_app_for_later(app) register_exception_handlers(app)
return app return app
@ -114,10 +114,6 @@ def register_filters(app: QuartTrio):
def register_request_hooks(app: QuartTrio): def register_request_hooks(app: QuartTrio):
"""Open the core db for each request so everything happens in a big transaction""" """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 @app.after_request
async def set_secure_headers(response): async def set_secure_headers(response):
secure_headers.quart(response) secure_headers.quart(response)
@ -139,3 +135,21 @@ def register_async_tasks(app):
@app.after_serving @app.after_serving
async def stop_listeners(): async def stop_listeners():
pass 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,
)

View file

@ -267,11 +267,22 @@ async def get_payments(
async def delete_expired_invoices( async def delete_expired_invoices(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> 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( rows = await (conn or db).fetchall(
""" """
SELECT bolt11 SELECT bolt11
FROM apipayments 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: for (payment_request,) in rows:

View file

@ -129,7 +129,7 @@
<q-badge v-if="props.row.tag" color="yellow" text-color="black"> <q-badge v-if="props.row.tag" color="yellow" text-color="black">
<a <a
class="inherit" class="inherit"
:href="['/', props.row.tag, '?usr=', user.id].join('')" :href="['/', props.row.tag, '/?usr=', user.id].join('')"
> >
#{{ props.row.tag }} #{{ props.row.tag }}
</a> </a>

View file

@ -3,7 +3,7 @@ import json
import lnurl # type: ignore import lnurl # type: ignore
import httpx import httpx
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult 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 http import HTTPStatus
from binascii import unhexlify from binascii import unhexlify
from typing import Dict, Union from typing import Dict, Union
@ -310,8 +310,8 @@ async def api_payments_sse():
await send_event.send(("keepalive", "")) await send_event.send(("keepalive", ""))
await trio.sleep(25) await trio.sleep(25)
g.nursery.start_soon(payment_received) current_app.nursery.start_soon(payment_received)
g.nursery.start_soon(repeat_keepalive) current_app.nursery.start_soon(repeat_keepalive)
async def send_events(): async def send_events():
try: try:

View file

@ -2,6 +2,7 @@ from os import path
from http import HTTPStatus from http import HTTPStatus
from quart import ( from quart import (
g, g,
current_app,
abort, abort,
jsonify, jsonify,
request, request,
@ -154,7 +155,7 @@ async def lnurl_full_withdraw_callback():
async def pay(): async def pay():
await pay_invoice(wallet_id=wallet.id, payment_request=pr) 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") balance_notify = request.args.get("balanceNotify")
if balance_notify: if balance_notify:
@ -197,7 +198,7 @@ async def lnurlwallet():
user = await get_user(account.id, conn=conn) user = await get_user(account.id, conn=conn)
wallet = await create_wallet(user_id=user.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, redeem_lnurl_withdraw,
wallet.id, wallet.id,
request.args.get("lightning"), request.args.get("lightning"),

View file

@ -61,7 +61,7 @@ async def lnurl_callback(track_id):
if not track: if not track:
return jsonify({"status": "ERROR", "reason": "Couldn't find 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: if amount_received < track.min_sendable:
return ( return (

View file

@ -54,7 +54,7 @@ async def api_lnurl_callback(link_id):
min = link.min * 1000 min = link.min * 1000
max = link.max * 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: if amount_received < min:
return ( return (
jsonify( jsonify(

View file

@ -88,6 +88,9 @@ async def api_link_create_or_update(link_id=None):
): ):
return jsonify({"message": "Must use full satoshis."}), HTTPStatus.BAD_REQUEST 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: if link_id:
link = await get_pay_link(link_id) link = await get_pay_link(link_id)

View file

@ -49,7 +49,7 @@ async def lnurl_callback(item_id):
min = price * 995 min = price * 995
max = price * 1010 max = price * 1010
amount_received = int(request.args.get("amount")) amount_received = int(request.args.get("amount") or 0)
if amount_received < min: if amount_received < min:
return jsonify( return jsonify(
LnurlErrorResponse( LnurlErrorResponse(

View file

@ -452,7 +452,10 @@
id: this.domainDialog.data.wallet id: this.domainDialog.data.wallet
}) })
var data = this.domainDialog.data 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) console.log(this.domainDialog)
if (data.id) { if (data.id) {
this.updateDomain(wallet, data) this.updateDomain(wallet, data)

View file

@ -17,7 +17,11 @@ from .models import Users, Wallets
async def create_usermanager_user( 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: ) -> Users:
account = await create_account() account = await create_account()
user = await get_user(account.id) user = await get_user(account.id)
@ -27,10 +31,10 @@ async def create_usermanager_user(
await db.execute( await db.execute(
""" """
INSERT INTO users (id, name, admin) INSERT INTO users (id, name, admin, email, password)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", """,
(user.id, user_name, admin_id), (user.id, user_name, admin_id, email, password),
) )
await db.execute( await db.execute(

View file

@ -48,6 +48,26 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET user">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/usermanager/api/v1/users/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON list of users</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}api/v1/users/&lt;user_id&gt; -H
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET wallets"> <q-expansion-item group="api" dense expand-separator label="GET wallets">
<q-card> <q-card>
<q-card-section> <q-card-section>
@ -114,7 +134,8 @@
</h5> </h5>
<code <code
>{"admin_id": &lt;string&gt;, "user_name": &lt;string&gt;, >{"admin_id": &lt;string&gt;, "user_name": &lt;string&gt;,
"wallet_name": &lt;string&gt;}</code "wallet_name": &lt;string&gt;,"email": &lt;Optional string&gt;
,"password": &lt;Optional string&gt;}</code
> >
<h5 class="text-caption q-mt-sm q-mb-none"> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json) Returns 201 CREATED (application/json)
@ -128,7 +149,8 @@
<code <code
>curl -X POST {{ request.url_root }}api/v1/users -d '{"admin_id": "{{ >curl -X POST {{ request.url_root }}api/v1/users -d '{"admin_id": "{{
g.user.id }}", "wallet_name": &lt;string&gt;, "user_name": g.user.id }}", "wallet_name": &lt;string&gt;, "user_name":
&lt;string&gt;}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H &lt;string&gt;, "email": &lt;Optional string&gt;, "password": &lt;
Optional string&gt;}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
"Content-type: application/json" "Content-type: application/json"
</code> </code>
</q-card-section> </q-card-section>

View file

@ -157,6 +157,18 @@
v-model.trim="userDialog.data.walname" v-model.trim="userDialog.data.walname"
label="Initial wallet name" label="Initial wallet name"
></q-input> ></q-input>
<q-input
filled
dense
v-model.trim="userDialog.data.email"
label="Email"
></q-input>
<q-input
filled
dense
v-model.trim="userDialog.data.password"
label="Password"
></q-input>
<q-btn <q-btn
unelevated unelevated
@ -224,7 +236,14 @@
usersTable: { usersTable: {
columns: [ columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'}, {name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Username', field: 'name'} {name: 'name', align: 'left', label: 'Username', field: 'name'},
{name: 'email', align: 'left', label: 'Email', field: 'email'},
{
name: 'password',
align: 'left',
label: 'Password',
field: 'password'
}
], ],
pagination: { pagination: {
rowsPerPage: 10 rowsPerPage: 10
@ -299,7 +318,9 @@
var data = { var data = {
admin_id: this.g.user.id, admin_id: this.g.user.id,
user_name: this.userDialog.data.usrname, user_name: this.userDialog.data.usrname,
wallet_name: this.userDialog.data.walname wallet_name: this.userDialog.data.walname,
email: this.userDialog.data.email,
password: this.userDialog.data.password
} }
} }

View file

@ -33,19 +33,29 @@ async def api_usermanager_users():
) )
@usermanager_ext.route("/api/v1/users/<user_id>", 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"]) @usermanager_ext.route("/api/v1/users", methods=["POST"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
@api_validate_post_request( @api_validate_post_request(
schema={ schema={
"admin_id": {"type": "string", "empty": False, "required": True},
"user_name": {"type": "string", "empty": False, "required": True}, "user_name": {"type": "string", "empty": False, "required": True},
"wallet_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(): async def api_usermanager_users_create():
user = await create_usermanager_user( user = await create_usermanager_user(**g.data)
g.data["user_name"], g.data["wallet_name"], g.data["admin_id"]
)
return jsonify(user._asdict()), HTTPStatus.CREATED return jsonify(user._asdict()), HTTPStatus.CREATED

View file

@ -13,13 +13,6 @@ from lnbits.core.crud import (
) )
from lnbits.core.services import redeem_lnurl_withdraw 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] = [] deferred_async: List[Callable] = []