lnbits-legend/lnbits/core/views/api.py

1015 lines
33 KiB
Python
Raw Normal View History

import asyncio
2021-07-30 21:01:19 -03:00
import hashlib
2021-08-29 19:38:42 +02:00
import json
import uuid
2021-08-29 19:38:42 +02:00
from http import HTTPStatus
2022-07-28 11:02:49 +01:00
from io import BytesIO
from math import ceil
from typing import Dict, List, Optional, Union
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
2021-10-29 13:06:59 +01:00
2021-08-29 19:38:42 +02:00
import httpx
import pyqrcode
2022-12-01 14:41:57 +00:00
from fastapi import (
APIRouter,
Body,
2022-12-01 14:41:57 +00:00
Depends,
Header,
Request,
WebSocket,
WebSocketDisconnect,
)
2021-08-29 19:38:42 +02:00
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse
2022-07-16 14:23:03 +02:00
from loguru import logger
from sse_starlette.sse import EventSourceResponse
from starlette.responses import StreamingResponse
from lnbits import bolt11
from lnbits.core.db import core_app_extra, db
2023-02-15 17:25:58 +02:00
from lnbits.core.helpers import (
migrate_extension_database,
stop_extension_background_work,
)
from lnbits.core.models import (
BaseWallet,
ConversionData,
CreateInvoice,
CreateLnurl,
2023-08-24 11:52:12 +02:00
CreateLnurlAuth,
CreateWallet,
DecodePayment,
Payment,
PaymentFilters,
PaymentHistoryPoint,
Query,
User,
Wallet,
WalletType,
)
from lnbits.db import Filters, Page
from lnbits.decorators import (
WalletTypeInfo,
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092) no more superuser url! delete cookie on logout add usr login feature fix node management * Cleaned up login form * CreateUser * information leak * cleaner parsing usr from url * rename decorators * login secret * fix: add back `superuser` command * chore: remove `fastapi_login` * fix: extract `token` from cookie * chore: prepare to extract user * feat: check user * chore: code clean-up * feat: happy flow working * fix: usr only login * fix: user already logged in * feat: check user in URL * fix: verify password at DB level * fix: do not show `Login` controls if user already logged in * fix: separate login endpoints * fix: remove `usr` param * chore: update error message * refactor: register method * feat: logout * chore: move comments * fix: remove user auth check from API * fix: user check unnecessary * fix: redirect after logout * chore: remove garbage files * refactor: simplify constructor call * fix: hide user icon if not authorized * refactor: rename auth env vars * chore: code clean-up * fix: add types for `python-jose` * fix: add types for `passlib` * fix: return type * feat: set default value for `auth_secret_key` to hash of super user * fix: default value * feat: rework login page * feat: ui polishing * feat: google auth * feat: add google auth * chore: remove `authlib` dependency * refactor: extract `_handle_sso_login` method * refactor: convert methods to `properties` * refactor: rename: `user_api` to `auth_api` * feat: store user info from SSO * chore: re-arange the buttons * feat: conditional rendering of login options * feat: correctly render buttons * fix: re-add `Claim Bitcoin` from the main page * fix: create wallet must send new user * fix: no `username-password` auth method * refactor: rename auth method * fix: do not force API level UUID4 validation * feat: add validation for username * feat: add account page * feat: update account * feat: add `has_password` for user * fix: email not editable * feat: validate email for existing account * fix: register check * feat: reset password * chore: code clean-up * feat: handle token expired * fix: only redirect if `text/html` * refactor: remove `OAuth2PasswordRequestForm` * chore: remove `python-multipart` dependency * fix: handle no headers for exception * feat: add back button on error screen * feat: show user profile image * fix: check account creation permissions * fix: auth for internal api call * chore: add some docs * chore: code clean-up * fix: rebase stuff * fix: default value types * refactor: customize error messages * fix: move types libs to dev dependencies * doc: specify the `Authorization callback URL` * fix: pass missing superuser id in node ui test * fix: keep usr param on wallet redirect removing usr param causes an issue if the browser doesnt yet have an access token. * fix: do not redirect if `wal` query param not present * fix: add nativeBuildInputs and buildInputs overrides to flake.nix * bump fastapi-sso to 0.9.0 which fixes some security issues * refactor: move the `lnbits_admin_extensions` to decorators * chore: bring package config from `dev` * chore: re-add dependencies * chore: re-add cev dependencies * chore: re-add mypy ignores * feat: i18n * refactor: move admin ext check to decorator (fix after rebase) * fix: label mapping * fix: re-fetch user after first wallet was created * fix: unlikely case that `user` is not found * refactor translations (move '*' to code) * reorganize deps in pyproject.toml, add comment * update flake.lock and simplify flake.nix after upstreaming overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463 * fix: more relaxed email verification (by @prusnak) * fix: remove `\b` (boundaries) since we re using `fullmatch` * chore: `make bundle` --------- Co-authored-by: dni ⚡ <office@dnilabs.com> Co-authored-by: Arc <ben@arc.wales> Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 12:38:19 +02:00
check_access_token,
check_admin,
check_user_exists,
get_key_type,
parse_filters,
require_admin_key,
require_invoice_key,
)
2023-01-20 10:06:32 +02:00
from lnbits.extension_manager import (
2023-01-17 11:16:54 +02:00
CreateExtension,
2023-01-11 11:25:18 +02:00
Extension,
ExtensionRelease,
2023-01-11 11:25:18 +02:00
InstallableExtension,
fetch_github_release_config,
fetch_release_payment_info,
2023-01-11 11:25:18 +02:00
get_valid_extensions,
)
from lnbits.helpers import generate_filter_params_openapi, url_for
from lnbits.lnurl import decode as lnurl_decode
from lnbits.settings import settings
from lnbits.utils.exchange_rates import (
allowed_currencies,
fiat_amount_as_satoshis,
satoshis_amount_as_fiat,
)
from ..crud import (
DateTrunc,
add_installed_extension,
create_account,
create_wallet,
delete_dbversion,
2023-01-25 09:56:05 +02:00
delete_installed_extension,
delete_wallet,
drop_extension_db,
2023-01-25 09:56:05 +02:00
get_dbversions,
get_installed_extension,
get_payments,
get_payments_history,
get_payments_paginated,
get_standalone_payment,
get_wallet_for_key,
save_balance_check,
update_pending_payments,
update_wallet,
)
from ..services import (
InvoiceFailure,
PaymentFailure,
check_transaction_status,
create_invoice,
fee_reserve_total,
pay_invoice,
perform_lnurlauth,
websocketManager,
websocketUpdater,
)
from ..tasks import api_invoice_listeners
api_router = APIRouter()
@api_router.get("/api/v1/health", status_code=HTTPStatus.OK)
2022-12-25 18:49:51 +01:00
async def health():
return
@api_router.get("/api/v1/wallet")
2021-08-29 19:38:42 +02:00
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
if wallet.wallet_type == WalletType.admin:
2021-11-09 17:44:05 +00:00
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat,
}
else:
2021-11-12 04:14:55 +00:00
return {"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat}
2021-10-17 18:33:29 +01:00
2021-08-16 19:27:39 +01:00
@api_router.get(
"/api/v1/wallets",
name="Wallets",
description="Get basic info for all of user's wallets.",
)
async def api_wallets(user: User = Depends(check_user_exists)) -> List[BaseWallet]:
return [BaseWallet(**w.dict()) for w in user.wallets]
@api_router.put("/api/v1/wallet/{new_name}")
async def api_update_wallet_name(
2022-05-16 11:21:30 +01:00
new_name: str, wallet: WalletTypeInfo = Depends(require_admin_key)
2022-02-16 22:42:27 +01:00
):
2021-08-29 19:38:42 +02:00
await update_wallet(wallet.wallet.id, new_name)
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat,
}
2021-08-06 11:15:07 +01:00
@api_router.patch("/api/v1/wallet", response_model=Wallet)
async def api_update_wallet(
name: Optional[str] = Body(None),
currency: Optional[str] = Body(None),
wallet: WalletTypeInfo = Depends(require_admin_key),
):
return await update_wallet(wallet.wallet.id, name, currency)
@api_router.delete("/api/v1/wallet")
async def api_delete_wallet(
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> None:
await delete_wallet(
user_id=wallet.wallet.user,
wallet_id=wallet.wallet.id,
)
@api_router.post("/api/v1/wallet", response_model=Wallet)
async def api_create_wallet(
data: CreateWallet,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Wallet:
return await create_wallet(user_id=wallet.wallet.user, wallet_name=data.name)
@api_router.post("/api/v1/account", response_model=Wallet)
async def api_create_account(data: CreateWallet) -> Wallet:
if not settings.new_accounts_allowed:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Account creation is disabled.",
)
account = await create_account()
return await create_wallet(user_id=account.id, wallet_name=data.name)
@api_router.get(
"/api/v1/payments",
name="Payment List",
summary="get list of payments",
response_description="list of payments",
response_model=List[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments(
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
await update_pending_payments(wallet.wallet.id)
return await get_payments(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
filters=filters,
)
2021-10-17 18:33:29 +01:00
@api_router.get(
"/api/v1/payments/history",
name="Get payments history",
response_model=List[PaymentHistoryPoint],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_history(
wallet: WalletTypeInfo = Depends(get_key_type),
group: DateTrunc = Query("day"),
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
):
await update_pending_payments(wallet.wallet.id)
return await get_payments_history(wallet.wallet.id, group, filters)
@api_router.get(
"/api/v1/payments/paginated",
name="Payment List",
summary="get paginated list of payments",
response_description="list of payments",
response_model=Page[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_paginated(
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
await update_pending_payments(wallet.wallet.id)
page = await get_payments_paginated(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
filters=filters,
)
return page
async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
description_hash = b""
unhashed_description = b""
memo = data.memo or settings.lnbits_site_title
if data.description_hash or data.unhashed_description:
if data.description_hash:
try:
description_hash = bytes.fromhex(data.description_hash)
except ValueError:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'description_hash' must be a valid hex string",
)
if data.unhashed_description:
try:
unhashed_description = bytes.fromhex(data.unhashed_description)
except ValueError:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'unhashed_description' must be a valid hex string",
)
# do not save memo if description_hash or unhashed_description is set
memo = ""
async with db.connect() as conn:
try:
Wallets: add cln-rest (#1775) * receive and pay works * fix linter issues * import Paymentstatus from core.models * fix test real payment * fix get_payment_status check in lnbits * fix tests? * simplify * refactor AsyncClient * inline import of get_wallet_class fixes the previous cyclic import * invoice stream working * add notes as a reminder to get rid of labels when cln-rest supports payment_hash * create Payment dummy classmethod * remove unnecessary fields from dummy * fixes tests? * fix model * fix cln bug (#1814) * auth header * rename cln to corelightning * add clnrest to admin_ui * add to clnrest allowed sources * add allowed sources to .env.example * allow macaroon files * add corelightning rest to workflow * proper env names * cleanup routine * log wallet connection errors and fix macaroon clnrest * print error on connection fails * clnrest: handle disconnects faster * fix test use of get_payment_status * make format * clnrest: add unhashed_description * add unhashed_description to test * description_hash test * unhashed_description not supported by clnrest * fix checking_id return in api_payments_create_invoice * refactor test to use client instead of api_payments * formatting, some errorlogging * fix test 1 * fix other tests, paid statuses was missing * error handling * revert unnecessary changes (#1854) * apply review of motorina0 --------- Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-08-23 08:59:39 +02:00
payment_hash, payment_request = await create_invoice(
2021-08-29 19:38:42 +02:00
wallet_id=wallet.id,
amount=data.amount,
memo=memo,
currency=data.unit,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=data.expiry,
2021-08-20 21:31:01 +01:00
extra=data.extra,
webhook=data.webhook,
internal=data.internal,
conn=conn,
)
# NOTE: we get the checking_id with a seperate query because create_invoice
# does not return it and it would be a big hustle to change its return type
# (used across extensions)
Wallets: add cln-rest (#1775) * receive and pay works * fix linter issues * import Paymentstatus from core.models * fix test real payment * fix get_payment_status check in lnbits * fix tests? * simplify * refactor AsyncClient * inline import of get_wallet_class fixes the previous cyclic import * invoice stream working * add notes as a reminder to get rid of labels when cln-rest supports payment_hash * create Payment dummy classmethod * remove unnecessary fields from dummy * fixes tests? * fix model * fix cln bug (#1814) * auth header * rename cln to corelightning * add clnrest to admin_ui * add to clnrest allowed sources * add allowed sources to .env.example * allow macaroon files * add corelightning rest to workflow * proper env names * cleanup routine * log wallet connection errors and fix macaroon clnrest * print error on connection fails * clnrest: handle disconnects faster * fix test use of get_payment_status * make format * clnrest: add unhashed_description * add unhashed_description to test * description_hash test * unhashed_description not supported by clnrest * fix checking_id return in api_payments_create_invoice * refactor test to use client instead of api_payments * formatting, some errorlogging * fix test 1 * fix other tests, paid statuses was missing * error handling * revert unnecessary changes (#1854) * apply review of motorina0 --------- Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-08-23 08:59:39 +02:00
payment_db = await get_standalone_payment(payment_hash, conn=conn)
assert payment_db is not None, "payment not found"
checking_id = payment_db.checking_id
except InvoiceFailure as e:
2021-10-17 18:33:29 +01:00
raise HTTPException(status_code=520, detail=str(e))
except Exception as exc:
raise exc
invoice = bolt11.decode(payment_request)
lnurl_response: Union[None, bool, str] = None
2021-08-20 21:31:01 +01:00
if data.lnurl_callback:
if data.lnurl_balance_check is not None:
2022-07-19 18:51:35 +02:00
await save_balance_check(wallet.id, data.lnurl_balance_check)
2021-04-17 18:27:15 -03:00
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
2020-12-31 18:50:16 +01:00
r = await client.get(
2021-08-20 21:31:01 +01:00
data.lnurl_callback,
2021-04-17 18:27:15 -03:00
params={
"pr": payment_request,
"balanceNotify": url_for(
f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
external=True,
2022-02-01 23:11:26 +00:00
wal=wallet.id,
2021-04-17 18:27:15 -03:00
),
},
2020-12-31 18:50:16 +01:00
timeout=10,
)
if r.is_error:
lnurl_response = r.text
else:
resp = json.loads(r.text)
if resp["status"] != "OK":
lnurl_response = resp["reason"]
else:
lnurl_response = True
2022-12-21 14:56:27 +02:00
except (httpx.ConnectError, httpx.RequestError) as ex:
logger.error(ex)
lnurl_response = False
return {
"payment_hash": invoice.payment_hash,
"payment_request": payment_request,
# maintain backwards compatibility with API clients:
Wallets: add cln-rest (#1775) * receive and pay works * fix linter issues * import Paymentstatus from core.models * fix test real payment * fix get_payment_status check in lnbits * fix tests? * simplify * refactor AsyncClient * inline import of get_wallet_class fixes the previous cyclic import * invoice stream working * add notes as a reminder to get rid of labels when cln-rest supports payment_hash * create Payment dummy classmethod * remove unnecessary fields from dummy * fixes tests? * fix model * fix cln bug (#1814) * auth header * rename cln to corelightning * add clnrest to admin_ui * add to clnrest allowed sources * add allowed sources to .env.example * allow macaroon files * add corelightning rest to workflow * proper env names * cleanup routine * log wallet connection errors and fix macaroon clnrest * print error on connection fails * clnrest: handle disconnects faster * fix test use of get_payment_status * make format * clnrest: add unhashed_description * add unhashed_description to test * description_hash test * unhashed_description not supported by clnrest * fix checking_id return in api_payments_create_invoice * refactor test to use client instead of api_payments * formatting, some errorlogging * fix test 1 * fix other tests, paid statuses was missing * error handling * revert unnecessary changes (#1854) * apply review of motorina0 --------- Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-08-23 08:59:39 +02:00
"checking_id": checking_id,
"lnurl_response": lnurl_response,
}
async def api_payments_pay_invoice(
bolt11: str, wallet: Wallet, extra: Optional[dict] = None
):
try:
payment_hash = await pay_invoice(
wallet_id=wallet.id, payment_request=bolt11, extra=extra
)
2020-04-16 17:10:53 +02:00
except ValueError as e:
2021-10-17 18:33:29 +01:00
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
2020-04-16 17:10:53 +02:00
except PermissionError as e:
2021-10-17 18:33:29 +01:00
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail=str(e))
except PaymentFailure as e:
2021-10-17 18:33:29 +01:00
raise HTTPException(status_code=520, detail=str(e))
except Exception as exc:
raise exc
return {
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
@api_router.post(
2021-10-17 18:33:29 +01:00
"/api/v1/payments",
summary="Create or pay an invoice",
description="""
This endpoint can be used both to generate and pay a BOLT11 invoice.
To generate a new invoice for receiving funds into the authorized account,
specify at least the first four fields in the POST body: `out: false`,
`amount`, `unit`, and `memo`. To pay an arbitrary invoice from the funds
already in the authorized account, specify `out: true` and use the `bolt11`
field to supply the BOLT11 invoice to be paid.
""",
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.CREATED,
)
async def api_payments_create(
wallet: WalletTypeInfo = Depends(require_invoice_key),
invoiceData: CreateInvoice = Body(...),
2021-10-17 18:33:29 +01:00
):
if invoiceData.out is True and wallet.wallet_type == WalletType.admin:
2021-09-19 09:31:16 +02:00
if not invoiceData.bolt11:
2021-10-17 18:33:29 +01:00
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="BOLT11 string is invalid or not given",
)
2022-02-16 22:42:27 +01:00
return await api_payments_pay_invoice(
invoiceData.bolt11, wallet.wallet, invoiceData.extra
2022-02-16 22:42:27 +01:00
) # admin key
elif not invoiceData.out:
# invoice key
return await api_payments_create_invoice(invoiceData, wallet.wallet)
else:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invoice (or Admin) key required.",
)
2021-10-17 18:33:29 +01:00
@api_router.get("/api/v1/payments/fee-reserve")
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
invoice_obj = bolt11.decode(invoice)
if invoice_obj.amount_msat:
response = {
"fee_reserve": fee_reserve_total(invoice_obj.amount_msat),
}
return JSONResponse(response)
else:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Invoice has no amount.",
)
@api_router.post("/api/v1/payments/lnurl")
2022-02-16 22:42:27 +01:00
async def api_payments_pay_lnurl(
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
2022-02-16 22:42:27 +01:00
):
2021-08-20 21:31:01 +01:00
domain = urlparse(data.callback).netloc
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
try:
if data.unit and data.unit != "sat":
amount_msat = await fiat_amount_as_satoshis(data.amount, data.unit)
# no msat precision
amount_msat = ceil(amount_msat // 1000) * 1000
else:
amount_msat = data.amount
r = await client.get(
2021-08-20 21:31:01 +01:00
data.callback,
params={"amount": amount_msat, "comment": data.comment},
2020-12-31 18:50:16 +01:00
timeout=40,
)
if r.is_error:
raise httpx.ConnectError("LNURL callback connection error")
r.raise_for_status()
except (httpx.ConnectError, httpx.RequestError):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
2021-10-17 18:33:29 +01:00
detail=f"Failed to connect to {domain}.",
2021-07-30 21:01:19 -03:00
)
params = json.loads(r.text)
if params.get("status") == "ERROR":
raise HTTPException(
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} said: '{params.get('reason', '')}'",
)
if not params.get("pr"):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} did not return a payment request.",
)
invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != amount_msat:
raise HTTPException(
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.BAD_REQUEST,
detail=(
(
f"{domain} returned an invalid invoice. Expected"
f" {amount_msat} msat, got {invoice.amount_msat}."
),
),
)
2021-10-17 18:33:29 +01:00
extra = {}
if params.get("successAction"):
extra["success_action"] = params["successAction"]
2021-08-20 21:31:01 +01:00
if data.comment:
extra["comment"] = data.comment
if data.unit and data.unit != "sat":
extra["fiat_currency"] = data.unit
extra["fiat_amount"] = data.amount / 1000
assert data.description is not None, "description is required"
payment_hash = await pay_invoice(
2021-09-19 13:25:39 +02:00
wallet_id=wallet.wallet.id,
payment_request=params["pr"],
2021-08-20 21:31:01 +01:00
description=data.description,
extra=extra,
)
return {
"success_action": params.get("successAction"),
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
2021-10-17 18:33:29 +01:00
async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
"""
Subscribe to new invoices for a wallet. Can be wrapped in EventSourceResponse.
Listenes invoming payments for a wallet and yields jsons with payment details.
"""
this_wallet_id = wallet.id
payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0)
uid = f"{this_wallet_id}_{str(uuid.uuid4())[:8]}"
logger.debug(f"adding sse listener for wallet: {uid}")
api_invoice_listeners[uid] = payment_queue
try:
while True:
if await request.is_disconnected():
await request.close()
break
payment: Payment = await payment_queue.get()
if payment.wallet_id == this_wallet_id:
logger.debug("sse listener: payment received", payment)
yield dict(data=payment.json(), event="payment-received")
except asyncio.CancelledError:
2022-12-16 17:18:02 +01:00
logger.debug(f"removing listener for wallet {uid}")
2023-09-27 11:25:42 +02:00
except Exception as exc:
logger.error(f"Error in sse: {exc}")
finally:
api_invoice_listeners.pop(uid)
@api_router.get("/api/v1/payments/sse")
2022-02-16 22:42:27 +01:00
async def api_payments_sse(
request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
):
return EventSourceResponse(
subscribe_wallet_invoices(request, wallet.wallet),
ping=20,
media_type="text/event-stream",
2022-02-16 22:42:27 +01:00
)
# TODO: refactor this route into a public and admin one
@api_router.get("/api/v1/payments/{payment_hash}")
2022-03-07 08:08:08 +00:00
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
# We use X_Api_Key here because we want this call to work with and without keys
# If a valid key is given, we also return the field "details", otherwise not
wallet = await get_wallet_for_key(X_Api_Key) if isinstance(X_Api_Key, str) else None
payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
2022-07-20 11:21:38 +02:00
)
if payment is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
await check_transaction_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
)
2021-08-29 19:38:42 +02:00
if not payment:
2022-02-16 22:42:27 +01:00
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
2021-08-29 19:38:42 +02:00
elif not payment.pending:
2022-03-07 08:08:08 +00:00
if wallet and wallet.id == payment.wallet_id:
return {"paid": True, "preimage": payment.preimage, "details": payment}
2021-09-11 11:02:48 +02:00
return {"paid": True, "preimage": payment.preimage}
2021-08-29 19:38:42 +02:00
try:
await payment.check_status()
2021-08-29 19:38:42 +02:00
except Exception:
2022-03-07 08:08:08 +00:00
if wallet and wallet.id == payment.wallet_id:
return {"paid": False, "details": payment}
2021-09-11 11:02:48 +02:00
return {"paid": False}
2021-08-29 19:38:42 +02:00
2022-03-07 08:08:08 +00:00
if wallet and wallet.id == payment.wallet_id:
2022-06-01 14:53:05 +02:00
return {
"paid": not payment.pending,
"preimage": payment.preimage,
"details": payment,
}
return {"paid": not payment.pending, "preimage": payment.preimage}
2021-10-17 18:33:29 +01:00
@api_router.get("/api/v1/lnurlscan/{code}")
async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type)):
try:
url = str(lnurl_decode(code))
2021-07-30 21:01:19 -03:00
domain = urlparse(url).netloc
except Exception:
2021-07-30 21:01:19 -03:00
# parse internet identifier (user@domain.com)
name_domain = code.split("@")
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
2021-07-30 21:01:19 -03:00
name, domain = name_domain
2022-02-16 22:42:27 +01:00
url = (
("http://" if domain.endswith(".onion") else "https://")
+ domain
+ "/.well-known/lnurlp/"
+ name
)
2021-07-30 21:01:19 -03:00
# will proceed with these values
else:
2022-02-16 22:42:27 +01:00
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="invalid lnurl"
)
# params is what will be returned to the client
params: Dict = {"domain": domain}
2021-07-30 21:01:19 -03:00
if "tag=login" in url:
params.update(kind="auth")
2021-07-30 21:01:19 -03:00
params.update(callback=url) # with k1 already in it
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
assert lnurlauth_key.verifying_key
2022-01-30 19:43:30 +00:00
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
2021-07-30 21:01:19 -03:00
r = await client.get(url, timeout=5)
r.raise_for_status()
if r.is_error:
raise HTTPException(
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
2022-01-30 19:43:30 +00:00
detail={"domain": domain, "message": "failed to get parameters"},
)
try:
2021-07-30 21:01:19 -03:00
data = json.loads(r.text)
except json.decoder.JSONDecodeError:
raise HTTPException(
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={
"domain": domain,
"message": f"got invalid response '{r.text[:200]}'",
},
)
2021-07-30 21:01:19 -03:00
try:
tag: str = data.get("tag")
params.update(**data)
2021-07-30 21:01:19 -03:00
if tag == "channelRequest":
raise HTTPException(
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.BAD_REQUEST,
detail={
"domain": domain,
"kind": "channel",
"message": "unsupported",
},
2021-07-30 21:01:19 -03:00
)
elif tag == "withdrawRequest":
2021-07-30 21:01:19 -03:00
params.update(kind="withdraw")
2022-01-30 19:43:30 +00:00
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
2021-07-30 21:01:19 -03:00
# callback with k1 already in it
parsed_callback: ParseResult = urlparse(data["callback"])
qs: Dict = parse_qs(parsed_callback.query)
qs["k1"] = data["k1"]
# balanceCheck/balanceNotify
if "balanceCheck" in data:
params.update(balanceCheck=data["balanceCheck"])
# format callback url and send to client
2022-02-16 22:42:27 +01:00
parsed_callback = parsed_callback._replace(
query=urlencode(qs, doseq=True)
)
2021-07-30 21:01:19 -03:00
params.update(callback=urlunparse(parsed_callback))
elif tag == "payRequest":
2021-07-30 21:01:19 -03:00
params.update(kind="pay")
params.update(fixed=data["minSendable"] == data["maxSendable"])
2022-02-16 22:42:27 +01:00
params.update(
description_hash=hashlib.sha256(
data["metadata"].encode()
2022-02-16 22:42:27 +01:00
).hexdigest()
)
2021-07-30 21:01:19 -03:00
metadata = json.loads(data["metadata"])
for [k, v] in metadata:
if k == "text/plain":
params.update(description=v)
2023-01-21 12:11:45 +00:00
if k in ("image/jpeg;base64", "image/png;base64"):
data_uri = f"data:{k},{v}"
2021-07-30 21:01:19 -03:00
params.update(image=data_uri)
2023-01-21 12:11:45 +00:00
if k in ("text/email", "text/identifier"):
2021-07-30 21:01:19 -03:00
params.update(targetUser=v)
params.update(commentAllowed=data.get("commentAllowed", 0))
2021-07-30 21:01:19 -03:00
except KeyError as exc:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={
"domain": domain,
"message": f"lnurl JSON response invalid: {exc}",
2021-10-17 18:33:29 +01:00
},
)
2021-08-20 21:31:01 +01:00
return params
@api_router.post("/api/v1/payments/decode", status_code=HTTPStatus.OK)
async def api_payments_decode(data: DecodePayment) -> JSONResponse:
2022-03-24 12:50:57 +01:00
payment_str = data.data
try:
2022-03-24 12:50:57 +01:00
if payment_str[:5] == "LNURL":
url = str(lnurl_decode(payment_str))
return JSONResponse({"domain": url})
else:
2022-03-24 12:50:57 +01:00
invoice = bolt11.decode(payment_str)
return JSONResponse(invoice.data)
except Exception as exc:
return JSONResponse(
{"message": f"Failed to decode: {str(exc)}"},
status_code=HTTPStatus.BAD_REQUEST,
)
@api_router.post("/api/v1/lnurlauth")
async def api_perform_lnurlauth(
2023-08-24 11:52:12 +02:00
data: CreateLnurlAuth, wallet: WalletTypeInfo = Depends(require_admin_key)
):
err = await perform_lnurlauth(data.callback, wallet=wallet)
2020-11-10 23:01:55 -03:00
if err:
2022-02-16 22:42:27 +01:00
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
)
return ""
@api_router.get("/api/v1/currencies")
async def api_list_currencies_available() -> List[str]:
return allowed_currencies()
@api_router.post("/api/v1/conversion")
async def api_fiat_as_sats(data: ConversionData):
2022-01-14 12:19:30 +00:00
output = {}
2022-01-30 19:43:30 +00:00
if data.from_ == "sat":
2022-01-14 12:19:30 +00:00
output["BTC"] = data.amount / 100000000
2022-07-26 12:46:43 +02:00
output["sats"] = int(data.amount)
2022-01-30 19:43:30 +00:00
for currency in data.to.split(","):
2022-02-16 22:42:27 +01:00
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
data.amount, currency.strip()
)
2022-01-14 12:19:30 +00:00
return output
else:
output[data.from_.upper()] = data.amount
2022-03-10 12:16:51 +00:00
output["sats"] = await fiat_amount_as_satoshis(data.amount, data.from_)
2022-01-14 12:19:30 +00:00
output["BTC"] = output["sats"] / 100000000
return output
2022-07-28 11:02:49 +01:00
@api_router.get("/api/v1/qrcode/{data}", response_class=StreamingResponse)
async def img(data):
qr = pyqrcode.create(data)
stream = BytesIO()
qr.svg(stream, scale=3)
stream.seek(0)
async def _generator(stream: BytesIO):
yield stream.getvalue()
return StreamingResponse(
_generator(stream),
headers={
"Content-Type": "image/svg+xml",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
},
)
@api_router.websocket("/api/v1/ws/{item_id}")
2022-11-23 22:22:33 +00:00
async def websocket_connect(websocket: WebSocket, item_id: str):
await websocketManager.connect(websocket, item_id)
try:
while True:
await websocket.receive_text()
except WebSocketDisconnect:
2022-11-23 23:35:02 +00:00
websocketManager.disconnect(websocket)
2022-11-23 22:42:32 +00:00
@api_router.post("/api/v1/ws/{item_id}")
2022-11-24 00:46:39 +00:00
async def websocket_update_post(item_id: str, data: str):
2022-11-23 22:31:11 +00:00
try:
2022-11-23 23:35:02 +00:00
await websocketUpdater(item_id, data)
2022-11-23 22:31:11 +00:00
return {"sent": True, "data": data}
except Exception:
2022-11-23 22:31:11 +00:00
return {"sent": False, "data": data}
2022-11-23 22:42:32 +00:00
@api_router.get("/api/v1/ws/{item_id}/{data}")
2022-11-24 00:46:39 +00:00
async def websocket_update_get(item_id: str, data: str):
2022-11-23 22:31:11 +00:00
try:
2022-11-23 23:35:02 +00:00
await websocketUpdater(item_id, data)
2022-11-23 22:31:11 +00:00
return {"sent": True, "data": data}
except Exception:
2022-11-23 22:31:11 +00:00
return {"sent": False, "data": data}
@api_router.post("/api/v1/extension")
async def api_install_extension(
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092) no more superuser url! delete cookie on logout add usr login feature fix node management * Cleaned up login form * CreateUser * information leak * cleaner parsing usr from url * rename decorators * login secret * fix: add back `superuser` command * chore: remove `fastapi_login` * fix: extract `token` from cookie * chore: prepare to extract user * feat: check user * chore: code clean-up * feat: happy flow working * fix: usr only login * fix: user already logged in * feat: check user in URL * fix: verify password at DB level * fix: do not show `Login` controls if user already logged in * fix: separate login endpoints * fix: remove `usr` param * chore: update error message * refactor: register method * feat: logout * chore: move comments * fix: remove user auth check from API * fix: user check unnecessary * fix: redirect after logout * chore: remove garbage files * refactor: simplify constructor call * fix: hide user icon if not authorized * refactor: rename auth env vars * chore: code clean-up * fix: add types for `python-jose` * fix: add types for `passlib` * fix: return type * feat: set default value for `auth_secret_key` to hash of super user * fix: default value * feat: rework login page * feat: ui polishing * feat: google auth * feat: add google auth * chore: remove `authlib` dependency * refactor: extract `_handle_sso_login` method * refactor: convert methods to `properties` * refactor: rename: `user_api` to `auth_api` * feat: store user info from SSO * chore: re-arange the buttons * feat: conditional rendering of login options * feat: correctly render buttons * fix: re-add `Claim Bitcoin` from the main page * fix: create wallet must send new user * fix: no `username-password` auth method * refactor: rename auth method * fix: do not force API level UUID4 validation * feat: add validation for username * feat: add account page * feat: update account * feat: add `has_password` for user * fix: email not editable * feat: validate email for existing account * fix: register check * feat: reset password * chore: code clean-up * feat: handle token expired * fix: only redirect if `text/html` * refactor: remove `OAuth2PasswordRequestForm` * chore: remove `python-multipart` dependency * fix: handle no headers for exception * feat: add back button on error screen * feat: show user profile image * fix: check account creation permissions * fix: auth for internal api call * chore: add some docs * chore: code clean-up * fix: rebase stuff * fix: default value types * refactor: customize error messages * fix: move types libs to dev dependencies * doc: specify the `Authorization callback URL` * fix: pass missing superuser id in node ui test * fix: keep usr param on wallet redirect removing usr param causes an issue if the browser doesnt yet have an access token. * fix: do not redirect if `wal` query param not present * fix: add nativeBuildInputs and buildInputs overrides to flake.nix * bump fastapi-sso to 0.9.0 which fixes some security issues * refactor: move the `lnbits_admin_extensions` to decorators * chore: bring package config from `dev` * chore: re-add dependencies * chore: re-add cev dependencies * chore: re-add mypy ignores * feat: i18n * refactor: move admin ext check to decorator (fix after rebase) * fix: label mapping * fix: re-fetch user after first wallet was created * fix: unlikely case that `user` is not found * refactor translations (move '*' to code) * reorganize deps in pyproject.toml, add comment * update flake.lock and simplify flake.nix after upstreaming overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463 * fix: more relaxed email verification (by @prusnak) * fix: remove `\b` (boundaries) since we re using `fullmatch` * chore: `make bundle` --------- Co-authored-by: dni ⚡ <office@dnilabs.com> Co-authored-by: Arc <ben@arc.wales> Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 12:38:19 +02:00
data: CreateExtension,
user: User = Depends(check_admin),
access_token: Optional[str] = Depends(check_access_token),
):
2023-01-17 14:51:09 +02:00
release = await InstallableExtension.get_extension_release(
data.ext_id, data.source_repo, data.archive, data.version
)
2023-01-17 14:51:09 +02:00
if not release:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
)
if not release.is_version_compatible:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Incompatible extension version"
)
release.payment_hash = data.payment_hash
2023-01-17 11:16:54 +02:00
ext_info = InstallableExtension(
id=data.ext_id, name=data.ext_id, installed_release=release, icon=release.icon
)
2023-01-17 11:16:54 +02:00
try:
installed_ext = await get_installed_extension(data.ext_id)
ext_info.payments = installed_ext.payments if installed_ext else []
await ext_info.download_archive()
ext_info.extract_archive()
2022-11-29 18:48:11 +02:00
2023-01-11 14:34:05 +02:00
extension = Extension.from_installable_ext(ext_info)
2023-01-17 11:16:54 +02:00
db_version = (await get_dbversions()).get(data.ext_id, 0)
2023-01-11 14:34:05 +02:00
await migrate_extension_database(extension, db_version)
2023-01-17 16:28:24 +02:00
await add_installed_extension(ext_info)
2023-02-15 17:25:58 +02:00
if extension.is_upgrade_extension:
# call stop while the old routes are still active
await stop_extension_background_work(data.ext_id, user.id, access_token)
2023-02-15 17:25:58 +02:00
if data.ext_id not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [data.ext_id]
2022-12-22 16:59:14 +02:00
# mount routes for the new version
core_app_extra.register_new_ext_routes(extension)
2023-01-25 14:32:41 +02:00
if extension.upgrade_hash:
ext_info.nofiy_upgrade()
2023-02-01 06:12:00 +00:00
return extension
except AssertionError as e:
raise HTTPException(HTTPStatus.BAD_REQUEST, str(e))
except Exception as ex:
logger.warning(ex)
ext_info.clean_extension_files()
raise HTTPException(
2023-01-25 14:32:41 +02:00
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=(
f"Failed to install extension {ext_info.id} "
f"({ext_info.installed_version})."
),
)
@api_router.delete("/api/v1/extension/{ext_id}")
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092) no more superuser url! delete cookie on logout add usr login feature fix node management * Cleaned up login form * CreateUser * information leak * cleaner parsing usr from url * rename decorators * login secret * fix: add back `superuser` command * chore: remove `fastapi_login` * fix: extract `token` from cookie * chore: prepare to extract user * feat: check user * chore: code clean-up * feat: happy flow working * fix: usr only login * fix: user already logged in * feat: check user in URL * fix: verify password at DB level * fix: do not show `Login` controls if user already logged in * fix: separate login endpoints * fix: remove `usr` param * chore: update error message * refactor: register method * feat: logout * chore: move comments * fix: remove user auth check from API * fix: user check unnecessary * fix: redirect after logout * chore: remove garbage files * refactor: simplify constructor call * fix: hide user icon if not authorized * refactor: rename auth env vars * chore: code clean-up * fix: add types for `python-jose` * fix: add types for `passlib` * fix: return type * feat: set default value for `auth_secret_key` to hash of super user * fix: default value * feat: rework login page * feat: ui polishing * feat: google auth * feat: add google auth * chore: remove `authlib` dependency * refactor: extract `_handle_sso_login` method * refactor: convert methods to `properties` * refactor: rename: `user_api` to `auth_api` * feat: store user info from SSO * chore: re-arange the buttons * feat: conditional rendering of login options * feat: correctly render buttons * fix: re-add `Claim Bitcoin` from the main page * fix: create wallet must send new user * fix: no `username-password` auth method * refactor: rename auth method * fix: do not force API level UUID4 validation * feat: add validation for username * feat: add account page * feat: update account * feat: add `has_password` for user * fix: email not editable * feat: validate email for existing account * fix: register check * feat: reset password * chore: code clean-up * feat: handle token expired * fix: only redirect if `text/html` * refactor: remove `OAuth2PasswordRequestForm` * chore: remove `python-multipart` dependency * fix: handle no headers for exception * feat: add back button on error screen * feat: show user profile image * fix: check account creation permissions * fix: auth for internal api call * chore: add some docs * chore: code clean-up * fix: rebase stuff * fix: default value types * refactor: customize error messages * fix: move types libs to dev dependencies * doc: specify the `Authorization callback URL` * fix: pass missing superuser id in node ui test * fix: keep usr param on wallet redirect removing usr param causes an issue if the browser doesnt yet have an access token. * fix: do not redirect if `wal` query param not present * fix: add nativeBuildInputs and buildInputs overrides to flake.nix * bump fastapi-sso to 0.9.0 which fixes some security issues * refactor: move the `lnbits_admin_extensions` to decorators * chore: bring package config from `dev` * chore: re-add dependencies * chore: re-add cev dependencies * chore: re-add mypy ignores * feat: i18n * refactor: move admin ext check to decorator (fix after rebase) * fix: label mapping * fix: re-fetch user after first wallet was created * fix: unlikely case that `user` is not found * refactor translations (move '*' to code) * reorganize deps in pyproject.toml, add comment * update flake.lock and simplify flake.nix after upstreaming overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463 * fix: more relaxed email verification (by @prusnak) * fix: remove `\b` (boundaries) since we re using `fullmatch` * chore: `make bundle` --------- Co-authored-by: dni ⚡ <office@dnilabs.com> Co-authored-by: Arc <ben@arc.wales> Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 12:38:19 +02:00
async def api_uninstall_extension(
ext_id: str,
user: User = Depends(check_admin),
access_token: Optional[str] = Depends(check_access_token),
):
2023-02-16 10:48:27 +02:00
installable_extensions = await InstallableExtension.get_installable_extensions()
2023-01-11 15:51:56 +02:00
extensions = [e for e in installable_extensions if e.id == ext_id]
if len(extensions) == 0:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Unknown extension id: {ext_id}",
)
# check that other extensions do not depend on this one
for valid_ext_id in list(map(lambda e: e.code, get_valid_extensions())):
2023-01-11 15:51:56 +02:00
installed_ext = next(
(ext for ext in installable_extensions if ext.id == valid_ext_id), None
)
2023-01-11 15:51:56 +02:00
if installed_ext and ext_id in installed_ext.dependencies:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=(
f"Cannot uninstall. Extension '{installed_ext.name}' "
"depends on this one."
),
)
try:
# call stop while the old routes are still active
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092) no more superuser url! delete cookie on logout add usr login feature fix node management * Cleaned up login form * CreateUser * information leak * cleaner parsing usr from url * rename decorators * login secret * fix: add back `superuser` command * chore: remove `fastapi_login` * fix: extract `token` from cookie * chore: prepare to extract user * feat: check user * chore: code clean-up * feat: happy flow working * fix: usr only login * fix: user already logged in * feat: check user in URL * fix: verify password at DB level * fix: do not show `Login` controls if user already logged in * fix: separate login endpoints * fix: remove `usr` param * chore: update error message * refactor: register method * feat: logout * chore: move comments * fix: remove user auth check from API * fix: user check unnecessary * fix: redirect after logout * chore: remove garbage files * refactor: simplify constructor call * fix: hide user icon if not authorized * refactor: rename auth env vars * chore: code clean-up * fix: add types for `python-jose` * fix: add types for `passlib` * fix: return type * feat: set default value for `auth_secret_key` to hash of super user * fix: default value * feat: rework login page * feat: ui polishing * feat: google auth * feat: add google auth * chore: remove `authlib` dependency * refactor: extract `_handle_sso_login` method * refactor: convert methods to `properties` * refactor: rename: `user_api` to `auth_api` * feat: store user info from SSO * chore: re-arange the buttons * feat: conditional rendering of login options * feat: correctly render buttons * fix: re-add `Claim Bitcoin` from the main page * fix: create wallet must send new user * fix: no `username-password` auth method * refactor: rename auth method * fix: do not force API level UUID4 validation * feat: add validation for username * feat: add account page * feat: update account * feat: add `has_password` for user * fix: email not editable * feat: validate email for existing account * fix: register check * feat: reset password * chore: code clean-up * feat: handle token expired * fix: only redirect if `text/html` * refactor: remove `OAuth2PasswordRequestForm` * chore: remove `python-multipart` dependency * fix: handle no headers for exception * feat: add back button on error screen * feat: show user profile image * fix: check account creation permissions * fix: auth for internal api call * chore: add some docs * chore: code clean-up * fix: rebase stuff * fix: default value types * refactor: customize error messages * fix: move types libs to dev dependencies * doc: specify the `Authorization callback URL` * fix: pass missing superuser id in node ui test * fix: keep usr param on wallet redirect removing usr param causes an issue if the browser doesnt yet have an access token. * fix: do not redirect if `wal` query param not present * fix: add nativeBuildInputs and buildInputs overrides to flake.nix * bump fastapi-sso to 0.9.0 which fixes some security issues * refactor: move the `lnbits_admin_extensions` to decorators * chore: bring package config from `dev` * chore: re-add dependencies * chore: re-add cev dependencies * chore: re-add mypy ignores * feat: i18n * refactor: move admin ext check to decorator (fix after rebase) * fix: label mapping * fix: re-fetch user after first wallet was created * fix: unlikely case that `user` is not found * refactor translations (move '*' to code) * reorganize deps in pyproject.toml, add comment * update flake.lock and simplify flake.nix after upstreaming overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463 * fix: more relaxed email verification (by @prusnak) * fix: remove `\b` (boundaries) since we re using `fullmatch` * chore: `make bundle` --------- Co-authored-by: dni ⚡ <office@dnilabs.com> Co-authored-by: Arc <ben@arc.wales> Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 12:38:19 +02:00
await stop_extension_background_work(ext_id, user.id, access_token)
if ext_id not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [ext_id]
2023-01-11 15:51:56 +02:00
for ext_info in extensions:
ext_info.clean_extension_files()
await delete_installed_extension(ext_id=ext_info.id)
logger.success(f"Extension '{ext_id}' uninstalled.")
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
)
2023-01-16 12:07:46 +02:00
@api_router.get(
"/api/v1/extension/{ext_id}/releases", dependencies=[Depends(check_admin)]
)
async def get_extension_releases(ext_id: str):
2023-01-16 12:07:46 +02:00
try:
extension_releases: List[ExtensionRelease] = (
await InstallableExtension.get_extension_releases(ext_id)
)
2023-01-16 12:07:46 +02:00
installed_ext = await get_installed_extension(ext_id)
if not installed_ext:
return extension_releases
for release in extension_releases:
payment_info = installed_ext.find_existing_payment(release.pay_link)
if payment_info:
release.paid_sats = payment_info.amount
return extension_releases
2023-01-16 12:07:46 +02:00
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
2023-01-25 09:56:05 +02:00
)
@api_router.put("/api/v1/extension/invoice", dependencies=[Depends(check_admin)])
async def get_extension_invoice(data: CreateExtension):
try:
assert data.cost_sats, "A non-zero amount must be specified"
release = await InstallableExtension.get_extension_release(
data.ext_id, data.source_repo, data.archive, data.version
)
assert release, "Release not found"
assert release.pay_link, "Pay link not found for release"
payment_info = await fetch_release_payment_info(
release.pay_link, data.cost_sats
)
assert payment_info and payment_info.payment_request, "Cannot request invoice"
invoice = bolt11.decode(payment_info.payment_request)
assert invoice.amount_msat is not None, "Invoic amount is missing"
invoice_amount = int(invoice.amount_msat / 1000)
assert (
invoice_amount == data.cost_sats
), f"Wrong invoice amount: {invoice_amount}."
assert (
payment_info.payment_hash == invoice.payment_hash
), "Wroong invoice payment hash"
return payment_info
except AssertionError as e:
raise HTTPException(HTTPStatus.BAD_REQUEST, str(e))
except Exception as ex:
logger.warning(ex)
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot request invoice")
@api_router.get(
"/api/v1/extension/release/{org}/{repo}/{tag_name}",
dependencies=[Depends(check_admin)],
)
async def get_extension_release(org: str, repo: str, tag_name: str):
try:
config = await fetch_github_release_config(org, repo, tag_name)
if not config:
return {}
return {
"min_lnbits_version": config.min_lnbits_version,
"is_version_compatible": config.is_version_compatible(),
"warning": config.warning,
}
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
)
@api_router.delete(
"/api/v1/extension/{ext_id}/db",
dependencies=[Depends(check_admin)],
)
async def delete_extension_db(ext_id: str):
try:
db_version = (await get_dbversions()).get(ext_id, None)
if not db_version:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Unknown extension id: {ext_id}",
)
await drop_extension_db(ext_id=ext_id)
await delete_dbversion(ext_id=ext_id)
logger.success(f"Database removed for extension '{ext_id}'")
except HTTPException as ex:
logger.error(ex)
raise ex
except Exception as ex:
logger.error(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Cannot delete data for extension '{ext_id}'",
)