Merge branch 'master' into Jukebox
This commit is contained in:
commit
3a8a8f95e9
17 changed files with 122 additions and 39 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -267,11 +267,22 @@ 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:
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@
|
|||
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
|
||||
<a
|
||||
class="inherit"
|
||||
:href="['/', props.row.tag, '?usr=', user.id].join('')"
|
||||
:href="['/', props.row.tag, '/?usr=', user.id].join('')"
|
||||
>
|
||||
#{{ props.row.tag }}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -88,6 +88,9 @@ async def api_link_create_or_update(link_id=None):
|
|||
):
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -48,6 +48,26 @@
|
|||
</q-card-section>
|
||||
</q-card>
|
||||
</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/<user_id></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/<user_id> -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-card>
|
||||
<q-card-section>
|
||||
|
|
@ -114,7 +134,8 @@
|
|||
</h5>
|
||||
<code
|
||||
>{"admin_id": <string>, "user_name": <string>,
|
||||
"wallet_name": <string>}</code
|
||||
"wallet_name": <string>,"email": <Optional string>
|
||||
,"password": <Optional string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
|
|
@ -128,7 +149,8 @@
|
|||
<code
|
||||
>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"
|
||||
</code>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -157,6 +157,18 @@
|
|||
v-model.trim="userDialog.data.walname"
|
||||
label="Initial wallet name"
|
||||
></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
|
||||
unelevated
|
||||
|
|
@ -224,7 +236,14 @@
|
|||
usersTable: {
|
||||
columns: [
|
||||
{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: {
|
||||
rowsPerPage: 10
|
||||
|
|
@ -299,7 +318,9 @@
|
|||
var data = {
|
||||
admin_id: this.g.user.id,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
@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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue