From fe79709698cbcdcd17536c5e46a3d7e6a5c4bee9 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Sun, 29 Aug 2021 19:38:42 +0200 Subject: [PATCH] fix: several more API calls restored --- lnbits/core/models.py | 6 +- lnbits/core/views/api.py | 165 ++++++++++++++++------------------- lnbits/core/views/generic.py | 28 +++--- lnbits/decorators.py | 117 +++++++++++++++++++++---- lnbits/static/js/base.js | 34 ++++---- 5 files changed, 208 insertions(+), 142 deletions(-) diff --git a/lnbits/core/models.py b/lnbits/core/models.py index d7d211bfd..672a252c5 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -84,10 +84,10 @@ class Payment(BaseModel): bolt11: str preimage: str payment_hash: str - extra: Dict + extra: Optional[Dict] = {} wallet_id: str - webhook: str - webhook_status: int + webhook: Optional[str] + webhook_status: Optional[int] @classmethod def from_row(cls, row: Row): diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 7f6604785..f277e6cc4 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -1,69 +1,59 @@ -from lnbits.helpers import url_for -from fastapi.param_functions import Depends -from lnbits.auth_bearer import AuthBearer -from pydantic import BaseModel -import trio -import json -import httpx import hashlib -from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult - -from fastapi import Query - -from http import HTTPStatus +import json from binascii import unhexlify -from typing import Dict, List, Optional, Union +from http import HTTPStatus +from typing import Dict, Optional, Union +from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse + +import httpx +import trio +from fastapi import Query, security +from fastapi.exceptions import HTTPException +from fastapi.param_functions import Depends +from fastapi.params import Body +from pydantic import BaseModel from lnbits import bolt11, lnurl -from lnbits.decorators import api_check_wallet_key, api_validate_post_request -from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis +from lnbits.core.models import Wallet +from lnbits.decorators import (WalletAdminKeyChecker, WalletInvoiceKeyChecker, + WalletTypeInfo, get_key_type) +from lnbits.helpers import url_for from lnbits.requestvars import g +from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis from .. import core_app, db from ..crud import get_payments, save_balance_check, update_wallet -from ..services import ( - PaymentFailure, - InvoiceFailure, - create_invoice, - pay_invoice, - perform_lnurlauth, -) +from ..services import (InvoiceFailure, PaymentFailure, create_invoice, + pay_invoice, perform_lnurlauth) from ..tasks import api_invoice_listeners -@core_app.get( - "/api/v1/wallet", - # dependencies=[Depends(AuthBearer())] -) -# @api_check_wallet_key("invoice") -async def api_wallet(): +@core_app.get("/api/v1/wallet") +async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)): return ( - {"id": g().wallet.id, "name": g().wallet.name, "balance": g().wallet.balance_msat}, + {"id": wallet.wallet.id, "name": wallet.wallet.name, "balance": wallet.wallet.balance_msat}, HTTPStatus.OK, ) @core_app.put("/api/v1/wallet/{new_name}") -@api_check_wallet_key("invoice") -async def api_update_wallet(new_name: str): - await update_wallet(g().wallet.id, new_name) +async def api_update_wallet(new_name: str, wallet: WalletTypeInfo = Depends(get_key_type)): + await update_wallet(wallet.wallet.id, new_name) return ( { - "id": g().wallet.id, - "name": g().wallet.name, - "balance": g().wallet.balance_msat, + "id": wallet.wallet.id, + "name": wallet.wallet.name, + "balance": wallet.wallet.balance_msat, }, HTTPStatus.OK, ) @core_app.get("/api/v1/payments") -@api_check_wallet_key("invoice") -async def api_payments(): - return ( - await get_payments(wallet_id=g().wallet.id, pending=True, complete=True), - HTTPStatus.OK, - ) +async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)): + return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True) + + class CreateInvoiceData(BaseModel): amount: int = Query(None, ge=1) @@ -75,9 +65,7 @@ class CreateInvoiceData(BaseModel): extra: Optional[dict] = None webhook: Optional[str] = None -@api_check_wallet_key("invoice") -# async def api_payments_create_invoice(amount: List[str] = Query([type: str = Query(None)])): -async def api_payments_create_invoice(data: CreateInvoiceData): +async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): if "description_hash" in data: description_hash = unhexlify(data.description_hash) memo = "" @@ -94,7 +82,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData): async with db.connect() as conn: try: payment_hash, payment_request = await create_invoice( - wallet_id=g().wallet.id, + wallet_id=wallet.id, amount=amount, memo=memo, description_hash=description_hash, @@ -151,10 +139,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData): ) -@api_check_wallet_key("admin") -async def api_payments_pay_invoice( - bolt11: str = Query(...), wallet: Optional[List[str]] = Query(None) -): +async def api_payments_pay_invoice(bolt11: str, wallet: Wallet): try: payment_hash = await pay_invoice( wallet_id=wallet.id, @@ -179,11 +164,20 @@ async def api_payments_pay_invoice( ) -@core_app.post("/api/v1/payments") -async def api_payments_create(out: bool = True): - if out is True: - return await api_payments_pay_invoice() - return await api_payments_create_invoice() +@core_app.post("/api/v1/payments", deprecated=True, + description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead") +async def api_payments_create(wallet: WalletTypeInfo = Depends(get_key_type), out: bool = True, + invoiceData: Optional[CreateInvoiceData] = Body(None), + bolt11: Optional[str] = Query(None)): + + if wallet.wallet_type < 0 or wallet.wallet_type > 2: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid") + + if out is True and wallet.wallet_type == 0: + if not bolt11: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="BOLT11 string is invalid or not given") + return await api_payments_pay_invoice(bolt11, wallet.wallet) # admin key + return await api_payments_create_invoice(invoiceData, wallet.wallet) # invoice key class CreateLNURLData(BaseModel): description_hash: str @@ -192,8 +186,7 @@ class CreateLNURLData(BaseModel): comment: Optional[str] = None description: Optional[str] = None -@core_app.post("/api/v1/payments/lnurl") -@api_check_wallet_key("admin") +@core_app.post("/api/v1/payments/lnurl", dependencies=[Depends(WalletAdminKeyChecker())]) async def api_payments_pay_lnurl(data: CreateLNURLData): domain = urlparse(data.callback).netloc @@ -258,32 +251,9 @@ async def api_payments_pay_lnurl(data: CreateLNURLData): HTTPStatus.CREATED, ) - -@core_app.get("/api/v1/payments/{payment_hash}") -@api_check_wallet_key("invoice") -async def api_payment(payment_hash): - payment = await g().wallet.get_payment(payment_hash) - - if not payment: - return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND - elif not payment.pending: - return {"paid": True, "preimage": payment.preimage}, HTTPStatus.OK - - try: - await payment.check_pending() - except Exception: - return {"paid": False}, HTTPStatus.OK - - return ( - {"paid": not payment.pending, "preimage": payment.preimage}, - HTTPStatus.OK, - ) - - @core_app.get("/api/v1/payments/sse") -@api_check_wallet_key("invoice", accept_querystring=True) -async def api_payments_sse(): - this_wallet_id = g().wallet.id +async def api_payments_sse(wallet: WalletTypeInfo = Depends(get_key_type)): + this_wallet_id = wallet.wallet.id send_payment, receive_payment = trio.open_memory_channel(0) @@ -303,9 +273,10 @@ async def api_payments_sse(): await send_event.send(("keepalive", "")) await trio.sleep(25) - current_app.nursery.start_soon(payment_received) - current_app.nursery.start_soon(repeat_keepalive) - + async with trio.open_nursery() as nursery: + nursery.start_soon(payment_received) + nursery.start_soon(repeat_keepalive) + async def send_events(): try: async for typ, data in event_to_send: @@ -332,9 +303,26 @@ async def api_payments_sse(): response.timeout = None return response +@core_app.get("/api/v1/payments/{payment_hash}") +async def api_payment(payment_hash, wallet: WalletTypeInfo = Depends(get_key_type)): + payment = await wallet.wallet.get_payment(payment_hash) -@core_app.get("/api/v1/lnurlscan/{code}") -@api_check_wallet_key("invoice") + if not payment: + return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND + elif not payment.pending: + return {"paid": True, "preimage": payment.preimage}, HTTPStatus.OK + + try: + await payment.check_pending() + except Exception: + return {"paid": False}, HTTPStatus.OK + + return ( + {"paid": not payment.pending, "preimage": payment.preimage}, + HTTPStatus.OK, + ) + +@core_app.get("/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())]) async def api_lnurlscan(code: str): try: url = lnurl.decode(code) @@ -443,8 +431,7 @@ async def api_lnurlscan(code: str): return params -@core_app.post("/api/v1/lnurlauth") -@api_check_wallet_key("admin") +@core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())]) async def api_perform_lnurlauth(callback: str): err = await perform_lnurlauth(callback) if err: @@ -452,6 +439,6 @@ async def api_perform_lnurlauth(callback: str): return "", HTTPStatus.OK -@core_app.route("/api/v1/currencies", methods=["GET"]) +@core_app.get("/api/v1/currencies") async def api_list_currencies_available(): return list(currencies.keys()) diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index c5d6b3f24..d628e6990 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -2,12 +2,14 @@ from http import HTTPStatus from typing import Optional from fastapi import Request, status +from fastapi.exceptions import HTTPException from fastapi.param_functions import Body from fastapi.params import Depends, Query from fastapi.responses import FileResponse, RedirectResponse from fastapi.routing import APIRouter from pydantic.types import UUID4 from starlette.responses import HTMLResponse +import trio from lnbits.core import db from lnbits.helpers import template_renderer, url_for @@ -40,9 +42,7 @@ async def extensions(request: Request, enable: str, disable: str): extension_to_disable = disable if extension_to_enable and extension_to_disable: - abort( - HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension." - ) + raise HTTPException(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.") if extension_to_enable: await update_user_extension( @@ -142,7 +142,8 @@ async def lnurl_full_withdraw_callback(request: Request): except: pass - current_app.nursery.start_soon(pay) + async with trio.open_nursery() as n: + n.start_soon(pay) balance_notify = request.args.get("balanceNotify") if balance_notify: @@ -159,7 +160,7 @@ async def deletewallet(request: Request): user_wallet_ids = g().user.wallet_ids if wallet_id not in user_wallet_ids: - abort(HTTPStatus.FORBIDDEN, "Not your wallet.") + raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.") else: await delete_wallet(user_id=g().user.id, wallet_id=wallet_id) user_wallet_ids.remove(wallet_id) @@ -186,16 +187,17 @@ async def lnurlwallet(request: Request): user = await get_user(account.id, conn=conn) wallet = await create_wallet(user_id=user.id, conn=conn) - current_app.nursery.start_soon( - redeem_lnurl_withdraw, - wallet.id, - request.args.get("lightning"), - "LNbits initial funding: voucher redeem.", - {"tag": "lnurlwallet"}, - 5, # wait 5 seconds before sending the invoice to the service + async with trio.open_nursery() as n: + n.start_soon( + redeem_lnurl_withdraw, + wallet.id, + request.args.get("lightning"), + "LNbits initial funding: voucher redeem.", + {"tag": "lnurlwallet"}, + 5, # wait 5 seconds before sending the invoice to the service ) - return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) + return RedirectResponse(f"/wallet?usr={user.id}&wal={wallet.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT) @core_html_routes.get("/manifest/{usr}.webmanifest") diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 880ddc5f3..372d3955b 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -1,35 +1,114 @@ -from cerberus import Validator # type: ignore from functools import wraps from http import HTTPStatus + +from fastapi.security import api_key +from lnbits.core.models import Wallet from typing import List, Union from uuid import UUID +from cerberus import Validator # type: ignore +from fastapi.exceptions import HTTPException +from fastapi.openapi.models import APIKey, APIKeyIn +from fastapi.params import Security +from fastapi.security.api_key import APIKeyHeader, APIKeyQuery +from fastapi.security.base import SecurityBase +from starlette.requests import Request + from lnbits.core.crud import get_user, get_wallet_for_key -from lnbits.settings import LNBITS_ALLOWED_USERS from lnbits.requestvars import g +from lnbits.settings import LNBITS_ALLOWED_USERS -def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False): - def wrap(view): - @wraps(view) - async def wrapped_view(**kwargs): - try: - key_value = request.headers.get("X-Api-Key") or request.args["api-key"] - g().wallet = await get_wallet_for_key(key_value, key_type) - except KeyError: - return ( - jsonify({"message": "`X-Api-Key` header missing."}), - HTTPStatus.BAD_REQUEST, - ) - if not g().wallet: - return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED +class KeyChecker(SecurityBase): + def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None): + self.scheme_name = scheme_name or self.__class__.__name__ + self.auto_error = auto_error + self._key_type = "invoice" + self._api_key = api_key + if api_key: + self.model: APIKey= APIKey( + **{"in": APIKeyIn.query}, name="X-API-KEY", description="Wallet API Key - QUERY" + ) + else: + self.model: APIKey= APIKey( + **{"in": APIKeyIn.header}, name="X-API-KEY", description="Wallet API Key - HEADER" + ) + self.wallet = None - return await view(**kwargs) + async def __call__(self, request: Request) -> Wallet: + try: + key_value = self._api_key if self._api_key else request.headers.get("X-API-KEY") or request.query_params["api-key"] + # FIXME: Find another way to validate the key. A fetch from DB should be avoided here. + # Also, we should not return the wallet here - thats silly. + # Possibly store it in a Redis DB + self.wallet = await get_wallet_for_key(key_value, self._key_type) + if not self.wallet: + raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid key or expired key.") - return wrapped_view + except KeyError: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, + detail="`X-API-KEY` header missing.") - return wrap +class WalletInvoiceKeyChecker(KeyChecker): + """ + WalletInvoiceKeyChecker will ensure that the provided invoice + wallet key is correct and populate g().wallet with the wallet + for the key in `X-API-key`. + The checker will raise an HTTPException when the key is wrong in some ways. + """ + def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None): + super().__init__(scheme_name, auto_error, api_key) + self._key_type = "invoice" + +class WalletAdminKeyChecker(KeyChecker): + """ + WalletAdminKeyChecker will ensure that the provided admin + wallet key is correct and populate g().wallet with the wallet + for the key in `X-API-key`. + + The checker will raise an HTTPException when the key is wrong in some ways. + """ + def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None): + super().__init__(scheme_name, auto_error, api_key) + self._key_type = "admin" + +class WalletTypeInfo(): + wallet_type: int + wallet: Wallet + + def __init__(self, wallet_type: int, wallet: Wallet) -> None: + self.wallet_type = wallet_type + self.wallet = wallet + + +api_key_header = APIKeyHeader(name="X-API-KEY", auto_error=False, description="Admin or Invoice key for wallet API's") +api_key_query = APIKeyQuery(name="api-key", auto_error=False, description="Admin or Invoice key for wallet API's") +async def get_key_type(r: Request, + api_key_header: str = Security(api_key_header), + api_key_query: str = Security(api_key_query)) -> WalletTypeInfo: + # 0: admin + # 1: invoice + # 2: invalid + try: + checker = WalletAdminKeyChecker(api_key=api_key_query) + await checker.__call__(r) + return WalletTypeInfo(0, checker.wallet) + except HTTPException as e: + if e.status_code == HTTPStatus.UNAUTHORIZED: + pass + except: + raise + + try: + checker = WalletInvoiceKeyChecker() + await checker.__call__(r) + return WalletTypeInfo(1, checker.wallet) + except HTTPException as e: + if e.status_code == HTTPStatus.UNAUTHORIZED: + return WalletTypeInfo(2, None) + except: + raise def api_validate_post_request(*, schema: dict): def wrap(view): diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index bb65a8245..fec75796b 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -161,24 +161,22 @@ window.LNbits = { return newWallet }, payment: function (data) { - var obj = _.object( - [ - 'checking_id', - 'pending', - 'amount', - 'fee', - 'memo', - 'time', - 'bolt11', - 'preimage', - 'payment_hash', - 'extra', - 'wallet_id', - 'webhook', - 'webhook_status' - ], - data - ) + obj = { + checking_id:data.id, + pending: data.pending, + amount: data.amount, + fee: data.fee, + memo: data.memo, + time: data.time, + bolt11: data.bolt11, + preimage: data.preimage, + payment_hash: data.payment_hash, + extra: data.extra, + wallet_id: data.wallet_id, + webhook: data.webhook, + webhook_status: data.webhook_status, + } + obj.date = Quasar.utils.date.formatDate( new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm'