diff --git a/.env.example b/.env.example index 987c6ca69..bfaeb5152 100644 --- a/.env.example +++ b/.env.example @@ -3,17 +3,19 @@ PORT=5000 DEBUG=false -LNBITS_ALLOWED_USERS="" -LNBITS_ADMIN_USERS="" -# Extensions only admin can access -LNBITS_ADMIN_EXTENSIONS="ngrok" +LNBITS_ADMIN_USERS="" # User IDs seperated by comma +LNBITS_ADMIN_EXTENSIONS="ngrok" # Extensions only admin can access +LNBITS_ADMIN_UI=false # Extensions only admin can access + +LNBITS_ALLOWED_USERS="" # Restricts access, User IDs seperated by comma + LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" # csv ad image filepaths or urls, extensions can choose to honor LNBITS_AD_SPACE="" # Hides wallet api, extensions can choose to honor -LNBITS_HIDE_API=false +LNBITS_HIDE_API=false # Disable extensions for all users, use "all" to disable all extensions LNBITS_DISABLED_EXTENSIONS="amilk" @@ -67,7 +69,7 @@ LNBITS_KEY=LNBITS_ADMIN_KEY LND_REST_ENDPOINT=https://127.0.0.1:8080/ LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING" -# To use an AES-encrypted macaroon, set +# To use an AES-encrypted macaroon, set # LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn" # LNPayWallet diff --git a/lnbits/extensions/admin/README.md b/lnbits/extensions/admin/README.md new file mode 100644 index 000000000..277294592 --- /dev/null +++ b/lnbits/extensions/admin/README.md @@ -0,0 +1,11 @@ +
curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"
diff --git a/lnbits/extensions/admin/__init__.py b/lnbits/extensions/admin/__init__.py
new file mode 100644
index 000000000..d5f26c90d
--- /dev/null
+++ b/lnbits/extensions/admin/__init__.py
@@ -0,0 +1,10 @@
+from quart import Blueprint
+from lnbits.db import Database
+
+db = Database("ext_admin")
+
+admin_ext: Blueprint = Blueprint("admin", __name__, static_folder="static", template_folder="templates")
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/admin/config.json b/lnbits/extensions/admin/config.json
new file mode 100644
index 000000000..696617335
--- /dev/null
+++ b/lnbits/extensions/admin/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Admin",
+ "short_description": "Manage your LNbits install",
+ "icon": "build",
+ "contributors": ["benarc"]
+}
diff --git a/lnbits/extensions/admin/crud.py b/lnbits/extensions/admin/crud.py
new file mode 100644
index 000000000..cb8f9b5be
--- /dev/null
+++ b/lnbits/extensions/admin/crud.py
@@ -0,0 +1,59 @@
+from typing import List, Optional
+
+from . import db
+from .models import Admin, Funding
+from lnbits.settings import *
+from lnbits.helpers import urlsafe_short_hash
+from lnbits.core.crud import create_payment
+from lnbits.db import Connection
+
+
+def update_wallet_balance(wallet_id: str, amount: int) -> str:
+ temp_id = f"temp_{urlsafe_short_hash()}"
+ internal_id = f"internal_{urlsafe_short_hash()}"
+ create_payment(
+ wallet_id=wallet_id,
+ checking_id=internal_id,
+ payment_request="admin_internal",
+ payment_hash="admin_internal",
+ amount=amount * 1000,
+ memo="Admin top up",
+ pending=False,
+ )
+ return "success"
+
+
+async def update_admin(
+) -> Optional[Admin]:
+ if not CLightningWallet:
+ print("poo")
+ await db.execute(
+ """
+ UPDATE admin
+ SET user = ?, site_title = ?, site_tagline = ?, site_description = ?, allowed_users = ?, default_wallet_name = ?, data_folder = ?, disabled_ext = ?, force_https = ?, service_fee = ?, funding_source = ?
+ WHERE 1
+ """,
+ (
+
+ ),
+ )
+ row = await db.fetchone("SELECT * FROM admin WHERE 1")
+ return Admin.from_row(row) if row else None
+
+async def update_admin(admin_id: str, **kwargs) -> Optional[Admin]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+ await db.execute(
+ f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (*kwargs.values(), juke_id)
+ )
+ row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
+ return Jukebox(**row) if row else None
+
+async def get_admin() -> List[Admin]:
+ row = await db.fetchone("SELECT * FROM admin WHERE 1")
+ return Admin.from_row(row) if row else None
+
+
+async def get_funding() -> List[Funding]:
+ rows = await db.fetchall("SELECT * FROM funding")
+
+ return [Funding.from_row(row) for row in rows]
diff --git a/lnbits/extensions/admin/migrations.py b/lnbits/extensions/admin/migrations.py
new file mode 100644
index 000000000..82d934cb6
--- /dev/null
+++ b/lnbits/extensions/admin/migrations.py
@@ -0,0 +1,256 @@
+from sqlalchemy.exc import OperationalError # type: ignore
+from os import getenv
+from lnbits.helpers import urlsafe_short_hash
+
+
+async def m001_create_admin_table(db):
+ user = None
+ site_title = None
+ site_tagline = None
+ site_description = None
+ allowed_users = None
+ admin_user = None
+ default_wallet_name = None
+ data_folder = None
+ disabled_ext = None
+ force_https = True
+ service_fee = 0
+ funding_source = ""
+
+ if getenv("LNBITS_SITE_TITLE"):
+ site_title = getenv("LNBITS_SITE_TITLE")
+
+ if getenv("LNBITS_SITE_TAGLINE"):
+ site_tagline = getenv("LNBITS_SITE_TAGLINE")
+
+ if getenv("LNBITS_SITE_DESCRIPTION"):
+ site_description = getenv("LNBITS_SITE_DESCRIPTION")
+
+ if getenv("LNBITS_ALLOWED_USERS"):
+ allowed_users = getenv("LNBITS_ALLOWED_USERS")
+
+ if getenv("LNBITS_ADMIN_USER"):
+ admin_user = getenv("LNBITS_ADMIN_USER")
+
+ if getenv("LNBITS_DEFAULT_WALLET_NAME"):
+ default_wallet_name = getenv("LNBITS_DEFAULT_WALLET_NAME")
+
+ if getenv("LNBITS_DATA_FOLDER"):
+ data_folder = getenv("LNBITS_DATA_FOLDER")
+
+ if getenv("LNBITS_DISABLED_EXTENSIONS"):
+ disabled_ext = getenv("LNBITS_DISABLED_EXTENSIONS")
+
+ if getenv("LNBITS_FORCE_HTTPS"):
+ force_https = getenv("LNBITS_FORCE_HTTPS")
+
+ if getenv("LNBITS_SERVICE_FEE"):
+ service_fee = getenv("LNBITS_SERVICE_FEE")
+
+ if getenv("LNBITS_BACKEND_WALLET_CLASS"):
+ funding_source = getenv("LNBITS_BACKEND_WALLET_CLASS")
+
+ await db.execute(
+ """
+ CREATE TABLE IF NOT EXISTS admin (
+ user TEXT,
+ site_title TEXT,
+ site_tagline TEXT,
+ site_description TEXT,
+ admin_user TEXT,
+ allowed_users TEXT,
+ default_wallet_name TEXT,
+ data_folder TEXT,
+ disabled_ext TEXT,
+ force_https BOOLEAN,
+ service_fee INT,
+ funding_source TEXT
+ );
+ """
+ )
+ await db.execute(
+ """
+ INSERT INTO admin (user, site_title, site_tagline, site_description, admin_user, allowed_users, default_wallet_name, data_folder, disabled_ext, force_https, service_fee, funding_source)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ user,
+ site_title,
+ site_tagline,
+ site_description,
+ admin_user,
+ allowed_users,
+ default_wallet_name,
+ data_folder,
+ disabled_ext,
+ force_https,
+ service_fee,
+ funding_source,
+ ),
+ )
+
+
+async def m001_create_funding_table(db):
+
+ funding_wallet = getenv("LNBITS_BACKEND_WALLET_CLASS")
+
+ # Make the funding table, if it does not already exist
+ await db.execute(
+ """
+ CREATE TABLE IF NOT EXISTS funding (
+ id TEXT PRIMARY KEY,
+ backend_wallet TEXT,
+ endpoint TEXT,
+ port INT,
+ read_key TEXT,
+ invoice_key TEXT,
+ admin_key TEXT,
+ cert TEXT,
+ balance INT,
+ selected INT
+ );
+ """
+ )
+
+ await db.execute(
+ """
+ INSERT INTO funding (id, backend_wallet, endpoint, selected)
+ VALUES (?, ?, ?, ?)
+ """,
+ (
+ urlsafe_short_hash(),
+ "CLightningWallet",
+ getenv("CLIGHTNING_RPC"),
+ 1 if funding_wallet == "CLightningWallet" else 0,
+ ),
+ )
+ await db.execute(
+ """
+ INSERT INTO funding (id, backend_wallet, endpoint, admin_key, selected)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (
+ urlsafe_short_hash(),
+ "SparkWallet",
+ getenv("SPARK_URL"),
+ getenv("SPARK_TOKEN"),
+ 1 if funding_wallet == "SparkWallet" else 0,
+ ),
+ )
+
+ await db.execute(
+ """
+ INSERT INTO funding (id, backend_wallet, endpoint, admin_key, selected)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (
+ urlsafe_short_hash(),
+ "LnbitsWallet",
+ getenv("LNBITS_ENDPOINT"),
+ getenv("LNBITS_KEY"),
+ 1 if funding_wallet == "LnbitsWallet" else 0,
+ ),
+ )
+
+ await db.execute(
+ """
+ INSERT INTO funding (id, backend_wallet, endpoint, port, admin_key, cert, selected)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ urlsafe_short_hash(),
+ "LndWallet",
+ getenv("LND_GRPC_ENDPOINT"),
+ getenv("LND_GRPC_PORT"),
+ getenv("LND_GRPC_MACAROON"),
+ getenv("LND_GRPC_CERT"),
+ 1 if funding_wallet == "LndWallet" else 0,
+ ),
+ )
+
+ await db.execute(
+ """
+ INSERT INTO funding (id, backend_wallet, endpoint, admin_key, cert, selected)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ (
+ urlsafe_short_hash(),
+ "LndRestWallet",
+ getenv("LND_REST_ENDPOINT"),
+ getenv("LND_REST_MACAROON"),
+ getenv("LND_REST_CERT"),
+ 1 if funding_wallet == "LndWallet" else 0,
+ ),
+ )
+
+ await db.execute(
+ """
+ INSERT INTO funding (id, backend_wallet, endpoint, admin_key, cert, selected)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ (
+ urlsafe_short_hash(),
+ "LNPayWallet",
+ getenv("LNPAY_API_ENDPOINT"),
+ getenv("LNPAY_WALLET_KEY"),
+ getenv("LNPAY_API_KEY"), # this is going in as the cert
+ 1 if funding_wallet == "LNPayWallet" else 0,
+ ),
+ )
+
+ await db.execute(
+ """
+ INSERT INTO funding (id, backend_wallet, endpoint, admin_key, selected)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (
+ urlsafe_short_hash(),
+ "LntxbotWallet",
+ getenv("LNTXBOT_API_ENDPOINT"),
+ getenv("LNTXBOT_KEY"),
+ 1 if funding_wallet == "LntxbotWallet" else 0,
+ ),
+ )
+
+ await db.execute(
+ """
+ INSERT INTO funding (id, backend_wallet, endpoint, admin_key, selected)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (
+ urlsafe_short_hash(),
+ "OpenNodeWallet",
+ getenv("OPENNODE_API_ENDPOINT"),
+ getenv("OPENNODE_KEY"),
+ 1 if funding_wallet == "OpenNodeWallet" else 0,
+ ),
+ )
+
+ await db.execute(
+ """
+ INSERT INTO funding (id, backend_wallet, endpoint, admin_key, selected)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (
+ urlsafe_short_hash(),
+ "SparkWallet",
+ getenv("SPARK_URL"),
+ getenv("SPARK_TOKEN"),
+ 1 if funding_wallet == "SparkWallet" else 0,
+ ),
+ )
+
+ ## PLACEHOLDER FOR ECLAIR WALLET
+ # await db.execute(
+ # """
+ # INSERT INTO funding (id, backend_wallet, endpoint, admin_key, selected)
+ # VALUES (?, ?, ?, ?, ?)
+ # """,
+ # (
+ # urlsafe_short_hash(),
+ # "EclairWallet",
+ # getenv("ECLAIR_URL"),
+ # getenv("ECLAIR_PASS"),
+ # 1 if funding_wallet == "EclairWallet" else 0,
+ # ),
+ # )
diff --git a/lnbits/extensions/admin/models.py b/lnbits/extensions/admin/models.py
new file mode 100644
index 000000000..c38f17f48
--- /dev/null
+++ b/lnbits/extensions/admin/models.py
@@ -0,0 +1,38 @@
+from typing import NamedTuple
+from sqlite3 import Row
+
+class Admin(NamedTuple):
+ user: str
+ site_title: str
+ site_tagline: str
+ site_description:str
+ allowed_users: str
+ admin_user: str
+ default_wallet_name: str
+ data_folder: str
+ disabled_ext: str
+ force_https: str
+ service_fee: str
+ funding_source: str
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Admin":
+ data = dict(row)
+ return cls(**data)
+
+class Funding(NamedTuple):
+ id: str
+ backend_wallet: str
+ endpoint: str
+ port: str
+ read_key: str
+ invoice_key: str
+ admin_key: str
+ cert: str
+ balance: int
+ selected: int
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Funding":
+ data = dict(row)
+ return cls(**data)
diff --git a/lnbits/extensions/admin/templates/admin/index.html b/lnbits/extensions/admin/templates/admin/index.html
new file mode 100644
index 000000000..87cf09efa
--- /dev/null
+++ b/lnbits/extensions/admin/templates/admin/index.html
@@ -0,0 +1,565 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
+
+