Co-authored-by: alan <alan@lnbits.com> Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
280 lines
9.2 KiB
Python
280 lines
9.2 KiB
Python
from hashlib import sha256
|
|
from http import HTTPStatus
|
|
from pathlib import Path
|
|
from urllib.parse import urlencode, urlparse
|
|
|
|
import httpx
|
|
from fastapi import Depends, Request
|
|
from fastapi.exceptions import HTTPException
|
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
|
from fastapi.routing import APIRouter
|
|
from lnurl import url_decode
|
|
|
|
from lnbits.core.helpers import to_valid_user_id
|
|
from lnbits.core.models import User
|
|
from lnbits.core.services import create_invoice, create_user_account
|
|
from lnbits.decorators import (
|
|
check_admin,
|
|
check_admin_ui,
|
|
check_extension_builder,
|
|
check_first_install,
|
|
check_user_exists,
|
|
)
|
|
from lnbits.helpers import check_callback_url, template_renderer
|
|
from lnbits.settings import settings
|
|
|
|
from ..crud import get_user
|
|
|
|
generic_router = APIRouter(
|
|
tags=["Core NON-API Website Routes"], include_in_schema=False
|
|
)
|
|
|
|
|
|
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
|
async def favicon():
|
|
return RedirectResponse(settings.lnbits_qr_logo)
|
|
|
|
|
|
@generic_router.get("/robots.txt", response_class=HTMLResponse)
|
|
async def robots():
|
|
data = "User-agent: *\nDisallow: /"
|
|
return HTMLResponse(content=data, media_type="text/plain")
|
|
|
|
|
|
@generic_router.get(
|
|
"/extensions/builder/preview/{ext_id}/{resource}",
|
|
name="extensions builder",
|
|
dependencies=[Depends(check_extension_builder)],
|
|
)
|
|
async def extensions_builder_preview(
|
|
ext_id: str,
|
|
resource: str | None = None,
|
|
page_name: str | None = None,
|
|
user: User = Depends(check_user_exists),
|
|
) -> FileResponse:
|
|
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
|
|
|
file_name = "index"
|
|
if page_name == "public_page":
|
|
file_name = "public_page"
|
|
|
|
resource_path = Path(
|
|
settings.extension_builder_working_dir_path,
|
|
"extension_builder_stub",
|
|
ext_id,
|
|
working_dir_name,
|
|
ext_id,
|
|
"static",
|
|
)
|
|
|
|
file_ext = ".vue" if resource == "template" else ".js"
|
|
file_full_path = Path(resource_path, file_name + file_ext)
|
|
|
|
if not file_full_path.is_file():
|
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
|
|
|
response = FileResponse(file_full_path.absolute().as_posix())
|
|
response.headers["Content-Security-Policy"] = (
|
|
"default-src 'self'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
|
)
|
|
|
|
return response
|
|
|
|
|
|
@generic_router.get("/service-worker.js")
|
|
async def service_worker(request: Request):
|
|
return template_renderer().TemplateResponse(
|
|
request,
|
|
"service-worker.js",
|
|
{
|
|
"cache_version": settings.server_startup_time,
|
|
},
|
|
media_type="text/javascript",
|
|
)
|
|
|
|
|
|
@generic_router.get("/manifest/{usr}.webmanifest")
|
|
async def manifest(request: Request, usr: str):
|
|
host = urlparse(str(request.url)).netloc
|
|
user = await get_user(usr)
|
|
if not user:
|
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
|
return {
|
|
"short_name": settings.lnbits_site_title,
|
|
"name": settings.lnbits_site_title + " Wallet",
|
|
"icons": [
|
|
{
|
|
"src": (
|
|
settings.lnbits_custom_logo
|
|
if settings.lnbits_custom_logo
|
|
else "images/logos/lnbits.png"
|
|
),
|
|
"sizes": "512x512",
|
|
"type": "image/png",
|
|
},
|
|
{"src": "/static/favicon.ico", "sizes": "32x32", "type": "image/x-icon"},
|
|
{
|
|
"src": "/static/images/maskable_icon_x192.png",
|
|
"type": "image/png",
|
|
"sizes": "192x192",
|
|
"purpose": "maskable",
|
|
},
|
|
{
|
|
"src": "/static/images/maskable_icon_x512.png",
|
|
"type": "image/png",
|
|
"sizes": "512x512",
|
|
"purpose": "maskable",
|
|
},
|
|
{
|
|
"src": "/static/images/maskable_icon.png",
|
|
"type": "image/png",
|
|
"sizes": "1024x1024",
|
|
"purpose": "maskable",
|
|
},
|
|
],
|
|
"screenshots": [
|
|
{
|
|
"src": "/static/images/screenshot_desktop.png",
|
|
"sizes": "2394x1314",
|
|
"type": "image/png",
|
|
"form_factor": "wide",
|
|
"label": "LNbits - Desktop screenshot",
|
|
},
|
|
{
|
|
"src": "/static/images/screenshot_phone.png",
|
|
"sizes": "1080x1739",
|
|
"type": "image/png",
|
|
"form_factor": "narrow",
|
|
"label": "LNbits - Phone screenshot",
|
|
},
|
|
],
|
|
"start_url": f"/wallet?usr={usr}&wal={user.wallets[0].id}",
|
|
"background_color": "#1F2234",
|
|
"description": "Bitcoin Lightning Wallet",
|
|
"display": "standalone",
|
|
"scope": "/",
|
|
"theme_color": "#1F2234",
|
|
"shortcuts": [
|
|
{
|
|
"name": wallet.name,
|
|
"short_name": wallet.name,
|
|
"description": wallet.name,
|
|
"url": f"/wallet?usr={usr}&wal={wallet.id}",
|
|
"icons": [
|
|
{
|
|
"src": "/static/images/maskable_icon_x96.png",
|
|
"sizes": "96x96",
|
|
"type": "image/png",
|
|
}
|
|
],
|
|
}
|
|
for wallet in user.wallets
|
|
],
|
|
"url_handlers": [{"origin": f"https://{host}"}],
|
|
}
|
|
|
|
|
|
admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
|
|
|
|
|
@generic_router.get("/payments")
|
|
@generic_router.get("/wallet")
|
|
@generic_router.get("/wallet/{wallet_id}")
|
|
@generic_router.get("/wallets")
|
|
@generic_router.get("/account")
|
|
@generic_router.get("/extensions")
|
|
@generic_router.get("/users", dependencies=admin_ui_checks)
|
|
@generic_router.get("/audit", dependencies=admin_ui_checks)
|
|
@generic_router.get("/node", dependencies=admin_ui_checks)
|
|
@generic_router.get("/admin", dependencies=admin_ui_checks)
|
|
@generic_router.get("/admin/extensions/wasm", dependencies=admin_ui_checks)
|
|
@generic_router.get("/admin/extensions/wasm/limits", dependencies=admin_ui_checks)
|
|
@generic_router.get("/admin/extensions/wasm/{ext_id}", dependencies=admin_ui_checks)
|
|
@generic_router.get(
|
|
"/extensions/builder", dependencies=[Depends(check_extension_builder)]
|
|
)
|
|
@generic_router.get(
|
|
"/extensions/builder/preview", dependencies=[Depends(check_extension_builder)]
|
|
)
|
|
async def index(
|
|
request: Request,
|
|
ext_id: str | None = None,
|
|
user: User = Depends(check_user_exists),
|
|
) -> HTMLResponse:
|
|
return template_renderer().TemplateResponse(
|
|
request,
|
|
"base.html",
|
|
{
|
|
"user": user.json(),
|
|
},
|
|
)
|
|
|
|
|
|
@generic_router.get("/")
|
|
@generic_router.get("/node/public")
|
|
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
|
async def index_public(request: Request) -> HTMLResponse:
|
|
return template_renderer().TemplateResponse(request, "base.html", {"public": True})
|
|
|
|
|
|
@generic_router.get("/uuidv4/{hex_value}")
|
|
async def hex_to_uuid4(hex_value: str):
|
|
try:
|
|
user_id = to_valid_user_id(hex_value).hex
|
|
return RedirectResponse(url=f"/wallet?usr={user_id}")
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
|
) from exc
|
|
|
|
|
|
@generic_router.get("/lnurlwallet", response_class=RedirectResponse)
|
|
async def lnurlwallet(request: Request, lightning: str = ""):
|
|
"""
|
|
If a user doesn't have a Lightning Network wallet and scans the LNURLw QR code with
|
|
their smartphone camera, or a QR scanner app, they can follow the link provided to
|
|
claim their satoshis and get an instant LNbits wallet! lnbits/withdraw docs
|
|
"""
|
|
|
|
if not lightning:
|
|
return {"status": "ERROR", "reason": "lightning parameter not provided."}
|
|
if not settings.lnbits_allow_new_accounts:
|
|
return {"status": "ERROR", "reason": "New accounts are not allowed."}
|
|
|
|
lnurl = url_decode(lightning)
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
check_callback_url(lnurl)
|
|
res1 = await client.get(lnurl, timeout=2)
|
|
res1.raise_for_status()
|
|
data1 = res1.json()
|
|
if data1.get("tag") != "withdrawRequest":
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail="Invalid lnurl. Expected tag=withdrawRequest",
|
|
)
|
|
if not data1.get("maxWithdrawable"):
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail="Invalid lnurl. Expected maxWithdrawable",
|
|
)
|
|
account = await create_user_account()
|
|
wallet = account.wallets[0]
|
|
payment = await create_invoice(
|
|
wallet_id=wallet.id,
|
|
amount=data1.get("maxWithdrawable") / 1000,
|
|
memo=data1.get("defaultDescription", "lnurl wallet withdraw"),
|
|
)
|
|
url = data1.get("callback")
|
|
params = {"k1": data1.get("k1"), "pr": payment.bolt11}
|
|
callback = url + ("&" if urlparse(url).query else "?") + urlencode(params)
|
|
|
|
res2 = await client.get(callback, timeout=5)
|
|
res2.raise_for_status()
|
|
|
|
return RedirectResponse(
|
|
f"/wallet?usr={account.id}&wal={wallet.id}",
|
|
)
|