mirror of
https://github.com/lnbits/lnbits.git
synced 2025-09-29 13:22:37 +02:00
feat: switch from Quart to FastAPI part I
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
|
from hypercorn.trio import serve
|
||||||
import trio
|
import trio
|
||||||
|
import trio_asyncio
|
||||||
|
from hypercorn.config import Config
|
||||||
|
|
||||||
from .commands import migrate_databases, transpile_scss, bundle_vendored
|
from .commands import migrate_databases, transpile_scss, bundle_vendored
|
||||||
|
|
||||||
@@ -8,7 +11,7 @@ bundle_vendored()
|
|||||||
|
|
||||||
from .app import create_app
|
from .app import create_app
|
||||||
|
|
||||||
app = create_app()
|
app = trio.run(create_app)
|
||||||
|
|
||||||
from .settings import (
|
from .settings import (
|
||||||
LNBITS_SITE_TITLE,
|
LNBITS_SITE_TITLE,
|
||||||
@@ -17,6 +20,8 @@ from .settings import (
|
|||||||
LNBITS_DATA_FOLDER,
|
LNBITS_DATA_FOLDER,
|
||||||
WALLET,
|
WALLET,
|
||||||
LNBITS_COMMIT,
|
LNBITS_COMMIT,
|
||||||
|
HOST,
|
||||||
|
PORT
|
||||||
)
|
)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
@@ -30,4 +35,6 @@ print(
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
app.run(host=app.config["HOST"], port=app.config["PORT"])
|
config = Config()
|
||||||
|
config.bind = [f"{HOST}:{PORT}"]
|
||||||
|
trio_asyncio.run(serve, app, config)
|
||||||
|
123
lnbits/app.py
123
lnbits/app.py
@@ -1,12 +1,15 @@
|
|||||||
|
import jinja2
|
||||||
|
from lnbits.jinja2_templating import Jinja2Templates
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
import importlib
|
import importlib
|
||||||
import traceback
|
import traceback
|
||||||
|
import trio
|
||||||
|
|
||||||
from quart import g
|
from fastapi import FastAPI
|
||||||
from quart_trio import QuartTrio
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from quart_cors import cors # type: ignore
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
from quart_compress import Compress # type: ignore
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from .commands import db_migrate, handle_assets
|
from .commands import db_migrate, handle_assets
|
||||||
from .core import core_app
|
from .core import core_app
|
||||||
@@ -26,32 +29,66 @@ from .tasks import (
|
|||||||
catch_everything_and_restart,
|
catch_everything_and_restart,
|
||||||
)
|
)
|
||||||
from .settings import WALLET
|
from .settings import WALLET
|
||||||
|
from .requestvars import g, request_global
|
||||||
|
import lnbits.settings
|
||||||
|
|
||||||
|
async def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||||
def create_app(config_object="lnbits.settings") -> QuartTrio:
|
|
||||||
"""Create application factory.
|
"""Create application factory.
|
||||||
:param config_object: The configuration object to use.
|
:param config_object: The configuration object to use.
|
||||||
"""
|
"""
|
||||||
app = QuartTrio(__name__, static_folder="static")
|
app = FastAPI()
|
||||||
app.config.from_object(config_object)
|
app.mount("/static", StaticFiles(directory="lnbits/static"), name="static")
|
||||||
app.asgi_http_class = ASGIProxyFix
|
|
||||||
|
|
||||||
cors(app)
|
origins = [
|
||||||
Compress(app)
|
"http://localhost",
|
||||||
|
"http://localhost:5000",
|
||||||
|
]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
g().config = lnbits.settings
|
||||||
|
g().templates = build_standard_jinja_templates()
|
||||||
|
|
||||||
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||||
|
# app.add_middleware(ASGIProxyFix)
|
||||||
|
|
||||||
check_funding_source(app)
|
check_funding_source(app)
|
||||||
register_assets(app)
|
register_assets(app)
|
||||||
register_blueprints(app)
|
register_routes(app)
|
||||||
register_filters(app)
|
# register_commands(app)
|
||||||
register_commands(app)
|
|
||||||
register_async_tasks(app)
|
register_async_tasks(app)
|
||||||
register_exception_handlers(app)
|
# register_exception_handlers(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
def build_standard_jinja_templates():
|
||||||
|
t = Jinja2Templates(
|
||||||
|
loader=jinja2.FileSystemLoader(["lnbits/templates", "lnbits/core/templates"]),
|
||||||
|
)
|
||||||
|
t.env.globals["SITE_TITLE"] = lnbits.settings.LNBITS_SITE_TITLE
|
||||||
|
t.env.globals["SITE_TAGLINE"] = lnbits.settings.LNBITS_SITE_TAGLINE
|
||||||
|
t.env.globals["SITE_DESCRIPTION"] = lnbits.settings.LNBITS_SITE_DESCRIPTION
|
||||||
|
t.env.globals["LNBITS_THEME_OPTIONS"] = lnbits.settings.LNBITS_THEME_OPTIONS
|
||||||
|
t.env.globals["LNBITS_VERSION"] = lnbits.settings.LNBITS_COMMIT
|
||||||
|
t.env.globals["EXTENSIONS"] = get_valid_extensions()
|
||||||
|
|
||||||
|
if g().config.DEBUG:
|
||||||
|
t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored())
|
||||||
|
t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored())
|
||||||
|
else:
|
||||||
|
t.env.globals["VENDORED_JS"] = ["/static/bundle.js"]
|
||||||
|
t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"]
|
||||||
|
|
||||||
def check_funding_source(app: QuartTrio) -> None:
|
return t
|
||||||
@app.before_serving
|
|
||||||
|
def check_funding_source(app: FastAPI) -> None:
|
||||||
|
@app.on_event("startup")
|
||||||
async def check_wallet_status():
|
async def check_wallet_status():
|
||||||
error_message, balance = await WALLET.status()
|
error_message, balance = await WALLET.status()
|
||||||
if error_message:
|
if error_message:
|
||||||
@@ -67,64 +104,60 @@ def check_funding_source(app: QuartTrio) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_blueprints(app: QuartTrio) -> None:
|
def register_routes(app: FastAPI) -> None:
|
||||||
"""Register Flask blueprints / LNbits extensions."""
|
"""Register Flask blueprints / LNbits extensions."""
|
||||||
app.register_blueprint(core_app)
|
app.include_router(core_app)
|
||||||
|
|
||||||
for ext in get_valid_extensions():
|
for ext in get_valid_extensions():
|
||||||
try:
|
try:
|
||||||
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
|
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
|
||||||
bp = getattr(ext_module, f"{ext.code}_ext")
|
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
||||||
|
|
||||||
app.register_blueprint(bp, url_prefix=f"/{ext.code}")
|
app.include_router(ext_route)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
f"Please make sure that the extension `{ext.code}` follows conventions."
|
f"Please make sure that the extension `{ext.code}` follows conventions."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_commands(app: QuartTrio):
|
def register_commands(app: FastAPI):
|
||||||
"""Register Click commands."""
|
"""Register Click commands."""
|
||||||
app.cli.add_command(db_migrate)
|
app.cli.add_command(db_migrate)
|
||||||
app.cli.add_command(handle_assets)
|
app.cli.add_command(handle_assets)
|
||||||
|
|
||||||
|
|
||||||
def register_assets(app: QuartTrio):
|
def register_assets(app: FastAPI):
|
||||||
"""Serve each vendored asset separately or a bundle."""
|
"""Serve each vendored asset separately or a bundle."""
|
||||||
|
|
||||||
@app.before_request
|
@app.on_event("startup")
|
||||||
async def vendored_assets_variable():
|
async def vendored_assets_variable():
|
||||||
if app.config["DEBUG"]:
|
if g().config.DEBUG:
|
||||||
g.VENDORED_JS = map(url_for_vendored, get_js_vendored())
|
g().VENDORED_JS = map(url_for_vendored, get_js_vendored())
|
||||||
g.VENDORED_CSS = map(url_for_vendored, get_css_vendored())
|
g().VENDORED_CSS = map(url_for_vendored, get_css_vendored())
|
||||||
else:
|
else:
|
||||||
g.VENDORED_JS = ["/static/bundle.js"]
|
g().VENDORED_JS = ["/static/bundle.js"]
|
||||||
g.VENDORED_CSS = ["/static/bundle.css"]
|
g().VENDORED_CSS = ["/static/bundle.css"]
|
||||||
|
|
||||||
|
|
||||||
def register_filters(app: QuartTrio):
|
|
||||||
"""Jinja filters."""
|
|
||||||
app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"]
|
|
||||||
app.jinja_env.globals["SITE_TAGLINE"] = app.config["LNBITS_SITE_TAGLINE"]
|
|
||||||
app.jinja_env.globals["SITE_DESCRIPTION"] = app.config["LNBITS_SITE_DESCRIPTION"]
|
|
||||||
app.jinja_env.globals["LNBITS_THEME_OPTIONS"] = app.config["LNBITS_THEME_OPTIONS"]
|
|
||||||
app.jinja_env.globals["LNBITS_VERSION"] = app.config["LNBITS_COMMIT"]
|
|
||||||
app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions()
|
|
||||||
|
|
||||||
|
|
||||||
def register_async_tasks(app):
|
def register_async_tasks(app):
|
||||||
@app.route("/wallet/webhook", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
|
@app.route("/wallet/webhook")
|
||||||
async def webhook_listener():
|
async def webhook_listener():
|
||||||
return await webhook_handler()
|
return await webhook_handler()
|
||||||
|
|
||||||
@app.before_serving
|
@app.on_event("startup")
|
||||||
async def listeners():
|
async def listeners():
|
||||||
run_deferred_async()
|
run_deferred_async()
|
||||||
app.nursery.start_soon(catch_everything_and_restart, check_pending_payments)
|
trio.open_process(check_pending_payments)
|
||||||
app.nursery.start_soon(catch_everything_and_restart, invoice_listener)
|
trio.open_process(invoice_listener)
|
||||||
app.nursery.start_soon(catch_everything_and_restart, internal_invoice_listener)
|
trio.open_process(internal_invoice_listener)
|
||||||
|
|
||||||
|
async with trio.open_nursery() as n:
|
||||||
|
pass
|
||||||
|
# n.start_soon(catch_everything_and_restart, check_pending_payments)
|
||||||
|
# n.start_soon(catch_everything_and_restart, invoice_listener)
|
||||||
|
# n.start_soon(catch_everything_and_restart, internal_invoice_listener)
|
||||||
|
|
||||||
@app.after_serving
|
@app.on_event("shutdown")
|
||||||
async def stop_listeners():
|
async def stop_listeners():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
49
lnbits/auth_bearer.py
Normal file
49
lnbits/auth_bearer.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from fastapi import Request, HTTPException
|
||||||
|
from fastapi.security.api_key import APIKeyQuery, APIKeyCookie, APIKeyHeader, APIKey
|
||||||
|
|
||||||
|
# https://medium.com/data-rebels/fastapi-authentication-revisited-enabling-api-key-authentication-122dc5975680
|
||||||
|
|
||||||
|
from fastapi import Security, Depends, FastAPI, HTTPException
|
||||||
|
from fastapi.security.api_key import APIKeyQuery, APIKeyCookie, APIKeyHeader, APIKey
|
||||||
|
from fastapi.security.base import SecurityBase
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
API_KEY = "usr"
|
||||||
|
API_KEY_NAME = "X-API-key"
|
||||||
|
|
||||||
|
api_key_query = APIKeyQuery(name=API_KEY_NAME, auto_error=False)
|
||||||
|
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AuthBearer(SecurityBase):
|
||||||
|
def __init__(self, scheme_name: str = None, auto_error: bool = True):
|
||||||
|
self.scheme_name = scheme_name or self.__class__.__name__
|
||||||
|
self.auto_error = auto_error
|
||||||
|
|
||||||
|
async def __call__(self, request: Request):
|
||||||
|
key = await self.get_api_key()
|
||||||
|
print(key)
|
||||||
|
# credentials: HTTPAuthorizationCredentials = await super(AuthBearer, self).__call__(request)
|
||||||
|
# if credentials:
|
||||||
|
# if not credentials.scheme == "Bearer":
|
||||||
|
# raise HTTPException(
|
||||||
|
# status_code=403, detail="Invalid authentication scheme.")
|
||||||
|
# if not self.verify_jwt(credentials.credentials):
|
||||||
|
# raise HTTPException(
|
||||||
|
# status_code=403, detail="Invalid token or expired token.")
|
||||||
|
# return credentials.credentials
|
||||||
|
# else:
|
||||||
|
# raise HTTPException(
|
||||||
|
# status_code=403, detail="Invalid authorization code.")
|
||||||
|
async def get_api_key(self,
|
||||||
|
api_key_query: str = Security(api_key_query),
|
||||||
|
api_key_header: str = Security(api_key_header),
|
||||||
|
):
|
||||||
|
if api_key_query == API_KEY:
|
||||||
|
return api_key_query
|
||||||
|
elif api_key_header == API_KEY:
|
||||||
|
return api_key_header
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=403, detail="Could not validate credentials")
|
@@ -1,22 +1,19 @@
|
|||||||
from quart import Blueprint
|
from fastapi.routing import APIRouter
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
|
|
||||||
db = Database("database")
|
db = Database("database")
|
||||||
|
|
||||||
core_app: Blueprint = Blueprint(
|
core_app: APIRouter = APIRouter()
|
||||||
"core",
|
|
||||||
__name__,
|
|
||||||
template_folder="templates",
|
|
||||||
static_folder="static",
|
|
||||||
static_url_path="/core/static",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
from .views.api import * # noqa
|
|
||||||
from .views.generic import * # noqa
|
|
||||||
from .views.public_api import * # noqa
|
|
||||||
from .tasks import register_listeners
|
|
||||||
|
|
||||||
from lnbits.tasks import record_async
|
from lnbits.tasks import record_async
|
||||||
|
|
||||||
core_app.record(record_async(register_listeners))
|
from .tasks import register_listeners
|
||||||
|
from .views.api import * # noqa
|
||||||
|
from .views.generic import * # noqa
|
||||||
|
from .views.public_api import * # noqa
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.on_event("startup")
|
||||||
|
def do_startup():
|
||||||
|
record_async(register_listeners)
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
|
from fastapi.param_functions import Depends
|
||||||
|
from lnbits.auth_bearer import AuthBearer
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import trio
|
import trio
|
||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
import hashlib
|
import hashlib
|
||||||
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
||||||
from quart import g, current_app, make_response, url_for
|
from quart import current_app, make_response, url_for
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ from typing import Dict, List, Optional, Union
|
|||||||
from lnbits import bolt11, lnurl
|
from lnbits import bolt11, lnurl
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
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.utils.exchange_rates import currencies, fiat_amount_as_satoshis
|
||||||
|
from lnbits.requestvars import g
|
||||||
|
|
||||||
from .. import core_app, db
|
from .. import core_app, db
|
||||||
from ..crud import get_payments, save_balance_check, update_wallet
|
from ..crud import get_payments, save_balance_check, update_wallet
|
||||||
@@ -28,11 +31,14 @@ from ..services import (
|
|||||||
from ..tasks import api_invoice_listeners
|
from ..tasks import api_invoice_listeners
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/api/v1/wallet")
|
@core_app.get(
|
||||||
@api_check_wallet_key("invoice")
|
"/api/v1/wallet",
|
||||||
|
# dependencies=[Depends(AuthBearer())]
|
||||||
|
)
|
||||||
|
# @api_check_wallet_key("invoice")
|
||||||
async def api_wallet():
|
async def api_wallet():
|
||||||
return (
|
return (
|
||||||
{"id": g.wallet.id, "name": g.wallet.name, "balance": g.wallet.balance_msat},
|
{"id": g().wallet.id, "name": g().wallet.name, "balance": g().wallet.balance_msat},
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,12 +46,12 @@ async def api_wallet():
|
|||||||
@core_app.put("/api/v1/wallet/<new_name>")
|
@core_app.put("/api/v1/wallet/<new_name>")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_update_wallet(new_name: str):
|
async def api_update_wallet(new_name: str):
|
||||||
await update_wallet(g.wallet.id, new_name)
|
await update_wallet(g().wallet.id, new_name)
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
"id": g.wallet.id,
|
"id": g().wallet.id,
|
||||||
"name": g.wallet.name,
|
"name": g().wallet.name,
|
||||||
"balance": g.wallet.balance_msat,
|
"balance": g().wallet.balance_msat,
|
||||||
},
|
},
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
@@ -55,7 +61,7 @@ async def api_update_wallet(new_name: str):
|
|||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_payments():
|
async def api_payments():
|
||||||
return (
|
return (
|
||||||
await get_payments(wallet_id=g.wallet.id, pending=True, complete=True),
|
await get_payments(wallet_id=g().wallet.id, pending=True, complete=True),
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,7 +94,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
|
|||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
try:
|
try:
|
||||||
payment_hash, payment_request = await create_invoice(
|
payment_hash, payment_request = await create_invoice(
|
||||||
wallet_id=g.wallet.id,
|
wallet_id=g().wallet.id,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
memo=memo,
|
memo=memo,
|
||||||
description_hash=description_hash,
|
description_hash=description_hash,
|
||||||
@@ -105,8 +111,8 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
|
|||||||
|
|
||||||
lnurl_response: Union[None, bool, str] = None
|
lnurl_response: Union[None, bool, str] = None
|
||||||
if data.lnurl_callback:
|
if data.lnurl_callback:
|
||||||
if "lnurl_balance_check" in g.data:
|
if "lnurl_balance_check" in g().data:
|
||||||
save_balance_check(g.wallet.id, data.lnurl_balance_check)
|
save_balance_check(g().wallet.id, data.lnurl_balance_check)
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
@@ -117,7 +123,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
|
|||||||
"balanceNotify": url_for(
|
"balanceNotify": url_for(
|
||||||
"core.lnurl_balance_notify",
|
"core.lnurl_balance_notify",
|
||||||
service=urlparse(data.lnurl_callback).netloc,
|
service=urlparse(data.lnurl_callback).netloc,
|
||||||
wal=g.wallet.id,
|
wal=g().wallet.id,
|
||||||
_external=True,
|
_external=True,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -217,14 +223,14 @@ async def api_payments_pay_lnurl(data: CreateLNURLData):
|
|||||||
if invoice.amount_msat != data.amount:
|
if invoice.amount_msat != data.amount:
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
"message": f"{domain} returned an invalid invoice. Expected {g.data['amount']} msat, got {invoice.amount_msat}."
|
"message": f"{domain} returned an invalid invoice. Expected {g().data['amount']} msat, got {invoice.amount_msat}."
|
||||||
},
|
},
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if invoice.description_hash != g.data["description_hash"]:
|
if invoice.description_hash != g().data["description_hash"]:
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
"message": f"{domain} returned an invalid invoice. Expected description_hash == {g.data['description_hash']}, got {invoice.description_hash}."
|
"message": f"{domain} returned an invalid invoice. Expected description_hash == {g().data['description_hash']}, got {invoice.description_hash}."
|
||||||
},
|
},
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
@@ -237,7 +243,7 @@ async def api_payments_pay_lnurl(data: CreateLNURLData):
|
|||||||
extra["comment"] = data.comment
|
extra["comment"] = data.comment
|
||||||
|
|
||||||
payment_hash = await pay_invoice(
|
payment_hash = await pay_invoice(
|
||||||
wallet_id=g.wallet.id,
|
wallet_id=g().wallet.id,
|
||||||
payment_request=params["pr"],
|
payment_request=params["pr"],
|
||||||
description=data.description,
|
description=data.description,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
@@ -257,7 +263,7 @@ async def api_payments_pay_lnurl(data: CreateLNURLData):
|
|||||||
@core_app.get("/api/v1/payments/<payment_hash>")
|
@core_app.get("/api/v1/payments/<payment_hash>")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_payment(payment_hash):
|
async def api_payment(payment_hash):
|
||||||
payment = await g.wallet.get_payment(payment_hash)
|
payment = await g().wallet.get_payment(payment_hash)
|
||||||
|
|
||||||
if not payment:
|
if not payment:
|
||||||
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND
|
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND
|
||||||
@@ -278,7 +284,7 @@ async def api_payment(payment_hash):
|
|||||||
@core_app.get("/api/v1/payments/sse")
|
@core_app.get("/api/v1/payments/sse")
|
||||||
@api_check_wallet_key("invoice", accept_querystring=True)
|
@api_check_wallet_key("invoice", accept_querystring=True)
|
||||||
async def api_payments_sse():
|
async def api_payments_sse():
|
||||||
this_wallet_id = g.wallet.id
|
this_wallet_id = g().wallet.id
|
||||||
|
|
||||||
send_payment, receive_payment = trio.open_memory_channel(0)
|
send_payment, receive_payment = trio.open_memory_channel(0)
|
||||||
|
|
||||||
@@ -356,7 +362,7 @@ async def api_lnurlscan(code: str):
|
|||||||
params.update(kind="auth")
|
params.update(kind="auth")
|
||||||
params.update(callback=url) # with k1 already in it
|
params.update(callback=url) # with k1 already in it
|
||||||
|
|
||||||
lnurlauth_key = g.wallet.lnurlauth_key(domain)
|
lnurlauth_key = g().wallet.lnurlauth_key(domain)
|
||||||
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
||||||
else:
|
else:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
@@ -1,15 +1,10 @@
|
|||||||
|
from lnbits.requestvars import g
|
||||||
from os import path
|
from os import path
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from quart import (
|
from typing import Optional
|
||||||
g,
|
import jinja2
|
||||||
current_app,
|
|
||||||
abort,
|
from starlette.responses import HTMLResponse
|
||||||
request,
|
|
||||||
redirect,
|
|
||||||
render_template,
|
|
||||||
send_from_directory,
|
|
||||||
url_for,
|
|
||||||
)
|
|
||||||
|
|
||||||
from lnbits.core import core_app, db
|
from lnbits.core import core_app, db
|
||||||
from lnbits.decorators import check_user_exists, validate_uuids
|
from lnbits.decorators import check_user_exists, validate_uuids
|
||||||
@@ -26,20 +21,18 @@ from ..crud import (
|
|||||||
)
|
)
|
||||||
from ..services import redeem_lnurl_withdraw, pay_invoice
|
from ..services import redeem_lnurl_withdraw, pay_invoice
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.responses import FileResponse
|
||||||
|
from lnbits.jinja2_templating import Jinja2Templates
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
|
|
||||||
@core_app.get("/favicon.ico")
|
@core_app.get("/favicon.ico")
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return await send_from_directory(
|
return FileResponse("lnbits/core/static/favicon.ico")
|
||||||
path.join(core_app.root_path, "static"), "favicon.ico"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@core_app.get("/", response_class=HTMLResponse)
|
||||||
@core_app.get("/")
|
|
||||||
async def home(request: Request, lightning: str = None):
|
async def home(request: Request, lightning: str = None):
|
||||||
return templates.TemplateResponse("core/index.html", {"request": request, "lnurl": lightning})
|
return g().templates.TemplateResponse("core/index.html", {"request": request, "lnurl": lightning})
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/extensions")
|
@core_app.get("/extensions")
|
||||||
|
@@ -7,7 +7,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||||
from lnbits.settings import LNBITS_ALLOWED_USERS
|
from lnbits.settings import LNBITS_ALLOWED_USERS
|
||||||
|
from lnbits.requestvars import g
|
||||||
|
|
||||||
def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False):
|
def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False):
|
||||||
def wrap(view):
|
def wrap(view):
|
||||||
@@ -15,14 +15,14 @@ def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False):
|
|||||||
async def wrapped_view(**kwargs):
|
async def wrapped_view(**kwargs):
|
||||||
try:
|
try:
|
||||||
key_value = request.headers.get("X-Api-Key") or request.args["api-key"]
|
key_value = request.headers.get("X-Api-Key") or request.args["api-key"]
|
||||||
g.wallet = await get_wallet_for_key(key_value, key_type)
|
g().wallet = await get_wallet_for_key(key_value, key_type)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return (
|
return (
|
||||||
jsonify({"message": "`X-Api-Key` header missing."}),
|
jsonify({"message": "`X-Api-Key` header missing."}),
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not g.wallet:
|
if not g().wallet:
|
||||||
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED
|
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED
|
||||||
|
|
||||||
return await view(**kwargs)
|
return await view(**kwargs)
|
||||||
@@ -44,9 +44,9 @@ def api_validate_post_request(*, schema: dict):
|
|||||||
|
|
||||||
v = Validator(schema)
|
v = Validator(schema)
|
||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
g.data = {key: data[key] for key in schema.keys() if key in data}
|
g().data = {key: data[key] for key in schema.keys() if key in data}
|
||||||
|
|
||||||
if not v.validate(g.data):
|
if not v.validate(g().data):
|
||||||
return (
|
return (
|
||||||
jsonify({"message": f"Errors in request data: {v.errors}"}),
|
jsonify({"message": f"Errors in request data: {v.errors}"}),
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
@@ -63,11 +63,11 @@ def check_user_exists(param: str = "usr"):
|
|||||||
def wrap(view):
|
def wrap(view):
|
||||||
@wraps(view)
|
@wraps(view)
|
||||||
async def wrapped_view(**kwargs):
|
async def wrapped_view(**kwargs):
|
||||||
g.user = await get_user(request.args.get(param, type=str)) or abort(
|
g().user = await get_user(request.args.get(param, type=str)) or abort(
|
||||||
HTTPStatus.NOT_FOUND, "User does not exist."
|
HTTPStatus.NOT_FOUND, "User does not exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
if LNBITS_ALLOWED_USERS and g.user.id not in LNBITS_ALLOWED_USERS:
|
if LNBITS_ALLOWED_USERS and g().user.id not in LNBITS_ALLOWED_USERS:
|
||||||
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
|
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
|
||||||
|
|
||||||
return await view(**kwargs)
|
return await view(**kwargs)
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
from quart import Blueprint
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
|
|
||||||
db = Database("ext_offlineshop")
|
db = Database("ext_offlineshop")
|
||||||
|
|
||||||
offlineshop_ext: Blueprint = Blueprint(
|
offlineshop_ext: APIRouter = APIRouter(
|
||||||
"offlineshop", __name__, static_folder="static", template_folder="templates"
|
prefix="/Extension",
|
||||||
|
tags=["Apps", "Offlineshop"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
36
lnbits/jinja2_templating.py
Normal file
36
lnbits/jinja2_templating.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Borrowed from the excellent accent-starlette
|
||||||
|
# https://github.com/accent-starlette/starlette-core/blob/master/starlette_core/templating.py
|
||||||
|
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from starlette import templating
|
||||||
|
from starlette.datastructures import QueryParams
|
||||||
|
|
||||||
|
from lnbits.requestvars import g
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jinja2
|
||||||
|
except ImportError: # pragma: nocover
|
||||||
|
jinja2 = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Jinja2Templates(templating.Jinja2Templates):
|
||||||
|
def __init__(self, loader: jinja2.BaseLoader) -> None:
|
||||||
|
assert jinja2 is not None, "jinja2 must be installed to use Jinja2Templates"
|
||||||
|
self.env = self.get_environment(loader)
|
||||||
|
|
||||||
|
def get_environment(self, loader: "jinja2.BaseLoader") -> "jinja2.Environment":
|
||||||
|
@jinja2.contextfunction
|
||||||
|
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
|
||||||
|
request = context["request"]
|
||||||
|
return request.url_for(name, **path_params)
|
||||||
|
|
||||||
|
def url_params_update(init: QueryParams, **new: typing.Any) -> QueryParams:
|
||||||
|
values = dict(init)
|
||||||
|
values.update(new)
|
||||||
|
return QueryParams(**values)
|
||||||
|
|
||||||
|
env = jinja2.Environment(loader=loader, autoescape=True)
|
||||||
|
env.globals["url_for"] = url_for
|
||||||
|
env.globals["url_params_update"] = url_params_update
|
||||||
|
return env
|
9
lnbits/requestvars.py
Normal file
9
lnbits/requestvars.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import contextvars
|
||||||
|
import types
|
||||||
|
|
||||||
|
request_global = contextvars.ContextVar("request_global",
|
||||||
|
default=types.SimpleNamespace())
|
||||||
|
|
||||||
|
|
||||||
|
def g() -> types.SimpleNamespace:
|
||||||
|
return request_global.get()
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
{% for url in g.VENDORED_CSS %}
|
{% for url in VENDORED_CSS %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url }}" />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<!---->
|
<!---->
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
|
|
||||||
{% block vue_templates %}{% endblock %}
|
{% block vue_templates %}{% endblock %}
|
||||||
<!---->
|
<!---->
|
||||||
{% for url in g.VENDORED_JS %}
|
{% for url in VENDORED_JS %}
|
||||||
<script src="{{ url }}"></script>
|
<script src="{{ url }}"></script>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<!---->
|
<!---->
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
{% for url in g.VENDORED_CSS %}
|
{% for url in VENDORED_CSS %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url }}" />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<style>
|
<style>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</q-page-container>
|
</q-page-container>
|
||||||
</q-layout>
|
</q-layout>
|
||||||
|
|
||||||
{% for url in g.VENDORED_JS %}
|
{% for url in VENDORED_JS %}
|
||||||
<script src="{{ url }}"></script>
|
<script src="{{ url }}"></script>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<!---->
|
<!---->
|
||||||
|
Reference in New Issue
Block a user