Compare commits
87
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e71830817 | ||
|
|
0fb2334181 | ||
|
|
94c7796dd5 | ||
|
|
af4ce8da52 | ||
|
|
4e8a5e050b | ||
|
|
3fd7aae10d | ||
|
|
64ad19a65b | ||
|
|
f93efbfb3a | ||
|
|
4429c0cdb6 | ||
|
|
2bdb89b4b9 | ||
|
|
75754598e1 | ||
|
|
b25a3c2bb4 | ||
|
|
e7c4de6506 | ||
|
|
c102f10fab | ||
|
|
7c28b58aaa | ||
|
|
2b27f13fa8 | ||
|
|
130f4fd0bc | ||
|
|
4894b70bad | ||
|
|
05769240d3 | ||
|
|
7bd127d22c | ||
|
|
6dc3df148d | ||
|
|
e286546b63 | ||
|
|
eae1be1db8 | ||
|
|
25ab676a3f | ||
|
|
2fd6648a9a | ||
|
|
8eda653e3f | ||
|
|
7e327b2c6a | ||
|
|
40c055e373 | ||
|
|
1d9808b269 | ||
|
|
c65ab2af87 | ||
|
|
15f093c136 | ||
|
|
5311b0a393 | ||
|
|
e72396fe31 | ||
|
|
d6d0ebb20e | ||
|
|
563dd5b499 | ||
|
|
9be894da74 | ||
|
|
87b66c554a | ||
|
|
314b533e39 | ||
|
|
af8f3382c1 | ||
|
|
67bb86c1ce | ||
|
|
f0f9e41f99 | ||
|
|
2fbaf57b5e | ||
|
|
87afba2abb | ||
|
|
c375bf1b15 | ||
|
|
dbcc920d0a | ||
|
|
2c61e219b8 | ||
|
|
6cb9b7da1f | ||
|
|
ee6ff7179e | ||
|
|
684dfcc9fb | ||
|
|
a498d4a877 | ||
|
|
ef90c983f2 | ||
|
|
da8a82c78e | ||
|
|
51f98e8130 | ||
|
|
7b4436b288 | ||
|
|
ec88cf483e | ||
|
|
ca215063be | ||
|
|
2674cfb5f1 | ||
|
|
26f0b69085 | ||
|
|
2069e3de8b | ||
|
|
800be6042f | ||
|
|
c65d373f23 | ||
|
|
087e095bcf | ||
|
|
027a161494 | ||
|
|
83ef05696e | ||
|
|
42962308e6 | ||
|
|
783ee256a1 | ||
|
|
4e805fd00e | ||
|
|
f54824cc44 | ||
|
|
768d2cbe2a | ||
|
|
efd36d0221 | ||
|
|
4684939e13 | ||
|
|
193ee20da4 | ||
|
|
2fe7ab4f83 | ||
|
|
10c8ca4ca1 | ||
|
|
f079762b71 | ||
|
|
2e042d2597 | ||
|
|
49b57c9f0b | ||
|
|
656c6cac5b | ||
|
|
83334d67c8 | ||
|
|
45e773b66f | ||
|
|
f692ac6f12 | ||
|
|
78202a027b | ||
|
|
28fa0906a6 | ||
|
|
91354f8be4 | ||
|
|
5f5d45e89f | ||
|
|
52bb125a25 | ||
|
|
2ca1ed897c |
+6
-1
@@ -6,6 +6,10 @@
|
||||
# The following settings are ONLY set in your .env file.
|
||||
# They are NOT managed by the Admin UI and are not stored in the database.
|
||||
|
||||
# === First Install Token ===
|
||||
# if set the user is required to enter this token on the /first_install page
|
||||
# FIRST_INSTALL_TOKEN="myaccesstoken"
|
||||
|
||||
# === Logging and Development ===
|
||||
|
||||
DEBUG=False
|
||||
@@ -55,7 +59,7 @@ LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
|
||||
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
|
||||
|
||||
# which fundingsources are allowed in the admin ui
|
||||
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet, SparkWallet, LightsparkSparkWallet"
|
||||
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet, SparkWallet, SparkL2Wallet"
|
||||
|
||||
# uvicorn variable, allow https behind a proxy
|
||||
# IMPORTANT: this also needs the webserver to be configured to forward the headers
|
||||
@@ -66,6 +70,7 @@ FORWARDED_ALLOW_IPS="*"
|
||||
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
|
||||
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"
|
||||
|
||||
|
||||
# ID of the super user. The user ID must exist.
|
||||
# SUPER_USER=""
|
||||
|
||||
|
||||
@@ -55,11 +55,21 @@ jobs:
|
||||
- name: Build and push boltz
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.boltz
|
||||
context: docker/boltzclient
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-boltz:${{ inputs.tag }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
build-args: LNBITS_TAG=${{ inputs.tag }}
|
||||
|
||||
- name: Build and push sparkl2
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: docker/sparkl2
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-sparkl2:${{ inputs.tag }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
build-args: LNBITS_TAG=${{ inputs.tag }}
|
||||
|
||||
@@ -38,8 +38,7 @@ jobs:
|
||||
|
||||
- name: Setup Regtest
|
||||
run: |
|
||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||
cd docker
|
||||
cd docker/regtest
|
||||
chmod +x ./start-regtest
|
||||
./start-regtest
|
||||
sudo chmod -R a+rwx .
|
||||
@@ -50,16 +49,16 @@ jobs:
|
||||
LNBITS_DATABASE_URL: ${{ inputs.db-url }}
|
||||
LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }}
|
||||
LND_REST_ENDPOINT: https://localhost:8081/
|
||||
LND_REST_CERT: ./docker/data/lnd-3/tls.cert
|
||||
LND_REST_MACAROON: ./docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||
LND_REST_CERT: ./docker/regtest/data/lnd-3/tls.cert
|
||||
LND_REST_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||
LND_GRPC_ENDPOINT: localhost
|
||||
LND_GRPC_PORT: 10009
|
||||
LND_GRPC_CERT: docker/data/lnd-3/tls.cert
|
||||
LND_GRPC_MACAROON: docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||
CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
|
||||
LND_GRPC_CERT: ./docker/regtest/data/lnd-3/tls.cert
|
||||
LND_GRPC_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||
CORELIGHTNING_RPC: ./docker/regtest/data/clightning-1/regtest/lightning-rpc
|
||||
CORELIGHTNING_REST_URL: https://localhost:3001
|
||||
CORELIGHTNING_REST_MACAROON: ./docker/data/clightning-2-rest/access.macaroon
|
||||
CORELIGHTNING_REST_CERT: ./docker/data/clightning-2-rest/certificate.pem
|
||||
CORELIGHTNING_REST_MACAROON: ./docker/regtest/data/clightning-2-rest/access.macaroon
|
||||
CORELIGHTNING_REST_CERT: ./docker/regtest/data/clightning-2-rest/certificate.pem
|
||||
LNBITS_ENDPOINT: http://localhost:5001
|
||||
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
|
||||
ECLAIR_URL: http://127.0.0.1:8082
|
||||
|
||||
@@ -44,15 +44,19 @@ jobs:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Install dependencies for building secp256k1
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build and publish to pypi
|
||||
uses: JRubics/poetry-publish@v1.15
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||
python-version: "3.10"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Build the project
|
||||
run: uv build
|
||||
- name: Publish to pypi
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
||||
run: uv publish
|
||||
|
||||
appimage:
|
||||
needs: [ release ]
|
||||
|
||||
@@ -56,15 +56,19 @@ jobs:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Install dependencies for building secp256k1
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build and publish to pypi
|
||||
uses: JRubics/poetry-publish@v1.15
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||
python-version: "3.10"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Build the project
|
||||
run: uv build
|
||||
- name: Publish to pypi
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
||||
run: uv publish
|
||||
|
||||
appimage:
|
||||
needs: [ release ]
|
||||
|
||||
@@ -42,7 +42,6 @@ lnbits/static/bundle.min.js.old
|
||||
lnbits/static/bundle.min.css.old
|
||||
lnbits/static/bundle-components.min.js.old
|
||||
lnbits/upgrades
|
||||
docker
|
||||
|
||||
# Nix
|
||||
*result*
|
||||
|
||||
@@ -14,11 +14,13 @@ ENV PATH="/root/.local/bin:$PATH"
|
||||
# Reinstall dependencies just in case (needed for CMD usage)
|
||||
RUN uv sync --all-extras
|
||||
|
||||
# LNbits + boltzd configuration
|
||||
# LNbits
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
|
||||
ENV FUNDING_SOURCE_MAX_RETRIES=10
|
||||
|
||||
# Boltzd
|
||||
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
|
||||
ENV BOLTZ_CLIENT_MACAROON="/root/.boltz/macaroons/admin.macaroon"
|
||||
ENV BOLTZ_CLIENT_CERT="/root/.boltz/tls.cert"
|
||||
@@ -26,8 +28,7 @@ ENV BOLTZ_CLIENT_WALLET="lnbits"
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
# Entrypoint to start boltzd and LNbits
|
||||
COPY dockerboltz.sh /dockerboltz.sh
|
||||
RUN chmod +x /dockerboltz.sh
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
CMD ["/dockerboltz.sh"]
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,18 @@
|
||||
!data
|
||||
data/*
|
||||
|
||||
!data/boltz
|
||||
data/boltz/*
|
||||
!data/boltz/boltz.conf
|
||||
|
||||
!data/boltz-client
|
||||
data/boltz-client/*
|
||||
!data/boltz-client/boltz.toml
|
||||
|
||||
!data/boltz-nginx
|
||||
data/boltz-nginx/*
|
||||
!data/boltz-nginx/default.conf
|
||||
|
||||
!data/eclair
|
||||
data/eclair/*
|
||||
!data/eclair/eclair.conf
|
||||
@@ -0,0 +1,99 @@
|
||||

|
||||
|
||||
# nodes
|
||||
|
||||
- lnd-1: for locally testing your current lnbits
|
||||
- lnd-2: used for boltz backend
|
||||
- lnd-3: used for lnbits inside docker
|
||||
- cln-1: for locally testing your current lnbits
|
||||
- cln-2: used for clightning-REST
|
||||
- eclair-1: for locally testing your current lnbits
|
||||
|
||||
# Installing regtest
|
||||
|
||||
get the regtest enviroment ready
|
||||
|
||||
```sh
|
||||
# Install docker https://docs.docker.com/engine/install/
|
||||
# Make sure your user has permission to use docker 'sudo usermod -aG docker ${USER}' then reboot
|
||||
# Stop/start docker 'sudo systemctl stop docker' 'sudo systemctl start docker'
|
||||
|
||||
sudo apt install jq
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
docker build -t lnbits/lnbits .
|
||||
mkdir docker
|
||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||
cd docker
|
||||
chmod +x ./start-regtest
|
||||
./start-regtest # start the regtest and also run tests
|
||||
sudo chown -R $USER ./data # Give the data file permissions for user
|
||||
```
|
||||
|
||||
# Running LNbits on regtest
|
||||
|
||||
add this ENV variables to your `.env` file
|
||||
|
||||
```sh
|
||||
DEBUG=true
|
||||
|
||||
# LND
|
||||
LNBITS_BACKEND_WALLET_CLASS="LndRestWallet"
|
||||
LND_REST_ENDPOINT=https://127.0.0.1:8081/
|
||||
LND_REST_CERT=/home/user/repos/lnbits/docker/data/lnd-1/tls.cert
|
||||
LND_REST_MACAROON=/home/user/repos/lnbits/docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
|
||||
|
||||
# CLN
|
||||
LNBITS_BACKEND_WALLET_CLASS="CoreLightningWallet"
|
||||
CORELIGHTNING_RPC=./docker/data/clightning-1/regtest/lightning-rpc
|
||||
|
||||
|
||||
# Run LNbits
|
||||
uv run lnbits
|
||||
|
||||
# Run LNbits with hot reload
|
||||
make dev
|
||||
```
|
||||
|
||||
# testing
|
||||
|
||||
```sh
|
||||
chmod +x ./start-regtest
|
||||
./start-regtest
|
||||
# short answer :)
|
||||
./start-regtest && echo "PASSED" || echo "FAILED" > /dev/null
|
||||
```
|
||||
|
||||
usage of the `bitcoin-cli-sim`, `lightning-cli-sim` and `lncli-sim` aliases
|
||||
|
||||
```sh
|
||||
cd ~/lnbits/docker
|
||||
source docker-scripts.sh
|
||||
# use bitcoin core, mine a block
|
||||
bitcoin-cli-sim -generate 1
|
||||
|
||||
# use c-lightning nodes
|
||||
lightning-cli-sim 1 newaddr | jq -r '.bech32' # use node 1
|
||||
lightning-cli-sim 2 getinfo # use node 2
|
||||
lightning-cli-sim 3 getinfo # use node 3
|
||||
|
||||
# use lnd nodes
|
||||
lncli-sim 1 newaddr p2wsh
|
||||
lncli-sim 2 listpeers
|
||||
```
|
||||
|
||||
# urls
|
||||
|
||||
- mempool: http://localhost:8080/
|
||||
- boltz api: http://localhost:9001/
|
||||
- lnd-1 rest: http://localhost:8081/
|
||||
- lnbits: http://localhost:5001/
|
||||
|
||||
# debugging docker logs
|
||||
|
||||
```sh
|
||||
docker logs lnbits-lnbits-1 -f
|
||||
docker logs lnbits-boltz-1 -f
|
||||
docker logs lnbits-clightning-1-1 -f
|
||||
docker logs lnbits-lnd-2-1 -f
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
standalone = true
|
||||
network = "regtest"
|
||||
|
||||
# Path the the log file
|
||||
logfile = ""
|
||||
|
||||
electrumUrl = "electrs:19001"
|
||||
electrumLiquidUrl = "electrs-liquid:19002"
|
||||
|
||||
[BOLTZ]
|
||||
# By default the daemon automatically connects to the official Boltz instance for the network LND is on
|
||||
# This value is used to override that
|
||||
url = "http://boltz-nginx:9001"
|
||||
|
||||
[DATABASE]
|
||||
# Path to the SQLite database file
|
||||
# path = "/home/michael/test.db"
|
||||
|
||||
[RPC]
|
||||
# Host of
|
||||
host = "0.0.0.0"
|
||||
|
||||
# Port of the gRPC interface
|
||||
port = 9002
|
||||
|
||||
# Whether the REST proxy for the gRPC interface should be disabled
|
||||
restDisabled = false
|
||||
|
||||
# Host of the REST proxy
|
||||
restHost = "0.0.0.0"
|
||||
|
||||
# Port of the REST proxy
|
||||
restPort = 9003
|
||||
|
||||
# Path to the TLS cert for the gRPC and REST interface
|
||||
tlsCert = ""
|
||||
|
||||
# Path to the TLS private key for the gRPC and REST interface
|
||||
tlsKey = ""
|
||||
noTls = true
|
||||
|
||||
# Whether the macaroon authentication for the gRPC and REST interface should be disabled
|
||||
noMacaroons = true
|
||||
|
||||
# Path to the admin macaroon for the gRPC and REST interface
|
||||
adminMacaroonPath = ""
|
||||
|
||||
# Path to the read only macaroon for the gRPC and REST interface
|
||||
readOnlyMacaroonPath = ""
|
||||
@@ -0,0 +1,49 @@
|
||||
upstream boltz {
|
||||
server boltz:9001;
|
||||
}
|
||||
|
||||
upstream boltzr {
|
||||
server boltz:9005;
|
||||
}
|
||||
|
||||
upstream ws {
|
||||
server boltz:9004;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 9001;
|
||||
listen [::]:9001;
|
||||
server_name localhost;
|
||||
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Access-Control-Allow-Methods 'GET, PATCH, DELETE, POST, OPTIONS' always;
|
||||
add_header Access-Control-Allow-Headers "*" always;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
if ($request_method = OPTIONS) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
location /v2/ws {
|
||||
proxy_pass http://ws/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
}
|
||||
|
||||
location ~ ^/v2/(lightning|swap/rescue|swap/restore) {
|
||||
proxy_pass http://boltzr;
|
||||
}
|
||||
|
||||
location /streamswapstatus {
|
||||
proxy_pass http://boltzr;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://boltz;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
[api]
|
||||
host = "0.0.0.0"
|
||||
port = 9_001
|
||||
|
||||
[grpc]
|
||||
host = "0.0.0.0"
|
||||
port = 9_000
|
||||
|
||||
[postgres]
|
||||
host = "boltz-postgres"
|
||||
port = 5432
|
||||
database = "boltz"
|
||||
username = "boltz"
|
||||
password = "boltz"
|
||||
|
||||
[sidecar]
|
||||
[sidecar.grpc]
|
||||
host = "127.0.0.1"
|
||||
port = 9003
|
||||
|
||||
[sidecar.ws]
|
||||
host = "0.0.0.0"
|
||||
port = 9004
|
||||
|
||||
[sidecar.api]
|
||||
host = "0.0.0.0"
|
||||
port = 9005
|
||||
|
||||
[[pairs]]
|
||||
isLegacy = true
|
||||
base = "BTC"
|
||||
quote = "BTC"
|
||||
rate = 1
|
||||
fee = 0.5
|
||||
swapInFee = 0.1
|
||||
maxSwapAmount = 40_294_967
|
||||
minSwapAmount = 50_000
|
||||
|
||||
[pairs.timeoutDelta]
|
||||
chain = 1440
|
||||
reverse = 1440
|
||||
swapMinimal = 1440
|
||||
swapMaximal = 2880
|
||||
swapTaproot = 10080
|
||||
|
||||
[[pairs]]
|
||||
isLegacy = true
|
||||
base = "L-BTC"
|
||||
quote = "BTC"
|
||||
fee = 0.25
|
||||
swapInFee = 0.1
|
||||
rate = 1
|
||||
maxSwapAmount = 40_294_967
|
||||
minSwapAmount = 100
|
||||
|
||||
[pairs.submarineSwap]
|
||||
minSwapAmount = 1_000
|
||||
minBatchedAmount = 21
|
||||
|
||||
[pairs.chainSwap]
|
||||
minSwapAmount = 25_000
|
||||
|
||||
[pairs.timeoutDelta]
|
||||
chain = 1440
|
||||
reverse = 1440
|
||||
swapMinimal = 1440
|
||||
swapMaximal = 2880
|
||||
swapTaproot = 10080
|
||||
|
||||
[[currencies]]
|
||||
symbol = "BTC"
|
||||
network = "bitcoinRegtest"
|
||||
minWalletBalance = 10_000_000
|
||||
minChannelBalance = 10_000_000
|
||||
maxSwapAmount = 40_294_967
|
||||
minSwapAmount = 10_000
|
||||
maxZeroConfAmount = 0
|
||||
|
||||
[currencies.chain]
|
||||
# mempoolSpace = "http://mempool-web:8090/api"
|
||||
host = "bitcoind"
|
||||
zmqpubrawtx = "tcp://bitcoind:29000"
|
||||
zmqpubrawblock = "tcp://bitcoind:29001"
|
||||
port = 18_443
|
||||
cookie = "/root/.bitcoin/regtest/.cookie"
|
||||
|
||||
wallet = "lnbits"
|
||||
|
||||
[currencies.lnd]
|
||||
host = "lnd-2"
|
||||
port = 10_009
|
||||
certpath = "/data/lnd/tls.cert"
|
||||
macaroonpath = "/data/lnd/data/chain/bitcoin/regtest/admin.macaroon"
|
||||
|
||||
|
||||
[liquid]
|
||||
symbol = "L-BTC"
|
||||
network = "liquidRegtest"
|
||||
|
||||
maxSwapAmount = 40_294_967
|
||||
minSwapAmount = 10_000
|
||||
maxZeroConfAmount = 40_294_967
|
||||
|
||||
[liquid.chain]
|
||||
host = "elementsd"
|
||||
port = 18884
|
||||
cookie = "/root/.elements/liquidregtest/.cookie"
|
||||
zmqpubrawtx = "tcp://elementsd:31000"
|
||||
zmqpubhashblock = "tcp://elementsd:31002"
|
||||
|
||||
wallet = "lnbits"
|
||||
@@ -0,0 +1,28 @@
|
||||
eclair {
|
||||
chain = "regtest"
|
||||
|
||||
api {
|
||||
binding-ip = "0.0.0.0"
|
||||
enabled = true
|
||||
port = 8080
|
||||
password = "lnbits"
|
||||
}
|
||||
|
||||
bitcoind {
|
||||
host = "bitcoind"
|
||||
rpcport = 18443
|
||||
auth = "safecookie"
|
||||
cookie = "/root/.bitcoin/regtest/.cookie"
|
||||
|
||||
zmqblock = "tcp://bitcoind:29002"
|
||||
zmqtx = "tcp://bitcoind:29000"
|
||||
}
|
||||
|
||||
channel {
|
||||
max-funding-satoshis = 10000000000
|
||||
}
|
||||
|
||||
features {
|
||||
option_support_large_channel = mandatory
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
services:
|
||||
|
||||
lnbits:
|
||||
hostname: lnbits
|
||||
depends_on:
|
||||
- lnd-3
|
||||
image: lnbits/lnbits
|
||||
restart: on-failure
|
||||
user: "0:0"
|
||||
environment:
|
||||
LNBITS_PORT: 5001
|
||||
DEBUG: true
|
||||
LNBITS_ADMIN_UI: false
|
||||
LNBITS_BACKEND_WALLET_CLASS: "LndRestWallet"
|
||||
LNBITS_DATA_FOLDER: "./data"
|
||||
LND_REST_ENDPOINT: "https://lnd-3:8081/"
|
||||
LND_REST_CERT: "./lnd/tls.cert"
|
||||
LND_REST_MACAROON: "./lnd/data/chain/bitcoin/regtest/admin.macaroon"
|
||||
ports:
|
||||
- 5001:5001
|
||||
volumes:
|
||||
- lnbits-data:/app/data
|
||||
- ./data/lnd-3:/app/lnd:uid=1000,gid=1000
|
||||
|
||||
boltz:
|
||||
hostname: boltz
|
||||
depends_on:
|
||||
- lnd-2
|
||||
- boltz-postgres
|
||||
restart: always
|
||||
image: boltz/boltz:latest
|
||||
ports:
|
||||
- 9000:9000
|
||||
entrypoint: "sh -c 'sleep 30; /boltz-backend/bin/boltzd'"
|
||||
volumes:
|
||||
- ./data/lnd-2:/data/lnd/
|
||||
- ./data/boltz/:/root/.boltz/
|
||||
- elements-data:/root/.elements
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
boltz-client:
|
||||
hostname: boltz-client
|
||||
depends_on:
|
||||
- boltz
|
||||
restart: always
|
||||
image: boltz/boltz-client:latest
|
||||
ports:
|
||||
- 9002:9002
|
||||
- 9003:9003
|
||||
expose:
|
||||
- 9002
|
||||
healthcheck:
|
||||
test: ['CMD', 'boltzcli', '--host', 'boltz-client', 'getinfo']
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 0s
|
||||
volumes:
|
||||
- elements-data:/root/.elements
|
||||
- ./data/boltz-client:/root/.boltz
|
||||
|
||||
boltz-backend-nginx:
|
||||
hostname: boltz-nginx
|
||||
restart: always
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- 9001:9001
|
||||
volumes:
|
||||
- nginx-data:/etc/nginx/conf.d
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'curl http://localhost:9001/version']
|
||||
timeout: 1s
|
||||
retries: 10
|
||||
interval: 1s
|
||||
start_period: 0s
|
||||
|
||||
boltz-postgres:
|
||||
hostname: boltz-postgres
|
||||
restart: always
|
||||
image: postgres:14-alpine
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready --dbname boltz --username boltz"]
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
environment:
|
||||
- POSTGRES_DB=boltz
|
||||
- POSTGRES_USER=boltz
|
||||
- POSTGRES_PASSWORD=boltz
|
||||
expose:
|
||||
- 5432
|
||||
|
||||
bitcoind:
|
||||
hostname: bitcoind
|
||||
image: boltz/bitcoin-core:25.0
|
||||
command:
|
||||
- -regtest
|
||||
- -fallbackfee=0.00000253
|
||||
- -zmqpubrawtx=tcp://0.0.0.0:29000
|
||||
- -zmqpubrawblock=tcp://0.0.0.0:29001
|
||||
- -zmqpubhashblock=tcp://0.0.0.0:29002
|
||||
- -txindex
|
||||
- -rpcallowip=0.0.0.0/0
|
||||
- -rpcbind=0.0.0.0
|
||||
- -addresstype=bech32
|
||||
- -changetype=bech32
|
||||
- -dbcache=2048
|
||||
- -rpcworkqueue=256
|
||||
volumes:
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
expose:
|
||||
- 29000
|
||||
- 29001
|
||||
- 29002
|
||||
- 18443
|
||||
- 18444
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"bitcoin-cli",
|
||||
"--rpccookiefile=/root/.bitcoin/regtest/.cookie",
|
||||
"-regtest",
|
||||
"getblockchaininfo",
|
||||
]
|
||||
timeout: 1s
|
||||
retries: 1
|
||||
interval: 1s
|
||||
start_period: 0s
|
||||
|
||||
clightning-1:
|
||||
hostname: clightning-1
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/c-lightning:24.11
|
||||
command:
|
||||
- --large-channels
|
||||
- --network=regtest
|
||||
- --grpc-port=9736
|
||||
- --bind-addr=0.0.0.0:9735
|
||||
- --bitcoin-rpcconnect=bitcoind
|
||||
- --bitcoin-rpcport=18443
|
||||
- --clnrest-host=0.0.0.0
|
||||
- --clnrest-port=3010
|
||||
expose:
|
||||
- 9735
|
||||
ports:
|
||||
- 9736:9736
|
||||
- 3010:3010
|
||||
volumes:
|
||||
- ./data/clightning-1:/root/.lightning/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
clightning-2:
|
||||
hostname: clightning-2
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/c-lightning:22.11.1
|
||||
command:
|
||||
- --large-channels
|
||||
- --network=regtest
|
||||
- --grpc-port=9737
|
||||
- --bind-addr=0.0.0.0:9735
|
||||
- --bitcoin-rpcconnect=bitcoind
|
||||
- --bitcoin-rpcport=18443
|
||||
expose:
|
||||
- 9735
|
||||
ports:
|
||||
- 9737:9737
|
||||
volumes:
|
||||
- ./data/clightning-2:/root/.lightning/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
clightning-2-rest:
|
||||
hostname: clightning-2-rest
|
||||
depends_on:
|
||||
- clightning-2
|
||||
image: saubyk/c-lightning-rest:0.10.7
|
||||
entrypoint: "sh -c 'sleep 35 && /sbin/tini -g -- ./docker-entrypoint.sh'"
|
||||
ports:
|
||||
- 3001:3001
|
||||
expose:
|
||||
- 3001
|
||||
volumes:
|
||||
- ./data/clightning-2:/root/.lightning/:uid=1000,gid=1000
|
||||
- ./data/clightning-2-rest:/usr/src/app/certs/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
clightning-3:
|
||||
hostname: clightning-3
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/c-lightning:24.11
|
||||
command:
|
||||
- --large-channels
|
||||
- --network=regtest
|
||||
- --grpc-port=9738
|
||||
- --bind-addr=0.0.0.0:9735
|
||||
- --bitcoin-rpcconnect=bitcoind
|
||||
- --bitcoin-rpcport=18443
|
||||
expose:
|
||||
- 9735
|
||||
ports:
|
||||
- 9738:9738
|
||||
volumes:
|
||||
- ./data/clightning-3:/root/.lightning/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
lnd-1:
|
||||
hostname: lnd-1
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/lnd:0.18.4-beta
|
||||
restart: on-failure
|
||||
command:
|
||||
- --listen=lnd-1:9735
|
||||
- --rpclisten=lnd-1:10009
|
||||
- --restlisten=lnd-1:8081
|
||||
- --bitcoin.active
|
||||
- --bitcoin.regtest
|
||||
- --bitcoin.node=bitcoind
|
||||
- --bitcoind.rpchost=bitcoind
|
||||
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
|
||||
- --bitcoind.zmqpubrawtx=bitcoind:29000
|
||||
- --bitcoind.zmqpubrawblock=bitcoind:29001
|
||||
- --noseedbackup
|
||||
- --protocol.wumbo-channels
|
||||
expose:
|
||||
- 8081
|
||||
- 9735
|
||||
- 10009
|
||||
volumes:
|
||||
- ./data/lnd-1:/root/.lnd/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
lnd-2:
|
||||
hostname: lnd-2
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/lnd:0.19.3-beta
|
||||
restart: on-failure
|
||||
command:
|
||||
- --listen=lnd-2:9735
|
||||
- --rpclisten=lnd-2:10009
|
||||
- --restlisten=lnd-2:8081
|
||||
- --bitcoin.active
|
||||
- --bitcoin.regtest
|
||||
- --bitcoin.node=bitcoind
|
||||
- --bitcoind.rpchost=bitcoind
|
||||
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
|
||||
- --bitcoind.zmqpubrawtx=bitcoind:29000
|
||||
- --bitcoind.zmqpubrawblock=bitcoind:29001
|
||||
- --noseedbackup
|
||||
- --protocol.wumbo-channels
|
||||
expose:
|
||||
- 8081
|
||||
- 9735
|
||||
- 10009
|
||||
volumes:
|
||||
- ./data/lnd-2:/root/.lnd/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
lnd-3:
|
||||
hostname: lnd-3
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/lnd:0.18.4-beta
|
||||
restart: on-failure
|
||||
command:
|
||||
- --listen=lnd-3:9735
|
||||
- --rpclisten=lnd-3:10009
|
||||
- --restlisten=lnd-3:8081
|
||||
- --bitcoin.active
|
||||
- --bitcoin.regtest
|
||||
- --bitcoin.node=bitcoind
|
||||
- --bitcoind.rpchost=bitcoind
|
||||
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
|
||||
- --bitcoind.zmqpubrawtx=bitcoind:29000
|
||||
- --bitcoind.zmqpubrawblock=bitcoind:29001
|
||||
- --noseedbackup
|
||||
- --protocol.wumbo-channels
|
||||
ports:
|
||||
- 8081:8081
|
||||
- 10009:10009
|
||||
expose:
|
||||
- 8081
|
||||
- 9735
|
||||
- 10009
|
||||
volumes:
|
||||
- ./data/lnd-3:/root/.lnd/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
lnd-4:
|
||||
hostname: lnd-4
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/lnd:0.18.4-beta
|
||||
restart: on-failure
|
||||
command:
|
||||
- --listen=lnd-4:9735
|
||||
- --rpclisten=lnd-4:10009
|
||||
- --restlisten=lnd-4:8081
|
||||
- --bitcoin.active
|
||||
- --bitcoin.regtest
|
||||
- --bitcoin.node=bitcoind
|
||||
- --bitcoind.rpchost=bitcoind
|
||||
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
|
||||
- --bitcoind.zmqpubrawtx=bitcoind:29000
|
||||
- --bitcoind.zmqpubrawblock=bitcoind:29001
|
||||
- --noseedbackup
|
||||
- --protocol.wumbo-channels
|
||||
expose:
|
||||
- 8081
|
||||
- 9735
|
||||
- 10009
|
||||
volumes:
|
||||
- ./data/lnd-4:/root/.lnd/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
eclair:
|
||||
hostname: eclair
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/eclair:0.8.0
|
||||
restart: on-failure
|
||||
entrypoint: "sh -c 'JAVA_OPTS=-Xmx512m /eclair-node/bin/eclair-node.sh -Declair.datadir=/root/eclair -Declair.printToConsole'"
|
||||
ports:
|
||||
- 8082:8080
|
||||
expose:
|
||||
- 9735
|
||||
- 8080
|
||||
volumes:
|
||||
- ./data/eclair:/root/eclair
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
electrs:
|
||||
hostname: electrs
|
||||
restart: always
|
||||
image: boltz/electrs:latest
|
||||
entrypoint: ["electrs-bitcoin"]
|
||||
command:
|
||||
- --electrum-rpc-addr
|
||||
- electrs:19001
|
||||
- --http-addr
|
||||
- electrs:3002
|
||||
- --daemon-rpc-addr
|
||||
- bitcoind:18443
|
||||
- --network
|
||||
- regtest
|
||||
- --jsonrpc-import
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -s $(hostname):3002/blocks/tip/height"]
|
||||
timeout: 1s
|
||||
retries: 20
|
||||
interval: 2s
|
||||
start_period: 5s
|
||||
ports:
|
||||
- 19001:19001
|
||||
- 3002:3002
|
||||
volumes:
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
elementsd:
|
||||
hostname: elementsd
|
||||
restart: always
|
||||
image: boltz/elements:latest
|
||||
expose:
|
||||
- 31001
|
||||
ports:
|
||||
- 31000:31000
|
||||
- 31002:31002
|
||||
- 18884:18884
|
||||
command:
|
||||
- -chain=liquidregtest
|
||||
- -txindex=1
|
||||
- -rest=1
|
||||
- -server=1
|
||||
- -rpcallowip=0.0.0.0/0
|
||||
- -validatepegin=0
|
||||
- -initialfreecoins=2100000000000000
|
||||
- -fallbackfee=0.000001
|
||||
- -rpcbind=0.0.0.0
|
||||
- -rpcport=18884
|
||||
- -zmqpubrawtx=tcp://0.0.0.0:31000
|
||||
- -zmqpubrawblock=tcp://0.0.0.0:31001
|
||||
- -zmqpubhashblock=tcp://0.0.0.0:31002
|
||||
- -acceptdiscountct=1
|
||||
- -creatediscountct=1
|
||||
volumes:
|
||||
- elements-data:/root/.elements
|
||||
|
||||
electrs-liquid:
|
||||
hostname: electrs-liquid
|
||||
restart: always
|
||||
image: boltz/electrs:latest
|
||||
entrypoint: ["electrs-liquid"]
|
||||
command:
|
||||
- --electrum-rpc-addr
|
||||
- electrs-liquid:19002
|
||||
- --http-addr
|
||||
- electrs-liquid:3003
|
||||
- --daemon-rpc-addr
|
||||
- elementsd:18884
|
||||
- --daemon-dir
|
||||
- /root/.elements
|
||||
- --network
|
||||
- liquidregtest
|
||||
- --parent-network
|
||||
- regtest
|
||||
- --jsonrpc-import
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -s $(hostname):3003/blocks/tip/height"]
|
||||
timeout: 15s
|
||||
retries: 20
|
||||
interval: 2s
|
||||
start_period: 5s
|
||||
ports:
|
||||
- 19002:19002
|
||||
- 3003:3003
|
||||
volumes:
|
||||
- elements-data:/root/.elements
|
||||
|
||||
volumes:
|
||||
lnbits-data:
|
||||
bitcoin-data:
|
||||
elements-data:
|
||||
nginx-data:
|
||||
name: nginx-data
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: ./data/boltz-nginx/
|
||||
@@ -0,0 +1,322 @@
|
||||
#!/bin/sh
|
||||
export COMPOSE_PROJECT_NAME=lnbits
|
||||
|
||||
boltzcli-sim() {
|
||||
docker exec -it lnbits-boltz-client-1 boltzcli "$@"
|
||||
}
|
||||
|
||||
bitcoin-cli-sim() {
|
||||
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest "$@"
|
||||
}
|
||||
|
||||
elements-cli-sim() {
|
||||
docker exec lnbits-elementsd-1 elements-cli -rpcport=18884 -chain=liquidregtest "$@"
|
||||
}
|
||||
|
||||
# args(i, cmd)
|
||||
lightning-cli-sim() {
|
||||
i=$1
|
||||
shift # shift first argument so we can use $@
|
||||
docker exec lnbits-clightning-$i-1 lightning-cli --network regtest "$@"
|
||||
}
|
||||
|
||||
# args(i, cmd)
|
||||
lncli-sim() {
|
||||
i=$1
|
||||
shift # shift first argument so we can use $@
|
||||
docker exec lnbits-lnd-$i-1 lncli --network regtest --rpcserver=lnd-$i:10009 "$@"
|
||||
}
|
||||
|
||||
get-eclair-pubkey() {
|
||||
while true; do
|
||||
pubkey=$(docker exec lnbits-eclair-1 curl http://localhost:8080/getinfo -X POST -s -u :lnbits | jq -r .nodeId 2> /dev/null)
|
||||
pubkeyPrefix=$(echo $pubkey | cut -c1,2)
|
||||
if [[ "$pubkeyPrefix" == "02" || "$pubkeyPrefix" == "03" ]]; then
|
||||
echo $pubkey
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait-for-eclair-channel() {
|
||||
while true; do
|
||||
state=$(docker exec lnbits-eclair-1 curl http://localhost:8080/channels -X POST -s -u :lnbits | jq -r ".[0].state")
|
||||
pending=$(docker exec lnbits-eclair-1 curl -s http://localhost:8080/channels -X POST -u :lnbits| jq '. | length')
|
||||
echo "eclair-1 pendingchannels: $pending, current state: $state"
|
||||
if [[ "$state" == "NORMAL" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# args(i)
|
||||
fund_boltz_client() {
|
||||
# first address of seed: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
|
||||
address="el1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f3mmz5l7uw5pqmx6xf5xy50hsn6vhkm5euwt72x878eq6zxx2z0z676mna6kdq"
|
||||
echo "funding: $address on boltz-client"
|
||||
elements-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
|
||||
}
|
||||
|
||||
|
||||
# args(i)
|
||||
fund_clightning_node() {
|
||||
address=$(lightning-cli-sim $1 newaddr | jq -r .bech32)
|
||||
echo "funding: $address on clightning-node: $1"
|
||||
bitcoin-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
|
||||
}
|
||||
|
||||
# args(i)
|
||||
fund_lnd_node() {
|
||||
address=$(lncli-sim $1 newaddress p2wkh | jq -r .address)
|
||||
echo "funding: $address on lnd-node: $1"
|
||||
bitcoin-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
|
||||
}
|
||||
|
||||
# args(i, j)
|
||||
connect_clightning_node() {
|
||||
pubkey=$(lightning-cli-sim $2 getinfo | jq -r '.id')
|
||||
lightning-cli-sim $1 connect $pubkey@lnbits-clightning-$2-1:9735 | jq -r '.id'
|
||||
}
|
||||
|
||||
lnbits-regtest-start(){
|
||||
if ! command -v jq &> /dev/null
|
||||
then
|
||||
echo "jq is not installed"
|
||||
exit
|
||||
fi
|
||||
if ! command -v docker &> /dev/null
|
||||
then
|
||||
echo "docker is not installed"
|
||||
exit
|
||||
fi
|
||||
if ! command -v docker version &> /dev/null
|
||||
then
|
||||
echo "dockerd is not running"
|
||||
exit
|
||||
fi
|
||||
lnbits-regtest-stop
|
||||
docker compose up -d --remove-orphans
|
||||
lnbits-regtest-init
|
||||
}
|
||||
|
||||
lnbits-regtest-start-log(){
|
||||
lnbits-regtest-stop
|
||||
docker compose up --remove-orphans
|
||||
lnbits-regtest-init
|
||||
}
|
||||
|
||||
lnbits-regtest-stop(){
|
||||
docker compose down --volumes
|
||||
# clean up lightning node data
|
||||
sudo rm -rf ./data/clightning-1 ./data/clightning-2 ./data/clightning-3 ./data/lnd-1 ./data/lnd-2 ./data/lnd-3 ./data/lnd-4 ./data/boltz/boltz.db ./data/eclair/regtest ./data/boltz-client/liquid-wallet ./data/boltz-client/bitcoin-wallet ./data/boltz-client/wallet ./data/boltz-client/boltz.db
|
||||
# recreate lightning node data folders preventing permission errors
|
||||
mkdir ./data/clightning-1 ./data/clightning-2 ./data/clightning-3 ./data/lnd-1 ./data/lnd-2 ./data/lnd-3 ./data/lnd-4
|
||||
}
|
||||
|
||||
lnbits-regtest-restart(){
|
||||
lnbits-regtest-stop
|
||||
lnbits-regtest-start
|
||||
}
|
||||
|
||||
boltz-client-init(){
|
||||
for i in 0 1 2; do
|
||||
fund_boltz_client
|
||||
done
|
||||
elements-cli-sim -generate 3 > /dev/null
|
||||
}
|
||||
|
||||
lnbits-bitcoin-init(){
|
||||
echo "init_bitcoin_wallet..."
|
||||
bitcoin-cli-sim createwallet lnbits || bitcoin-cli-sim loadwallet lnbits
|
||||
echo "mining 150 blocks..."
|
||||
bitcoin-cli-sim -generate 150 > /dev/null
|
||||
}
|
||||
|
||||
lnbits-elements-init(){
|
||||
echo "init_elements_wallet..."
|
||||
elements-cli-sim createwallet lnbits || elements-cli-sim loadwallet lnbits
|
||||
echo "mining 150 blocks..."
|
||||
elements-cli-sim -generate 150 > /dev/null
|
||||
elements-cli-sim rescanblockchain
|
||||
}
|
||||
|
||||
lnbits-init(){
|
||||
echo "init_lnbits..."
|
||||
docker exec lnbits-lnbits-1 uv run python tools/create_fake_admin.py
|
||||
}
|
||||
|
||||
lnbits-regtest-init(){
|
||||
lnbits-bitcoin-init
|
||||
lnbits-elements-init
|
||||
lnbits-lightning-sync
|
||||
lnbits-lightning-init
|
||||
boltz-client-init
|
||||
lnbits-init
|
||||
}
|
||||
|
||||
lnbits-lightning-sync(){
|
||||
wait-for-clightning-sync 1
|
||||
wait-for-clightning-sync 2
|
||||
wait-for-clightning-sync 3
|
||||
wait-for-lnd-sync 1
|
||||
wait-for-lnd-sync 2
|
||||
wait-for-lnd-sync 3
|
||||
wait-for-lnd-sync 4
|
||||
}
|
||||
|
||||
lnbits-lightning-init(){
|
||||
|
||||
# create 10 UTXOs for each node
|
||||
for i in 0 1 2; do
|
||||
fund_clightning_node 1
|
||||
fund_clightning_node 2
|
||||
fund_clightning_node 3
|
||||
fund_lnd_node 1
|
||||
fund_lnd_node 2
|
||||
fund_lnd_node 3
|
||||
fund_lnd_node 4
|
||||
done
|
||||
|
||||
echo "mining 3 blocks..."
|
||||
bitcoin-cli-sim -generate 3 > /dev/null
|
||||
|
||||
lnbits-lightning-sync
|
||||
|
||||
channel_confirms=6
|
||||
channel_size=24000000 # 0.024 btc
|
||||
balance_size=12000000 # 0.12 btc
|
||||
balance_size_msat=12000000000 # 0.12 btc
|
||||
|
||||
# lnd-1 -> lnd-2
|
||||
lncli-sim 1 connect $(lncli-sim 2 getinfo | jq -r '.identity_pubkey')@lnbits-lnd-2-1 > /dev/null
|
||||
echo "open channel from lnd-1 to lnd-2"
|
||||
lncli-sim 1 openchannel $(lncli-sim 2 getinfo | jq -r '.identity_pubkey') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-1 -> lnd-3
|
||||
lncli-sim 1 connect $(lncli-sim 3 getinfo | jq -r '.identity_pubkey')@lnbits-lnd-3-1 > /dev/null
|
||||
echo "open channel from lnd-1 to lnd-3"
|
||||
lncli-sim 1 openchannel $(lncli-sim 3 getinfo | jq -r '.identity_pubkey') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-1 -> cln-1
|
||||
lncli-sim 1 connect $(lightning-cli-sim 1 getinfo | jq -r '.id')@lnbits-clightning-1-1 > /dev/null
|
||||
echo "open channel from lnd-1 to cln-1"
|
||||
lncli-sim 1 openchannel $(lightning-cli-sim 1 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-1 -> cln-2
|
||||
lncli-sim 1 connect $(lightning-cli-sim 2 getinfo | jq -r '.id')@lnbits-clightning-2-1 > /dev/null
|
||||
echo "open channel from lnd-1 to cln-2"
|
||||
lncli-sim 1 openchannel $(lightning-cli-sim 2 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-1 -> cln-3
|
||||
lncli-sim 1 connect $(lightning-cli-sim 3 getinfo | jq -r '.id')@lnbits-clightning-3-1 > /dev/null
|
||||
echo "open channel from lnd-1 to cln-3"
|
||||
lncli-sim 1 openchannel $(lightning-cli-sim 3 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-2 -> cln-2
|
||||
lncli-sim 2 connect $(lightning-cli-sim 2 getinfo | jq -r '.id')@lnbits-clightning-2-1 > /dev/null
|
||||
echo "open channel from lnd-2 to cln-2"
|
||||
lncli-sim 2 openchannel $(lightning-cli-sim 2 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 2
|
||||
|
||||
# lnd-3 -> cln-3
|
||||
lncli-sim 3 connect $(lightning-cli-sim 3 getinfo | jq -r '.id')@lnbits-clightning-3-1 > /dev/null
|
||||
echo "open channel from lnd-3 to cln-1"
|
||||
lncli-sim 3 openchannel $(lightning-cli-sim 3 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 3
|
||||
|
||||
# lnd-3 -> cln-1
|
||||
lncli-sim 3 connect $(lightning-cli-sim 1 getinfo | jq -r '.id')@lnbits-clightning-1-1 > /dev/null
|
||||
echo "open channel from lnd-3 to cln-1"
|
||||
lncli-sim 3 openchannel $(lightning-cli-sim 1 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 3
|
||||
|
||||
# lnd-1 -> eclair-1
|
||||
lncli-sim 1 connect $(get-eclair-pubkey)@lnbits-eclair-1 > /dev/null
|
||||
echo "open channel from lnd-2 to eclair-1"
|
||||
lncli-sim 1 openchannel $(get-eclair-pubkey) $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-2 -> eclair-1
|
||||
lncli-sim 2 connect $(get-eclair-pubkey)@lnbits-eclair-1 > /dev/null
|
||||
echo "open channel from lnd-2 to eclair-1"
|
||||
lncli-sim 2 openchannel $(get-eclair-pubkey) $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 2
|
||||
|
||||
wait-for-clightning-channel 1
|
||||
wait-for-clightning-channel 2
|
||||
wait-for-clightning-channel 3
|
||||
|
||||
wait-for-eclair-channel
|
||||
|
||||
lnbits-lightning-sync
|
||||
|
||||
}
|
||||
|
||||
wait-for-lnd-channel(){
|
||||
while true; do
|
||||
pending=$(lncli-sim $1 pendingchannels | jq -r '.pending_open_channels | length')
|
||||
echo "lnd-$1 pendingchannels: $pending"
|
||||
if [[ "$pending" == "0" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait-for-lnd-sync(){
|
||||
while true; do
|
||||
if [[ "$(lncli-sim $1 getinfo 2>&1 | jq -r '.synced_to_chain' 2> /dev/null)" == "true" ]]; then
|
||||
echo "lnd-$1 is synced!"
|
||||
break
|
||||
fi
|
||||
echo "waiting for lnd-$1 to sync..."
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait-for-clightning-channel(){
|
||||
while true; do
|
||||
pending=$(lightning-cli-sim $1 getinfo | jq -r '.num_pending_channels | length')
|
||||
echo "cln-$1 pendingchannels: $pending"
|
||||
if [[ "$pending" == "0" ]]; then
|
||||
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_bitcoind_sync' 2> /dev/null)" == "null" ]]; then
|
||||
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_lightningd_sync' 2> /dev/null)" == "null" ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait-for-clightning-sync(){
|
||||
while true; do
|
||||
if [[ ! "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.id' 2> /dev/null)" == "null" ]]; then
|
||||
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_bitcoind_sync' 2> /dev/null)" == "null" ]]; then
|
||||
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_lightningd_sync' 2> /dev/null)" == "null" ]]; then
|
||||
echo "cln-$1 is synced!"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "waiting for cln-$1 to sync..."
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
print_success() {
|
||||
printf "\033[;1;32mPASSED\033[;0m $1\n"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
printf "\033[;1;31mFAILED\033[;0m $1\n"
|
||||
}
|
||||
|
||||
run(){
|
||||
label=$1
|
||||
value=$2
|
||||
cmd=$3
|
||||
if [[ "$cmd" == "$value" ]]; then
|
||||
print_success "$label is $cmd"
|
||||
else
|
||||
print_error "$label is $cmd, should be $value"
|
||||
failed="true"
|
||||
fi
|
||||
}
|
||||
|
||||
failed="false"
|
||||
blockheight=213
|
||||
utxos=3
|
||||
channel_size=24000000 # 0.024 btc
|
||||
balance_size=12000000 # 0.012 btc
|
||||
|
||||
source $(pwd)/docker-scripts.sh
|
||||
lnbits-regtest-start
|
||||
echo "=================================="
|
||||
printf "\033[;1;36mregtest started! starting tests...\033[;0m\n"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
for i in 1 2 3; do
|
||||
run "lnd-$i .synced_to_chain" "true" $(lncli-sim $i getinfo | jq -r ".synced_to_chain")
|
||||
run "lnd-$i utxo count" $utxos $(lncli-sim $i listunspent | jq -r ".utxos | length")
|
||||
run "lnd-$i .block_height" $blockheight $(lncli-sim $i getinfo | jq -r ".block_height")
|
||||
if [[ "$i" == "1" ]]; then
|
||||
channel_count=6
|
||||
else
|
||||
channel_count=3
|
||||
fi
|
||||
run "lnd-$i openchannels" $channel_count $(lncli-sim $i listchannels | jq -r ".channels | length")
|
||||
run "lnd-$i .channels[0].capacity" $channel_size $(lncli-sim $i listchannels | jq -r ".channels[0].capacity")
|
||||
run "lnd-$i .channels[0].push_amount_sat" $balance_size $(lncli-sim $i listchannels | jq -r ".channels[0].push_amount_sat")
|
||||
done
|
||||
for i in 1 2 3; do
|
||||
# run "cln-$i blockheight" $blockheight $(lightning-cli-sim $i getinfo | jq -r ".blockheight")
|
||||
run "cln-$i utxo count" $utxos $(lightning-cli-sim $i listfunds | jq -r ".outputs | length")
|
||||
run "cln-$i openchannels" 2 $(lightning-cli-sim $i getinfo | jq -r ".num_active_channels")
|
||||
run "cln-$i channel[0].state" "CHANNELD_NORMAL" $(lightning-cli-sim $i listfunds | jq -r ".channels[0].state")
|
||||
run "cln-$i channel[0].amount_msat" $(($channel_size * 1000)) $(lightning-cli-sim $i listfunds | jq -r ".channels[0].amount_msat" | sed 's/msat//g')
|
||||
run "cln-$i channel[0].our_amount_msat" $(($balance_size * 1000)) $(lightning-cli-sim $i listfunds | jq -r ".channels[0].our_amount_msat" | sed 's/msat//g')
|
||||
done
|
||||
|
||||
run "eclair-1 openchannels" 2 $(docker exec lnbits-eclair-1 curl -s http://localhost:8080/channels -X POST -u :lnbits| jq '. | length')
|
||||
run "eclair-1 blockHeight" $blockheight $(docker exec lnbits-eclair-1 curl -s http://localhost:8080/getinfo -X POST -u :lnbits| jq '.blockHeight')
|
||||
run "lnbits service status" "200" $(curl -s -o /dev/null -w "%{http_code}" "http://localhost:5001/")
|
||||
run "boltz service status" "200" $(curl -s -o /dev/null --head -w "%{http_code}" "http://localhost:9001/version")
|
||||
|
||||
# return non-zero exit code if a test fails
|
||||
if [[ "$failed" == "true" ]]; then
|
||||
echo ""
|
||||
echo "=================================="
|
||||
print_error "one more more tests failed"
|
||||
echo "=================================="
|
||||
exit 1
|
||||
else
|
||||
echo ""
|
||||
echo "=================================="
|
||||
print_success "all tests passed! yay!"
|
||||
echo "=================================="
|
||||
fi
|
||||
@@ -0,0 +1,36 @@
|
||||
ARG LNBITS_TAG=latest
|
||||
|
||||
FROM lnbits/lnbits:${LNBITS_TAG}
|
||||
|
||||
RUN apt-get update && apt-get -y upgrade
|
||||
RUN apt-get install -y netcat-openbsd npm nodejs git
|
||||
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
# install sparksidecar
|
||||
RUN git clone https://github.com/lnbits/spark_sidecar
|
||||
RUN cd spark_sidecar && npm install
|
||||
|
||||
# Reinstall dependencies for lnbits just in case (needed for CMD usage)
|
||||
RUN uv sync --all-extras
|
||||
|
||||
# LNBITS
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
ENV LNBITS_BACKEND_WALLET_CLASS="SparkL2Wallet"
|
||||
ENV FUNDING_SOURCE_MAX_RETRIES=10
|
||||
|
||||
# spark sidecar
|
||||
ENV SPARK_MNEMONIC="bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom"
|
||||
ENV SPARK_NETWORK="MAINNET"
|
||||
ENV SPARK_SIDECAR_PORT="8765"
|
||||
ENV SPARK_PAY_WAIT_MS="20000"
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Entrypoint to start sparksidebar and LNbits
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
node spark_sidecar/server.mjs &
|
||||
|
||||
SIDECAR_PID=$!
|
||||
|
||||
# Wait for startup
|
||||
for i in {1..10}; do
|
||||
if nc -z localhost $SPARK_SIDECAR_PORT; then
|
||||
echo "sparksidebar is up!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for sparksidebar to start..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Optional: check if still not up
|
||||
if ! nc -z localhost $SPARK_SIDECAR_PORT; then
|
||||
echo "sparksidebar did not start successfully."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
|
||||
exec uv run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
|
||||
@@ -50,6 +50,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
|
||||
| **Blink** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; focuses on mobile integrations. |
|
||||
| **ZBD** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
|
||||
| **Spark (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
|
||||
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | Medium | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
|
||||
| **Cliche Wallet** | Self-custodial | ❌ | Medium | ❌ | Medium | Manual | Moderate | Moderate | Minimal fees | Medium | Lightweight wallet; suitable for embedded systems. |
|
||||
| **Strike** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
||||
| **LNPay** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
||||
@@ -57,7 +58,9 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
|
||||
| **LN.tips** | Custodial/Self-Custodial | Depends on provider | Medium | ❌ | Low | Provider-managed | Moderate | Low | Transaction fees may apply | Medium | Simple hosted service; use LN.tips API as your backend. |
|
||||
| **Fake Wallet** | Testing (simulated) | ❌ | Low | ❌ | N/A | N/A | Easy | Low | None (test only) | N/A | For testing only; mints accounting units in LNbits (no real sats, unit name configurable). |
|
||||
|
||||
## TODO: add Spark
|
||||
## Spark (L2)
|
||||
|
||||
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. You can let LNbits run the sidecar internally or run your own instance (see `https://github.com/lnbits/spark_sidecar`).
|
||||
|
||||
### Notes for readers
|
||||
|
||||
|
||||
+42
-2
@@ -41,6 +41,7 @@ A backend wallet is selected and configured entirely through LNbits environment
|
||||
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
|
||||
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
|
||||
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
|
||||
| [Spark (L2)](#spark-l2) | | |
|
||||
| [Cliche Wallet](#cliche-wallet) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
|
||||
| [Breez SDK](#breez-sdk) | [Breez Liquid SDK](#breez-liquid-sdk) | [Nostr Wallet Connect](#nostr-wallet-connect-nwc) |
|
||||
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
|
||||
@@ -127,6 +128,47 @@ Old REST interface using [RTL c-lightning-REST](https://github.com/Ride-The-Ligh
|
||||
- `SPARK_URL`: `http://10.147.17.230:9737/rpc`
|
||||
- `SPARK_TOKEN`: `secret_access_key`
|
||||
|
||||
## Spark (L2)
|
||||
|
||||
Spark L2 is a self-custodial funding source that uses a small Node.js sidecar to talk to the Spark network and expose an HTTP API. You can let LNbits run the sidecar internally, or run it yourself and point LNbits at it.
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `SparkL2Wallet`
|
||||
|
||||
**Internal sidecar (LNbits-managed)**
|
||||
|
||||
- `SPARK_L2_INTERNAL_SIDECAR_VERSION`: `0.1.1`
|
||||
- `SPARK_L2_MNEMONIC`: `...` (optional; auto-generated if blank)
|
||||
- `SPARK_L2_NETWORK`: `MAINNET`
|
||||
|
||||
Optional tuning:
|
||||
|
||||
- `SPARK_L2_PAY_WAIT_MS`: `4000`
|
||||
- `SPARK_L2_PAY_POLL_MS`: `500`
|
||||
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000`
|
||||
|
||||
**External sidecar**
|
||||
|
||||
- `SPARK_L2_EXTERNAL_ENDPOINT`: `http://127.0.0.1:8765`
|
||||
- `SPARK_L2_EXTERNAL_API_KEY`: `...` (if your sidecar requires auth)
|
||||
|
||||
Example: run the sidecar manually (external mode)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lnbits/spark_sidecar.git
|
||||
cd spark_sidecar
|
||||
npm install
|
||||
|
||||
SPARK_MNEMONIC="bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom" \
|
||||
SPARK_NETWORK=MAINNET \
|
||||
SPARK_SIDECAR_PORT=8765 \
|
||||
SPARK_PAY_WAIT_MS=20000 \
|
||||
node server.mjs
|
||||
```
|
||||
|
||||
For testing, you can generate a 12-word mnemonic at `https://iancoleman.io/bip39/`. Store it securely — it controls your funds. Then select **Spark (L2)** as the funding source in LNbits.
|
||||
|
||||
## LND (REST)
|
||||
|
||||
**Required env vars**
|
||||
@@ -338,8 +380,6 @@ Configure in the admin UI or via env vars:
|
||||
|
||||
<a id="strike"></a>
|
||||
|
||||
## TODO: add Spark
|
||||
|
||||
## Strike (alpha)
|
||||
|
||||
Custodial provider integrated via **Strike OAuth Connect** (OAuth 2.0 / OIDC). Authenticate a Strike user in your app, then call Strike APIs on the user’s behalf once scopes are granted. Requires a Strike business account, registered OAuth client, minimal scopes, and login/logout redirect URLs.
|
||||
|
||||
+28
-2
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import glob
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
@@ -35,9 +36,12 @@ from lnbits.core.tasks import (
|
||||
purge_audit_data,
|
||||
run_by_the_minute_tasks,
|
||||
wait_for_audit_data,
|
||||
wait_for_paid_invoices,
|
||||
wait_notification_messages,
|
||||
)
|
||||
from lnbits.core.tasks import (
|
||||
wait_for_paid_invoices as wait_for_paid_invoices_core,
|
||||
)
|
||||
from lnbits.core.wasm.extension_host import register_wasm_ext_routes
|
||||
from lnbits.exceptions import register_exception_handlers
|
||||
from lnbits.helpers import version_parse
|
||||
from lnbits.settings import settings
|
||||
@@ -419,6 +423,8 @@ def register_new_ratelimiter(app: FastAPI) -> Callable:
|
||||
|
||||
def register_ext_tasks(ext: Extension) -> None:
|
||||
"""Register extension async tasks."""
|
||||
if ext.extension_type == "wasm":
|
||||
return
|
||||
ext_module = importlib.import_module(ext.module_name)
|
||||
|
||||
if hasattr(ext_module, f"{ext.code}_start"):
|
||||
@@ -428,6 +434,12 @@ def register_ext_tasks(ext: Extension) -> None:
|
||||
|
||||
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
"""Register FastAPI routes for extension."""
|
||||
if ext.extension_type != "wasm":
|
||||
ext.extension_type = _load_extension_type(ext.code) or ext.extension_type
|
||||
if ext.extension_type == "wasm":
|
||||
settings.activate_extension_paths(ext.code, ext.upgrade_hash, [])
|
||||
register_wasm_ext_routes(app, ext)
|
||||
return
|
||||
ext_module = importlib.import_module(ext.module_name)
|
||||
|
||||
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
||||
@@ -453,6 +465,20 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
app.include_router(router=ext_route, prefix=prefix)
|
||||
|
||||
|
||||
def _load_extension_type(ext_id: str) -> str | None:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return None
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
return config_json.get("extension_type")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def check_and_register_extensions(app: FastAPI) -> None:
|
||||
await check_installed_extensions(app)
|
||||
for ext in await get_valid_extensions(False):
|
||||
@@ -476,7 +502,7 @@ def register_async_tasks() -> None:
|
||||
# core invoice listener
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
create_permanent_task(lambda: wait_for_paid_invoices_core(invoice_queue))
|
||||
|
||||
create_permanent_task(run_by_the_minute_tasks)
|
||||
create_permanent_task(purge_audit_data)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.extensions import (
|
||||
InstallableExtension,
|
||||
@@ -49,7 +51,8 @@ async def drop_extension_db(ext_id: str, conn: Connection | None = None) -> None
|
||||
)
|
||||
# Check that 'ext_id' is a valid extension id and not a malicious string
|
||||
if not row:
|
||||
raise Exception(f"Extension '{ext_id}' db version cannot be found")
|
||||
if not re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*", ext_id):
|
||||
raise Exception(f"Extension '{ext_id}' db version cannot be found")
|
||||
|
||||
is_file_based_db = await Database.clean_ext_db_files(ext_id)
|
||||
if is_file_based_db:
|
||||
|
||||
+76
-21
@@ -4,10 +4,12 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||
from lnbits.core.crud.wallets import create_wallet, get_wallets
|
||||
from lnbits.core.crud.wallets import clear_wallet_cache, create_wallet, get_wallets
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import UserAcls
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
from lnbits.helpers import sha256s
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
from ..models import (
|
||||
Account,
|
||||
@@ -41,6 +43,7 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
||||
"DELETE from accounts WHERE id = :user",
|
||||
{"user": user_id},
|
||||
)
|
||||
await clear_user_id_cache(user_id)
|
||||
|
||||
|
||||
async def get_accounts(
|
||||
@@ -69,6 +72,7 @@ async def get_accounts(
|
||||
accounts.email,
|
||||
accounts.pubkey,
|
||||
accounts.external_id,
|
||||
accounts.activated,
|
||||
SUM(COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
), 0)) as balance_msat,
|
||||
@@ -93,12 +97,18 @@ async def get_accounts(
|
||||
)
|
||||
|
||||
|
||||
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
|
||||
async def get_account(
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(user_id) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"SELECT * FROM accounts WHERE id = :id",
|
||||
{"id": user_id},
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE id = :id AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"id": user_id, "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
@@ -124,55 +134,79 @@ async def delete_accounts_no_wallets(
|
||||
|
||||
|
||||
async def get_account_by_username(
|
||||
username: str, conn: Connection | None = None
|
||||
username: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(username) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"SELECT * FROM accounts WHERE LOWER(username) = :username",
|
||||
{"username": username.lower()},
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(username) = :username
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"username": username.lower(), "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_pubkey(
|
||||
pubkey: str, conn: Connection | None = None
|
||||
pubkey: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
return await (conn or db).fetchone(
|
||||
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
||||
{"pubkey": pubkey.lower()},
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(pubkey) = :pubkey
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"pubkey": pubkey.lower(), "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_email(
|
||||
email: str, conn: Connection | None = None
|
||||
email: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(email) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"SELECT * FROM accounts WHERE LOWER(email) = :email",
|
||||
{"email": email.lower()},
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(email) = :email
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"email": email.lower(), "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_username_or_email(
|
||||
username_or_email: str, conn: Connection | None = None
|
||||
username_or_email: str,
|
||||
active_only: bool = True,
|
||||
conn: Connection | None = None,
|
||||
) -> Account | None:
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE LOWER(email) = :value or LOWER(username) = :value
|
||||
WHERE
|
||||
(LOWER(email) = :value or LOWER(username) = :value)
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"value": username_or_email.lower()},
|
||||
{"value": username_or_email.lower(), "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
||||
async def get_user(
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> User | None:
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
account = await get_account(user_id, conn=conn)
|
||||
account = await get_account(user_id, active_only, conn=conn)
|
||||
if not account:
|
||||
return None
|
||||
return await get_user_from_account(account, conn=conn)
|
||||
@@ -191,6 +225,7 @@ async def get_user_from_account(
|
||||
|
||||
return User(
|
||||
id=account.id,
|
||||
activated=account.activated,
|
||||
email=account.email,
|
||||
username=account.username,
|
||||
pubkey=account.pubkey,
|
||||
@@ -204,6 +239,7 @@ async def get_user_from_account(
|
||||
super_user=account.is_super_user,
|
||||
fiat_providers=account.fiat_providers,
|
||||
has_password=account.password_hash is not None,
|
||||
ui_customization=account.ui_customization or {},
|
||||
)
|
||||
|
||||
|
||||
@@ -215,12 +251,31 @@ async def update_user_access_control_list(
|
||||
|
||||
|
||||
async def get_user_access_control_lists(
|
||||
user_id: str, conn: Connection | None = None
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> UserAcls:
|
||||
user_acls = await (conn or db).fetchone(
|
||||
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
||||
{"id": user_id},
|
||||
"""
|
||||
SELECT id, access_control_list FROM accounts
|
||||
WHERE id = :user_id AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"user_id": user_id, "activated": active_only},
|
||||
UserAcls,
|
||||
)
|
||||
|
||||
return user_acls or UserAcls(id=user_id)
|
||||
|
||||
|
||||
async def clear_user_id_cache(user_id: str):
|
||||
user = await get_user(user_id, active_only=True)
|
||||
if user:
|
||||
clear_user_cache(user)
|
||||
|
||||
|
||||
def clear_user_cache(user: User):
|
||||
user_cache_key: str | None = cache.pop(
|
||||
f"auth:user:cache_key:{sha256s(user.id)}", None
|
||||
)
|
||||
if user_cache_key:
|
||||
cache.pop(user_cache_key)
|
||||
for wallet in user.wallets:
|
||||
clear_wallet_cache(wallet)
|
||||
|
||||
@@ -50,7 +50,7 @@ async def delete_wallet(
|
||||
deleted: bool = True,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
_clear_wallet_cache(wallet_id)
|
||||
clear_wallet_id_cache(wallet_id)
|
||||
now = int(time())
|
||||
|
||||
await (conn or db).execute(
|
||||
@@ -65,7 +65,7 @@ async def delete_wallet(
|
||||
|
||||
|
||||
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
||||
_clear_wallet_cache(wallet_id)
|
||||
clear_wallet_id_cache(wallet_id)
|
||||
await (conn or db).execute(
|
||||
"DELETE FROM wallets WHERE id = :wallet",
|
||||
{"wallet": wallet_id},
|
||||
@@ -75,7 +75,7 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
|
||||
async def delete_wallet_by_id(
|
||||
wallet_id: str, conn: Connection | None = None
|
||||
) -> int | None:
|
||||
_clear_wallet_cache(wallet_id)
|
||||
clear_wallet_id_cache(wallet_id)
|
||||
now = int(time())
|
||||
result = await (conn or db).execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
@@ -226,11 +226,14 @@ async def get_wallet_for_key(
|
||||
) -> Wallet | None:
|
||||
wallet = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT *, COALESCE((
|
||||
SELECT wallets.*, COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
), 0)
|
||||
AS balance_msat FROM wallets
|
||||
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
||||
INNER JOIN accounts ON wallets.user = accounts.id
|
||||
WHERE (adminkey = :key OR inkey = :key)
|
||||
AND deleted = false
|
||||
AND accounts.activated = true
|
||||
""",
|
||||
{"key": key},
|
||||
Wallet,
|
||||
@@ -250,8 +253,11 @@ async def get_base_wallet_for_key(
|
||||
) -> BaseWallet | None:
|
||||
wallet = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
|
||||
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
||||
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets
|
||||
INNER JOIN accounts ON wallets.user = accounts.id
|
||||
WHERE (adminkey = :key OR inkey = :key)
|
||||
AND deleted = false
|
||||
AND accounts.activated = true
|
||||
""",
|
||||
{"key": key},
|
||||
BaseWallet,
|
||||
@@ -294,8 +300,14 @@ async def get_total_balance(conn: Connection | None = None):
|
||||
return row.get("balance", 0) or 0
|
||||
|
||||
|
||||
def _clear_wallet_cache(wallet_id):
|
||||
def clear_wallet_id_cache(wallet_id: str):
|
||||
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
||||
if cached_wallet:
|
||||
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
||||
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
||||
|
||||
|
||||
def clear_wallet_cache(wallet: Wallet):
|
||||
cache.pop(f"auth:wallet:{wallet.id}")
|
||||
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
||||
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
||||
|
||||
@@ -22,6 +22,8 @@ from lnbits.settings import settings
|
||||
async def migrate_extension_database(
|
||||
ext: InstallableExtension, current_version: DbVersion | None = None
|
||||
):
|
||||
if _is_wasm_extension(ext):
|
||||
return
|
||||
|
||||
try:
|
||||
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
|
||||
@@ -58,6 +60,34 @@ async def run_migration(
|
||||
await update_migration_version(conn, db_name, version)
|
||||
|
||||
|
||||
def _is_wasm_extension(ext: InstallableExtension) -> bool:
|
||||
if ext.meta and ext.meta.extension_type == "wasm":
|
||||
return True
|
||||
|
||||
try:
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
candidate_dirs = [
|
||||
Path(ext.ext_dir),
|
||||
Path(settings.lnbits_extensions_path, "extensions", ext.id),
|
||||
Path(settings.lnbits_path, "extensions", ext.id),
|
||||
Path.cwd() / "extensions" / ext.id,
|
||||
]
|
||||
for base in candidate_dirs:
|
||||
conf_path = Path(base, "config.json")
|
||||
if not conf_path.is_file():
|
||||
continue
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
if config_json.get("extension_type") == "wasm":
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.debug(f"Failed to load extension config for '{ext.id}': {exc!s}")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def to_valid_user_id(user_id: str) -> UUID:
|
||||
if len(user_id) < 32:
|
||||
raise ValueError("User ID must have at least 128 bits")
|
||||
|
||||
@@ -848,3 +848,19 @@ async def m042_index_accounts(db: Connection):
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m043_add_ui_customization_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds ui_customization column to accounts.
|
||||
Used for server side persistence of UI customization settings.
|
||||
"""
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
||||
|
||||
|
||||
async def m044_add_activated_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds activated column to accounts.
|
||||
Used for account activation status.
|
||||
"""
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
|
||||
|
||||
@@ -83,6 +83,11 @@ class ExtensionConfig(BaseModel):
|
||||
warning: str | None = ""
|
||||
min_lnbits_version: str | None
|
||||
max_lnbits_version: str | None
|
||||
permissions: list[ExtensionPermission] = []
|
||||
extension_type: str | None = "python"
|
||||
public_kv_keys: list[str] = []
|
||||
public_wasm_functions: list[str] = []
|
||||
payment_tags: list[str] = []
|
||||
|
||||
def is_version_compatible(self) -> bool:
|
||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||
@@ -115,6 +120,20 @@ class PayToEnableInfo(BaseModel):
|
||||
class UserExtensionInfo(BaseModel):
|
||||
paid_to_enable: bool | None = False
|
||||
payment_hash_to_enable: str | None = None
|
||||
granted_permissions: list[str] | None = None
|
||||
granted_payment_tags: list[str] | None = None
|
||||
|
||||
|
||||
class ExtensionPermission(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
description: str
|
||||
dangerous: bool | None = False
|
||||
|
||||
|
||||
class ExtensionPermissionsGrant(BaseModel):
|
||||
permissions: list[str] = []
|
||||
payment_tags: list[str] = []
|
||||
|
||||
|
||||
class UserExtension(BaseModel):
|
||||
@@ -147,6 +166,10 @@ class Extension(BaseModel):
|
||||
short_description: str | None = None
|
||||
tile: str | None = None
|
||||
upgrade_hash: str | None = ""
|
||||
extension_type: str | None = None
|
||||
public_kv_keys: list[str] = []
|
||||
public_wasm_functions: list[str] = []
|
||||
payment_tags: list[str] = []
|
||||
|
||||
@property
|
||||
def module_name(self) -> str:
|
||||
@@ -170,6 +193,12 @@ class Extension(BaseModel):
|
||||
short_description=ext_info.short_description,
|
||||
tile=ext_info.icon,
|
||||
upgrade_hash=ext_info.hash if ext_info.ext_upgrade_dir.is_dir() else "",
|
||||
extension_type=ext_info.meta.extension_type if ext_info.meta else None,
|
||||
public_kv_keys=ext_info.meta.public_kv_keys if ext_info.meta else [],
|
||||
public_wasm_functions=(
|
||||
ext_info.meta.public_wasm_functions if ext_info.meta else []
|
||||
),
|
||||
payment_tags=ext_info.meta.payment_tags if ext_info.meta else [],
|
||||
)
|
||||
|
||||
|
||||
@@ -331,6 +360,11 @@ class ExtensionMeta(BaseModel):
|
||||
pay_to_enable: PayToEnableInfo | None = None
|
||||
payments: list[ReleasePaymentInfo] = []
|
||||
dependencies: list[str] = []
|
||||
permissions: list[ExtensionPermission] = []
|
||||
extension_type: str | None = "python"
|
||||
public_kv_keys: list[str] = []
|
||||
public_wasm_functions: list[str] = []
|
||||
payment_tags: list[str] = []
|
||||
archive: str | None = None
|
||||
featured: bool = False
|
||||
paid_features: str | None = None
|
||||
@@ -454,6 +488,14 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
self.name = config_json.get("name")
|
||||
self.short_description = config_json.get("short_description")
|
||||
if self.meta:
|
||||
self.meta.permissions = config_json.get("permissions", [])
|
||||
self.meta.extension_type = config_json.get("extension_type", "python")
|
||||
self.meta.public_kv_keys = config_json.get("public_kv_keys", [])
|
||||
self.meta.public_wasm_functions = config_json.get(
|
||||
"public_wasm_functions", []
|
||||
)
|
||||
self.meta.payment_tags = config_json.get("payment_tags", [])
|
||||
|
||||
if (
|
||||
self.meta
|
||||
@@ -572,6 +614,11 @@ class InstallableExtension(BaseModel):
|
||||
latest_release=ExtensionRelease.from_github_release(
|
||||
source_repo, latest_release
|
||||
),
|
||||
permissions=config.permissions,
|
||||
extension_type=config.extension_type,
|
||||
public_kv_keys=config.public_kv_keys,
|
||||
public_wasm_functions=config.public_wasm_functions,
|
||||
payment_tags=config.payment_tags,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -617,7 +664,14 @@ class InstallableExtension(BaseModel):
|
||||
source_repo=f"{conf_path}",
|
||||
min_lnbits_version=config_json.get("min_lnbits_version"),
|
||||
max_lnbits_version=config_json.get("max_lnbits_version"),
|
||||
)
|
||||
),
|
||||
permissions=config_json.get("permissions", []),
|
||||
extension_type=config_json.get("extension_type", "python"),
|
||||
public_kv_keys=config_json.get("public_kv_keys", []),
|
||||
public_wasm_functions=config_json.get(
|
||||
"public_wasm_functions", []
|
||||
),
|
||||
payment_tags=config_json.get("payment_tags", []),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -841,3 +895,6 @@ def icon_to_github_url(source_repo: str, path: str | None) -> str:
|
||||
_, _, *rest = path.split("/")
|
||||
tail = "/".join(rest)
|
||||
return f"https://github.com/{source_repo}/raw/main/{tail}"
|
||||
|
||||
|
||||
ExtensionConfig.update_forward_refs(ExtensionPermission=ExtensionPermission)
|
||||
|
||||
@@ -6,23 +6,16 @@ from typing import Literal
|
||||
|
||||
from fastapi import Query
|
||||
from lnurl import LnurlWithdrawResponse
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
from lnbits.db import FilterModel
|
||||
from lnbits.fiat import get_fiat_provider
|
||||
from lnbits.fiat.base import (
|
||||
FiatPaymentFailedStatus,
|
||||
FiatPaymentPendingStatus,
|
||||
FiatPaymentStatus,
|
||||
FiatPaymentSuccessStatus,
|
||||
)
|
||||
from lnbits.utils.exchange_rates import allowed_currencies
|
||||
from lnbits.wallets import get_funding_source
|
||||
from lnbits.wallets.base import (
|
||||
PaymentFailedStatus,
|
||||
PaymentPendingStatus,
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
)
|
||||
|
||||
|
||||
@@ -130,59 +123,23 @@ class Payment(BaseModel):
|
||||
"fiat_"
|
||||
)
|
||||
|
||||
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
||||
async def check_status(
|
||||
self, skip_internal_payment_notifications: bool | None = False
|
||||
) -> PaymentStatus:
|
||||
if self.is_internal:
|
||||
if self.success:
|
||||
return PaymentSuccessStatus()
|
||||
if self.failed:
|
||||
return PaymentFailedStatus()
|
||||
if self.is_in and self.fiat_provider:
|
||||
fiat_status = await self.check_fiat_status(
|
||||
skip_internal_payment_notifications
|
||||
)
|
||||
return PaymentStatus(paid=fiat_status.paid)
|
||||
return PaymentPendingStatus()
|
||||
funding_source = get_funding_source()
|
||||
if self.is_out:
|
||||
status = await funding_source.get_payment_status(self.checking_id)
|
||||
else:
|
||||
status = await funding_source.get_invoice_status(self.checking_id)
|
||||
return status
|
||||
logger.warning("payment.check_status() is deprecated.")
|
||||
from lnbits.core.services.payments import check_payment_status
|
||||
|
||||
return await check_payment_status(self, skip_internal_payment_notifications)
|
||||
|
||||
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
||||
async def check_fiat_status(
|
||||
self, skip_internal_payment_notifications: bool | None = False
|
||||
) -> FiatPaymentStatus:
|
||||
if not self.is_internal:
|
||||
return FiatPaymentPendingStatus()
|
||||
if self.success:
|
||||
return FiatPaymentSuccessStatus()
|
||||
if self.failed:
|
||||
return FiatPaymentFailedStatus()
|
||||
logger.warning("payment.check_fiat_status() is deprecated.")
|
||||
from lnbits.core.services.fiat_providers import check_fiat_status
|
||||
|
||||
if not self.fiat_provider:
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
checking_id = self.extra.get("fiat_checking_id")
|
||||
if not checking_id:
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
fiat_provider = await get_fiat_provider(self.fiat_provider)
|
||||
if not fiat_provider:
|
||||
return FiatPaymentPendingStatus()
|
||||
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
||||
|
||||
if skip_internal_payment_notifications:
|
||||
return fiat_status
|
||||
|
||||
if fiat_status.success:
|
||||
# notify receivers asynchronously
|
||||
from lnbits.tasks import internal_invoice_queue
|
||||
|
||||
await internal_invoice_queue.put(self.checking_id)
|
||||
|
||||
return fiat_status
|
||||
return await check_fiat_status(self, skip_internal_payment_notifications)
|
||||
|
||||
|
||||
class PaymentFilters(FilterModel):
|
||||
|
||||
@@ -181,12 +181,14 @@ class AccountId(BaseModel):
|
||||
|
||||
|
||||
class Account(AccountId):
|
||||
activated: bool = True
|
||||
external_id: str | None = None # for external account linking
|
||||
username: str | None = None
|
||||
password_hash: str | None = None
|
||||
pubkey: str | None = None
|
||||
email: str | None = None
|
||||
extra: UserExtra = UserExtra()
|
||||
ui_customization: dict = Field(default_factory=dict)
|
||||
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
@@ -240,6 +242,7 @@ class Account(AccountId):
|
||||
|
||||
|
||||
class AccountOverview(Account):
|
||||
activated: bool = True
|
||||
transaction_count: int | None = 0
|
||||
wallet_count: int | None = 0
|
||||
balance_msat: int | None = 0
|
||||
@@ -275,6 +278,7 @@ class AccountFilters(FilterModel):
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
activated: bool = True
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
email: str | None = None
|
||||
@@ -288,6 +292,7 @@ class User(BaseModel):
|
||||
fiat_providers: list[str] = []
|
||||
has_password: bool = False
|
||||
extra: UserExtra = UserExtra()
|
||||
ui_customization: dict = Field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def wallet_ids(self) -> list[str]:
|
||||
@@ -313,6 +318,7 @@ class RegisterUser(BaseModel):
|
||||
username: str = Query(default=..., min_length=2, max_length=20)
|
||||
password: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||
invitation_code: str | None = Query(default=None, max_length=256)
|
||||
|
||||
|
||||
class CreateUser(BaseModel):
|
||||
@@ -356,6 +362,7 @@ class UpdateSuperuserPassword(BaseModel):
|
||||
username: str = Query(default=..., min_length=2, max_length=20)
|
||||
password: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||
first_install_token: str | None = Query(None)
|
||||
|
||||
|
||||
class LoginUsr(BaseModel):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .fiat_providers import check_fiat_status
|
||||
from .funding_source import (
|
||||
get_balance_delta,
|
||||
switch_to_voidwallet,
|
||||
@@ -7,6 +8,7 @@ from .notifications import enqueue_admin_notification, send_payment_notification
|
||||
from .payments import (
|
||||
calculate_fiat_amounts,
|
||||
cancel_hold_invoice,
|
||||
check_payment_status,
|
||||
check_transaction_status,
|
||||
check_wallet_limits,
|
||||
create_fiat_invoice,
|
||||
@@ -40,6 +42,8 @@ __all__ = [
|
||||
"calculate_fiat_amounts",
|
||||
"cancel_hold_invoice",
|
||||
"check_admin_settings",
|
||||
"check_fiat_status",
|
||||
"check_payment_status",
|
||||
"check_transaction_status",
|
||||
"check_wallet_limits",
|
||||
"check_webpush_settings",
|
||||
|
||||
@@ -10,13 +10,18 @@ from lnbits.core.crud import (
|
||||
get_db_version,
|
||||
get_installed_extension,
|
||||
update_installed_extension_state,
|
||||
update_migration_version,
|
||||
)
|
||||
from lnbits.core.crud.extensions import (
|
||||
get_installed_extensions,
|
||||
update_installed_extension,
|
||||
)
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.db import Connection
|
||||
from lnbits.core.wasm.extension_host import (
|
||||
clear_schedules_for_extension,
|
||||
clear_tag_watches_for_extension,
|
||||
)
|
||||
from lnbits.db import COCKROACH, POSTGRES, Connection, Database
|
||||
from lnbits.settings import settings
|
||||
|
||||
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
||||
@@ -53,6 +58,9 @@ async def install_extension(
|
||||
else:
|
||||
await update_installed_extension(ext_info)
|
||||
|
||||
if ext_info.meta and ext_info.meta.extension_type == "wasm":
|
||||
await update_migration_version(None, ext_info.id, 0)
|
||||
|
||||
extension = Extension.from_installable_ext(ext_info)
|
||||
if extension.is_upgrade_extension:
|
||||
# call stop while the old routes are still active
|
||||
@@ -70,10 +78,26 @@ async def uninstall_extension(ext_id: str):
|
||||
|
||||
extension = await get_installed_extension(ext_id)
|
||||
if extension:
|
||||
if extension.meta and extension.meta.extension_type == "wasm":
|
||||
await clear_tag_watches_for_extension(ext_id)
|
||||
await clear_schedules_for_extension(ext_id)
|
||||
await _purge_wasm_extension_db(ext_id)
|
||||
extension.clean_extension_files()
|
||||
await delete_installed_extension(ext_id=ext_id)
|
||||
|
||||
|
||||
async def _purge_wasm_extension_db(ext_id: str) -> None:
|
||||
cleaned = await Database.clean_ext_db_files(ext_id)
|
||||
if cleaned:
|
||||
return
|
||||
try:
|
||||
db = Database(f"ext_{ext_id}")
|
||||
if db.type in {POSTGRES, COCKROACH}:
|
||||
await db.execute(f"DROP SCHEMA IF EXISTS {ext_id} CASCADE")
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to drop WASM extension schema for '{ext_id}': {exc}")
|
||||
|
||||
|
||||
async def activate_extension(ext: Extension):
|
||||
core_app_extra.register_new_ext_routes(ext)
|
||||
await update_installed_extension_state(ext_id=ext.code, active=True)
|
||||
@@ -83,6 +107,10 @@ async def activate_extension(ext: Extension):
|
||||
async def deactivate_extension(ext_id: str):
|
||||
settings.deactivate_extension_paths(ext_id)
|
||||
await update_installed_extension_state(ext_id=ext_id, active=False)
|
||||
extension = await get_installed_extension(ext_id)
|
||||
if extension and extension.meta and extension.meta.extension_type == "wasm":
|
||||
await clear_tag_watches_for_extension(ext_id)
|
||||
await clear_schedules_for_extension(ext_id)
|
||||
await stop_extension_background_work(ext_id)
|
||||
|
||||
|
||||
@@ -91,6 +119,9 @@ async def stop_extension_background_work(ext_id: str) -> bool:
|
||||
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||
Extension must expose a `myextension_stop()` function if it is starting tasks.
|
||||
"""
|
||||
installed = await get_installed_extension(ext_id)
|
||||
if installed and installed.meta and installed.meta.extension_type == "wasm":
|
||||
return True
|
||||
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
||||
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
||||
|
||||
@@ -123,6 +154,9 @@ async def start_extension_background_work(ext_id: str) -> bool:
|
||||
Extension CAN expose a `myextension_start()` function if it is starting tasks.
|
||||
Extension MUST expose a `myextension_stop()` in that case.
|
||||
"""
|
||||
installed = await get_installed_extension(ext_id)
|
||||
if installed and installed.meta and installed.meta.extension_type == "wasm":
|
||||
return True
|
||||
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
||||
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ from lnbits.core.models import CreatePayment, Payment, PaymentState
|
||||
from lnbits.core.models.misc import SimpleStatus
|
||||
from lnbits.db import Connection
|
||||
from lnbits.fiat import get_fiat_provider
|
||||
from lnbits.fiat.base import (
|
||||
FiatPaymentFailedStatus,
|
||||
FiatPaymentPendingStatus,
|
||||
FiatPaymentStatus,
|
||||
FiatPaymentSuccessStatus,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
|
||||
|
||||
@@ -29,6 +35,40 @@ async def handle_fiat_payment_confirmation(
|
||||
logger.warning(e)
|
||||
|
||||
|
||||
async def check_fiat_status(
|
||||
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
||||
) -> FiatPaymentStatus:
|
||||
if not payment.is_internal:
|
||||
return FiatPaymentPendingStatus()
|
||||
if payment.success:
|
||||
return FiatPaymentSuccessStatus()
|
||||
if payment.failed:
|
||||
return FiatPaymentFailedStatus()
|
||||
|
||||
if not payment.fiat_provider:
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
checking_id = payment.extra.get("fiat_checking_id")
|
||||
if not checking_id:
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
fiat_provider = await get_fiat_provider(payment.fiat_provider)
|
||||
if not fiat_provider:
|
||||
return FiatPaymentPendingStatus()
|
||||
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
||||
|
||||
if skip_internal_payment_notifications:
|
||||
return fiat_status
|
||||
|
||||
if fiat_status.success:
|
||||
# notify receivers asynchronously
|
||||
from lnbits.tasks import internal_invoice_queue
|
||||
|
||||
await internal_invoice_queue.put(payment.checking_id)
|
||||
|
||||
return fiat_status
|
||||
|
||||
|
||||
def check_stripe_signature(
|
||||
payload: bytes,
|
||||
sig_header: str | None,
|
||||
@@ -78,8 +118,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
|
||||
"""
|
||||
webhook_id = settings.paypal_webhook_id
|
||||
if not webhook_id:
|
||||
logger.warning("PayPal webhook ID not set; skipping verification.")
|
||||
return
|
||||
logger.warning("PayPal webhook ID not set.")
|
||||
raise ValueError("PayPal webhook cannot be verified. Missing webhook ID.")
|
||||
|
||||
required_headers = {
|
||||
"PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"),
|
||||
@@ -89,8 +129,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
|
||||
"PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"),
|
||||
}
|
||||
if not all(required_headers.values()):
|
||||
logger.warning("Missing PayPal webhook headers; skipping verification.")
|
||||
return
|
||||
logger.warning("Missing PayPal webhook headers.")
|
||||
raise ValueError("PayPal webhook cannot be verified. Missing headers.")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client:
|
||||
|
||||
@@ -313,6 +313,11 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
||||
payment.payment_hash,
|
||||
json.dumps({"pending": payment.pending, "status": payment.status}),
|
||||
)
|
||||
if payment.tag and not payment.is_out:
|
||||
await websocket_manager.send(
|
||||
f"tag:{wallet.id}:{payment.tag}",
|
||||
payment.json(),
|
||||
)
|
||||
|
||||
|
||||
async def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
||||
|
||||
@@ -25,6 +25,7 @@ from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount
|
||||
from lnbits.wallets import fake_wallet, get_funding_source
|
||||
from lnbits.wallets.base import (
|
||||
InvoiceResponse,
|
||||
PaymentFailedStatus,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
@@ -47,6 +48,7 @@ from ..models import (
|
||||
PaymentState,
|
||||
Wallet,
|
||||
)
|
||||
from .fiat_providers import check_fiat_status
|
||||
from .notifications import send_payment_notification_in_background
|
||||
|
||||
payment_lock = asyncio.Lock()
|
||||
@@ -364,7 +366,7 @@ async def update_pending_payments(wallet_id: str):
|
||||
async def update_pending_payment(
|
||||
payment: Payment, conn: Connection | None = None
|
||||
) -> Payment:
|
||||
status = await payment.check_status()
|
||||
status = await check_payment_status(payment)
|
||||
if status.failed:
|
||||
payment.status = PaymentState.FAILED
|
||||
await update_payment(payment, conn=conn)
|
||||
@@ -618,7 +620,29 @@ async def check_transaction_status(
|
||||
if payment.status == PaymentState.SUCCESS.value:
|
||||
return PaymentSuccessStatus(fee_msat=payment.fee)
|
||||
|
||||
return await payment.check_status()
|
||||
return await check_payment_status(payment)
|
||||
|
||||
|
||||
async def check_payment_status(
|
||||
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
||||
) -> PaymentStatus:
|
||||
if payment.is_internal:
|
||||
if payment.success:
|
||||
return PaymentSuccessStatus()
|
||||
if payment.failed:
|
||||
return PaymentFailedStatus()
|
||||
if payment.is_in and payment.fiat_provider:
|
||||
fiat_status = await check_fiat_status(
|
||||
payment, skip_internal_payment_notifications
|
||||
)
|
||||
return PaymentStatus(paid=fiat_status.paid)
|
||||
return PaymentPendingStatus()
|
||||
funding_source = get_funding_source()
|
||||
if payment.is_out:
|
||||
status = await funding_source.get_payment_status(payment.checking_id)
|
||||
else:
|
||||
status = await funding_source.get_invoice_status(payment.checking_id)
|
||||
return status
|
||||
|
||||
|
||||
async def get_payments_daily_stats(
|
||||
@@ -752,7 +776,7 @@ async def _pay_internal_invoice(
|
||||
await update_payment(internal_payment, conn=conn)
|
||||
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
||||
|
||||
send_payment_notification_in_background(wallet, payment)
|
||||
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
||||
|
||||
# notify receiver asynchronously
|
||||
from lnbits.tasks import internal_invoice_queue
|
||||
@@ -825,7 +849,8 @@ async def _pay_external_invoice(
|
||||
payment = await update_payment_success_status(
|
||||
payment, payment_response, conn=conn
|
||||
)
|
||||
send_payment_notification_in_background(wallet, payment)
|
||||
|
||||
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
||||
logger.success(f"payment successful {payment_response.checking_id}")
|
||||
|
||||
payment.checking_id = payment_response.checking_id
|
||||
@@ -868,7 +893,7 @@ async def _verify_external_payment(
|
||||
raise PaymentError("Payment already paid.", status="success")
|
||||
|
||||
# payment failed
|
||||
status = await payment.check_status()
|
||||
status = await check_payment_status(payment)
|
||||
if status.failed:
|
||||
raise PaymentError(
|
||||
"Payment is failed node, retrying is not possible.", status="failed"
|
||||
@@ -1033,3 +1058,13 @@ async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
|
||||
await update_payment(payment)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def _send_payment_notification_in_background(
|
||||
wallet_id: str, payment: Payment, conn: Connection | None = None
|
||||
):
|
||||
# fetch balance again
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if not wallet:
|
||||
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||
send_payment_notification_in_background(wallet, payment)
|
||||
|
||||
@@ -3,8 +3,10 @@ from uuid import uuid4
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.crud.settings import set_settings_field
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.extensions import UserExtension
|
||||
from lnbits.core.models.users import RegisterUser
|
||||
from lnbits.db import Connection
|
||||
from lnbits.settings import (
|
||||
EditableSettings,
|
||||
@@ -192,3 +194,25 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
||||
|
||||
editable_settings = EditableSettings.from_dict(settings.dict())
|
||||
return await create_admin_settings(account.id, editable_settings.dict())
|
||||
|
||||
|
||||
async def check_register_activation_settings(data: RegisterUser):
|
||||
if not settings.lnbits_require_user_activation:
|
||||
return None
|
||||
if settings.lnbits_user_activation_by_invitation_code:
|
||||
code = data.invitation_code.strip() if data.invitation_code else ""
|
||||
if len(code) == 0:
|
||||
raise ValueError("Invitation code cannot be empty.")
|
||||
|
||||
if code == settings.lnbits_register_reusable_activation_code:
|
||||
return None
|
||||
if code in settings.lnbits_register_one_time_activation_codes:
|
||||
settings.lnbits_register_one_time_activation_codes.remove(code)
|
||||
await set_settings_field(
|
||||
"lnbits_register_one_time_activation_codes",
|
||||
settings.lnbits_register_one_time_activation_codes,
|
||||
)
|
||||
return None
|
||||
raise ValueError("Invalid invitation code.")
|
||||
|
||||
raise ValueError("No activation method provided.")
|
||||
|
||||
@@ -26,7 +26,10 @@ from lnbits.core.models.users import (
|
||||
UpdateAccessControlList,
|
||||
)
|
||||
from lnbits.core.services import create_user_account
|
||||
from lnbits.core.services.users import update_user_account
|
||||
from lnbits.core.services.users import (
|
||||
check_register_activation_settings,
|
||||
update_user_account,
|
||||
)
|
||||
from lnbits.decorators import (
|
||||
access_token_payload,
|
||||
check_account_exists,
|
||||
@@ -86,6 +89,7 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
||||
account = await get_account_by_username_or_email(data.username)
|
||||
if not account or not account.verify_password(data.password):
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||
|
||||
return _auth_success_response(account.username, account.id, account.email)
|
||||
|
||||
|
||||
@@ -94,7 +98,7 @@ async def nostr_login(request: Request) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
||||
event = _nostr_nip98_event(request)
|
||||
account = await get_account_by_pubkey(event["pubkey"])
|
||||
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
|
||||
if not account:
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
@@ -102,6 +106,8 @@ async def nostr_login(request: Request) -> JSONResponse:
|
||||
extra=UserExtra(provider="nostr"),
|
||||
)
|
||||
await create_user_account(account)
|
||||
if not account.activated:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
|
||||
return _auth_success_response(account.username or "", account.id, account.email)
|
||||
|
||||
|
||||
@@ -357,12 +363,14 @@ async def register(data: RegisterUser) -> JSONResponse:
|
||||
if not is_valid_username(data.username):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
||||
|
||||
if await get_account_by_username(data.username):
|
||||
if await get_account_by_username(data.username, active_only=False):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||
|
||||
if data.email and not is_valid_email_address(data.email):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||
|
||||
await check_register_activation_settings(data)
|
||||
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
email=data.email,
|
||||
@@ -467,7 +475,7 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
||||
return _auth_success_response(account.username, user_id, account.email)
|
||||
|
||||
|
||||
@auth_router.put("/update")
|
||||
@auth_router.patch("")
|
||||
async def update(
|
||||
data: UpdateUser, account: Account = Depends(check_account_exists)
|
||||
) -> User | None:
|
||||
@@ -483,13 +491,41 @@ async def update(
|
||||
return await get_user_from_account(account)
|
||||
|
||||
|
||||
@auth_router.patch("/ui")
|
||||
async def update_ui_customization(
|
||||
req: Request, account: Account = Depends(check_account_exists)
|
||||
) -> Account:
|
||||
ui_customization = await req.json()
|
||||
account.ui_customization = {**(account.ui_customization or {}), **ui_customization}
|
||||
|
||||
if len(account.ui_customization or {}) > 1000 * 1024:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST, "UI customization too large. Drop some fields."
|
||||
)
|
||||
|
||||
await update_user_account(account)
|
||||
return account
|
||||
|
||||
|
||||
@auth_router.put("/first_install")
|
||||
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||
if not settings.first_install:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
|
||||
|
||||
if settings.first_install_token:
|
||||
if not data.first_install_token:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.")
|
||||
if settings.first_install_token != data.first_install_token:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid first_install_token.")
|
||||
|
||||
account = await get_account_by_username(data.username, False)
|
||||
if account:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||
|
||||
account = await get_account(settings.super_user)
|
||||
if not account:
|
||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
||||
|
||||
account.username = data.username
|
||||
account.extra = account.extra or UserExtra()
|
||||
account.extra.provider = "lnbits"
|
||||
@@ -505,7 +541,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||
|
||||
redirect_path = "/wallet"
|
||||
account = await get_account_by_email(email)
|
||||
account = await get_account_by_email(email, active_only=False)
|
||||
|
||||
if verified_user_id:
|
||||
if account:
|
||||
|
||||
@@ -9,6 +9,7 @@ from lnbits.core.crud.payments import (
|
||||
from lnbits.core.models.misc import SimpleStatus
|
||||
from lnbits.core.models.payments import CreateInvoice
|
||||
from lnbits.core.services.fiat_providers import (
|
||||
check_fiat_status,
|
||||
check_stripe_signature,
|
||||
verify_paypal_webhook,
|
||||
)
|
||||
@@ -23,7 +24,7 @@ callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
|
||||
async def api_generic_webhook_handler(
|
||||
provider_name: str, request: Request
|
||||
) -> SimpleStatus:
|
||||
|
||||
logger.info(f"Received callback from provider: '{provider_name}'.")
|
||||
if provider_name.lower() == "stripe":
|
||||
payload = await request.body()
|
||||
sig_header = request.headers.get("Stripe-Signature")
|
||||
@@ -87,7 +88,7 @@ async def _handle_stripe_intent_session_completed(event: dict):
|
||||
if not payment:
|
||||
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||
return
|
||||
await payment.check_fiat_status()
|
||||
await check_fiat_status(payment)
|
||||
|
||||
|
||||
async def _handle_stripe_checkout_session_completed(event: dict):
|
||||
@@ -110,7 +111,7 @@ async def _handle_stripe_checkout_session_completed(event: dict):
|
||||
payment = await get_standalone_payment(payment_hash)
|
||||
if not payment:
|
||||
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
|
||||
await payment.check_fiat_status()
|
||||
await check_fiat_status(payment)
|
||||
|
||||
|
||||
async def _handle_stripe_subscription_invoice_paid(event: dict):
|
||||
@@ -155,7 +156,7 @@ async def _handle_stripe_subscription_invoice_paid(event: dict):
|
||||
),
|
||||
)
|
||||
|
||||
await payment.check_fiat_status()
|
||||
await check_fiat_status(payment)
|
||||
|
||||
|
||||
async def _get_stripe_subscription_payment_options(
|
||||
@@ -180,8 +181,10 @@ async def _get_stripe_subscription_payment_options(
|
||||
|
||||
|
||||
async def handle_paypal_event(event: dict):
|
||||
event_id = event.get("id", "")
|
||||
event_type = event.get("event_type", "")
|
||||
resource = event.get("resource", {})
|
||||
logger.info(f"Handling PayPal event: '{event_id}'. Type: '{event_type}'.")
|
||||
|
||||
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
|
||||
payment_hash = _paypal_extract_payment_hash(resource)
|
||||
@@ -192,23 +195,20 @@ async def handle_paypal_event(event: dict):
|
||||
if not payment:
|
||||
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||
return
|
||||
await payment.check_fiat_status()
|
||||
await check_fiat_status(payment)
|
||||
return
|
||||
|
||||
if event_type in (
|
||||
"PAYMENT.SALE.COMPLETED",
|
||||
"BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED",
|
||||
):
|
||||
if event_type in ("PAYMENT.SALE.COMPLETED"):
|
||||
await _handle_paypal_subscription_payment(resource)
|
||||
return
|
||||
|
||||
logger.info(f"Unhandled PayPal event type: '{event_type}'.")
|
||||
logger.warning(f"Unhandled PayPal event type: '{event_type}'.")
|
||||
|
||||
|
||||
async def _handle_paypal_subscription_payment(resource: dict):
|
||||
amount_info = resource.get("amount") or {}
|
||||
currency = (amount_info.get("currency_code") or "").upper()
|
||||
total = amount_info.get("value")
|
||||
currency = (amount_info.get("currency") or "").upper()
|
||||
total = amount_info.get("total")
|
||||
if not currency or total is None:
|
||||
raise ValueError("PayPal subscription event missing amount.")
|
||||
|
||||
@@ -216,18 +216,14 @@ async def _handle_paypal_subscription_payment(resource: dict):
|
||||
if not custom_id:
|
||||
raise ValueError("PayPal subscription event missing custom metadata.")
|
||||
|
||||
try:
|
||||
metadata = json.loads(custom_id)
|
||||
except json.JSONDecodeError:
|
||||
metadata = {}
|
||||
|
||||
payment_options = FiatSubscriptionPaymentOptions(**metadata)
|
||||
payment_options = _deserialize_paypal_metadata(custom_id)
|
||||
if not payment_options.wallet_id:
|
||||
raise ValueError("PayPal subscription event missing wallet_id.")
|
||||
|
||||
memo = payment_options.memo or ""
|
||||
extra = {
|
||||
**(payment_options.extra or {}),
|
||||
"subscription_request_id": resource.get("billing_agreement_id"),
|
||||
"fiat_method": "subscription",
|
||||
"tag": payment_options.tag,
|
||||
"subscription": {
|
||||
@@ -247,7 +243,7 @@ async def _handle_paypal_subscription_payment(resource: dict):
|
||||
),
|
||||
)
|
||||
|
||||
await payment.check_fiat_status()
|
||||
await check_fiat_status(payment)
|
||||
|
||||
|
||||
def _paypal_extract_payment_hash(resource: dict) -> str | None:
|
||||
@@ -258,3 +254,29 @@ def _paypal_extract_payment_hash(resource: dict) -> str | None:
|
||||
if pu.get("custom_id"):
|
||||
return pu.get("custom_id")
|
||||
return None
|
||||
|
||||
|
||||
def _deserialize_paypal_metadata(custom_id: str) -> FiatSubscriptionPaymentOptions:
|
||||
try:
|
||||
meta = json.loads(custom_id)
|
||||
wallet_id = meta[0] if len(meta) > 0 else None
|
||||
tag = meta[1] if len(meta) > 1 else None
|
||||
subscription_request_id = meta[2] if len(meta) > 2 else None
|
||||
extra_link = meta[3] if len(meta) > 3 else None
|
||||
memo = meta[4] if len(meta) > 4 else None
|
||||
|
||||
extra = {
|
||||
"link": extra_link,
|
||||
"subscription_request_id": subscription_request_id,
|
||||
}
|
||||
|
||||
return FiatSubscriptionPaymentOptions(
|
||||
wallet_id=wallet_id,
|
||||
tag=tag,
|
||||
subscription_request_id=subscription_request_id,
|
||||
extra=extra,
|
||||
memo=memo,
|
||||
)
|
||||
except (json.JSONDecodeError, IndexError) as e:
|
||||
logger.warning(f"Failed to deserialize PayPal metadata: {e}")
|
||||
return FiatSubscriptionPaymentOptions()
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from fastapi.requests import Request
|
||||
from loguru import logger
|
||||
from starlette.routing import Match
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_extensions
|
||||
from lnbits.core.crud.wallets import get_wallets_ids
|
||||
@@ -20,6 +23,8 @@ from lnbits.core.models.extensions import (
|
||||
Extension,
|
||||
ExtensionConfig,
|
||||
ExtensionMeta,
|
||||
ExtensionPermission,
|
||||
ExtensionPermissionsGrant,
|
||||
ExtensionRelease,
|
||||
ExtensionReview,
|
||||
ExtensionReviewPaymentRequest,
|
||||
@@ -40,6 +45,11 @@ from lnbits.core.services.extensions import (
|
||||
install_extension,
|
||||
uninstall_extension,
|
||||
)
|
||||
from lnbits.core.wasm import WASM_HOST_MANIFEST
|
||||
from lnbits.core.wasm.extension_host import (
|
||||
clear_schedules_for_user,
|
||||
clear_tag_watches_for_user,
|
||||
)
|
||||
from lnbits.db import Page
|
||||
from lnbits.decorators import (
|
||||
check_account_exists,
|
||||
@@ -174,38 +184,146 @@ async def api_update_pay_to_enable(
|
||||
|
||||
@extension_router.put("/{ext_id}/enable")
|
||||
async def api_enable_extension(
|
||||
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||
ext_id: str,
|
||||
request: Request,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
grant: ExtensionPermissionsGrant | None = Body(default=None),
|
||||
) -> SimpleStatus:
|
||||
await _ensure_extension_exists(ext_id)
|
||||
logger.info(f"Enabling extension: {ext_id}.")
|
||||
|
||||
ext = await _get_installed_active_extension(ext_id)
|
||||
user_ext = await _get_or_create_user_extension(account_id.id, ext_id)
|
||||
|
||||
required_permissions = _get_required_permissions(ext_id, ext)
|
||||
granted_permissions = _get_granted_permissions(grant, user_ext)
|
||||
granted_tags = _get_granted_payment_tags(grant, user_ext)
|
||||
required_tags = _get_required_payment_tags(ext_id, ext)
|
||||
if _is_wasm_extension(ext_id, ext):
|
||||
_ensure_api_permissions_available(
|
||||
request, _merge_permissions(required_permissions, granted_permissions)
|
||||
)
|
||||
_ensure_payment_tags_allowed(required_tags, granted_tags)
|
||||
_ensure_permissions(required_permissions, granted_permissions)
|
||||
|
||||
if grant and grant.permissions:
|
||||
await _store_granted_permissions(user_ext, granted_permissions)
|
||||
if grant and grant.payment_tags:
|
||||
await _store_granted_payment_tags(user_ext, granted_tags)
|
||||
|
||||
if account_id.is_admin_id or not ext.requires_payment:
|
||||
await _activate_user_extension(user_ext)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
||||
|
||||
return await _enable_paid_extension(ext_id, ext, user_ext)
|
||||
|
||||
|
||||
async def _ensure_extension_exists(ext_id: str) -> None:
|
||||
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
|
||||
)
|
||||
|
||||
logger.info(f"Enabling extension: {ext_id}.")
|
||||
|
||||
async def _get_installed_active_extension(ext_id: str) -> InstallableExtension:
|
||||
ext = await get_installed_extension(ext_id)
|
||||
if not ext:
|
||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||
if not ext.active:
|
||||
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||
return ext
|
||||
|
||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||
|
||||
async def _get_or_create_user_extension(user_id: str, ext_id: str) -> UserExtension:
|
||||
user_ext = await get_user_extension(user_id, ext_id)
|
||||
if not user_ext:
|
||||
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||
user_ext = UserExtension(user=user_id, extension=ext_id, active=False)
|
||||
await create_user_extension(user_ext)
|
||||
return user_ext
|
||||
|
||||
if account_id.is_admin_id or not ext.requires_payment:
|
||||
user_ext.active = True
|
||||
await update_user_extension(user_ext)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
||||
|
||||
def _get_required_permissions(ext_id: str, ext: InstallableExtension) -> list[str]:
|
||||
permissions_source = (
|
||||
ext.meta.permissions
|
||||
if ext.meta and ext.meta.permissions
|
||||
else _load_permissions_from_config(ext_id)
|
||||
)
|
||||
return [p.id for p in permissions_source] if permissions_source else []
|
||||
|
||||
|
||||
def _get_granted_permissions(
|
||||
grant: ExtensionPermissionsGrant | None, user_ext: UserExtension
|
||||
) -> list[str]:
|
||||
if grant and grant.permissions:
|
||||
return grant.permissions
|
||||
if user_ext.extra and user_ext.extra.granted_permissions:
|
||||
return user_ext.extra.granted_permissions
|
||||
return []
|
||||
|
||||
|
||||
def _get_required_payment_tags(ext_id: str, ext: InstallableExtension) -> list[str]:
|
||||
tags_source = (
|
||||
ext.meta.payment_tags
|
||||
if ext.meta and ext.meta.payment_tags
|
||||
else _load_payment_tags_from_config(ext_id)
|
||||
)
|
||||
return tags_source if tags_source else []
|
||||
|
||||
|
||||
def _get_granted_payment_tags(
|
||||
grant: ExtensionPermissionsGrant | None, user_ext: UserExtension
|
||||
) -> list[str]:
|
||||
if grant and grant.payment_tags:
|
||||
return grant.payment_tags
|
||||
if user_ext.extra and user_ext.extra.granted_payment_tags:
|
||||
return user_ext.extra.granted_payment_tags
|
||||
return []
|
||||
|
||||
|
||||
def _ensure_permissions(required: list[str], granted: list[str]) -> None:
|
||||
if not required:
|
||||
return
|
||||
missing = [p for p in required if p not in granted]
|
||||
if missing:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Missing required permissions to enable this extension.",
|
||||
)
|
||||
|
||||
|
||||
async def _store_granted_permissions(
|
||||
user_ext: UserExtension, granted_permissions: list[str]
|
||||
) -> None:
|
||||
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||
user_ext_info.granted_permissions = granted_permissions
|
||||
user_ext.extra = user_ext_info
|
||||
await update_user_extension(user_ext)
|
||||
|
||||
|
||||
async def _store_granted_payment_tags(
|
||||
user_ext: UserExtension, granted_tags: list[str]
|
||||
) -> None:
|
||||
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||
user_ext_info.granted_payment_tags = granted_tags
|
||||
user_ext.extra = user_ext_info
|
||||
await update_user_extension(user_ext)
|
||||
|
||||
|
||||
async def _activate_user_extension(user_ext: UserExtension) -> None:
|
||||
user_ext.active = True
|
||||
await update_user_extension(user_ext)
|
||||
|
||||
|
||||
async def _enable_paid_extension(
|
||||
ext_id: str, ext: InstallableExtension, user_ext: UserExtension
|
||||
) -> SimpleStatus:
|
||||
if not (user_ext.extra and user_ext.extra.payment_hash_to_enable):
|
||||
raise HTTPException(
|
||||
HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment."
|
||||
)
|
||||
|
||||
if user_ext.is_paid:
|
||||
user_ext.active = True
|
||||
await update_user_extension(user_ext)
|
||||
await _activate_user_extension(user_ext)
|
||||
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
||||
|
||||
if not ext.meta or not ext.meta.pay_to_enable or not ext.meta.pay_to_enable.wallet:
|
||||
@@ -222,9 +340,8 @@ async def api_enable_extension(
|
||||
f"Invoice generated but not paid for enabeling extension '{ext_id}'.",
|
||||
)
|
||||
|
||||
user_ext.active = True
|
||||
user_ext.extra.paid_to_enable = True
|
||||
await update_user_extension(user_ext)
|
||||
await _activate_user_extension(user_ext)
|
||||
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
||||
|
||||
|
||||
@@ -243,10 +360,117 @@ async def api_disable_extension(
|
||||
)
|
||||
logger.info(f"Disabling extension: {ext_id}.")
|
||||
user_ext.active = False
|
||||
if user_ext.extra and user_ext.extra.granted_permissions:
|
||||
user_ext.extra.granted_permissions = []
|
||||
if user_ext.extra and user_ext.extra.granted_payment_tags:
|
||||
user_ext.extra.granted_payment_tags = []
|
||||
await update_user_extension(user_ext)
|
||||
ext = await get_installed_extension(ext_id)
|
||||
if ext and ext.meta and ext.meta.extension_type == "wasm":
|
||||
await clear_tag_watches_for_user(ext_id, account_id.id)
|
||||
await clear_schedules_for_user(ext_id, account_id.id)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/permissions")
|
||||
async def api_update_extension_permissions(
|
||||
ext_id: str,
|
||||
request: Request,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
grant: ExtensionPermissionsGrant | None = Body(default=None),
|
||||
) -> SimpleStatus:
|
||||
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
|
||||
)
|
||||
|
||||
ext = await get_installed_extension(ext_id)
|
||||
if not ext:
|
||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||
if not ext.active:
|
||||
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||
|
||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||
if not user_ext:
|
||||
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||
await create_user_extension(user_ext)
|
||||
|
||||
permissions_source = (
|
||||
ext.meta.permissions
|
||||
if ext.meta and ext.meta.permissions
|
||||
else _load_permissions_from_config(ext_id)
|
||||
)
|
||||
required_permissions = (
|
||||
[p.id for p in permissions_source] if permissions_source else []
|
||||
)
|
||||
granted_permissions = grant.permissions if grant and grant.permissions else []
|
||||
granted_tags = grant.payment_tags if grant and grant.payment_tags else []
|
||||
if _is_wasm_extension(ext_id, ext):
|
||||
_ensure_api_permissions_available(
|
||||
request, _merge_permissions(required_permissions, granted_permissions)
|
||||
)
|
||||
_ensure_payment_tags_allowed(
|
||||
_get_required_payment_tags(ext_id, ext), granted_tags
|
||||
)
|
||||
|
||||
if required_permissions:
|
||||
missing = [p for p in required_permissions if p not in granted_permissions]
|
||||
if missing:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Missing required permissions to save for this extension.",
|
||||
)
|
||||
|
||||
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||
user_ext_info.granted_permissions = granted_permissions
|
||||
user_ext_info.granted_payment_tags = granted_tags
|
||||
user_ext.extra = user_ext_info
|
||||
await update_user_extension(user_ext)
|
||||
|
||||
return SimpleStatus(success=True, message=f"Permissions saved for '{ext_id}'.")
|
||||
|
||||
|
||||
@extension_router.get("/{ext_id}/capabilities")
|
||||
async def api_extension_capabilities(
|
||||
ext_id: str,
|
||||
request: Request,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
) -> dict:
|
||||
await _ensure_extension_exists(ext_id)
|
||||
ext = await get_installed_extension(ext_id)
|
||||
if not ext:
|
||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||
|
||||
permissions_source = (
|
||||
ext.meta.permissions
|
||||
if ext.meta and ext.meta.permissions
|
||||
else _load_permissions_from_config(ext_id)
|
||||
)
|
||||
permissions = [p.id for p in permissions_source] if permissions_source else []
|
||||
missing = (
|
||||
_missing_api_permissions(request, permissions)
|
||||
if _is_wasm_extension(ext_id, ext)
|
||||
else []
|
||||
)
|
||||
payment_tags = _get_required_payment_tags(ext_id, ext)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"extension": ext_id,
|
||||
"is_wasm": _is_wasm_extension(ext_id, ext),
|
||||
"permissions": permissions,
|
||||
"missing_permissions": missing,
|
||||
"payment_tags": payment_tags,
|
||||
}
|
||||
|
||||
|
||||
@extension_router.get("/wasm/manifest")
|
||||
async def api_wasm_manifest(
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
) -> dict:
|
||||
return WASM_HOST_MANIFEST
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/activate", dependencies=[Depends(check_admin)])
|
||||
async def api_activate_extension(ext_id: str) -> SimpleStatus:
|
||||
try:
|
||||
@@ -519,6 +743,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
installed_exts: list[InstallableExtension] = await get_installed_extensions(
|
||||
conn=conn
|
||||
)
|
||||
user_exts = await get_user_extensions(account_id.id, conn=conn)
|
||||
all_ext_ids = [ext.code for ext in await get_valid_extensions(conn=conn)]
|
||||
inactive_extensions = [
|
||||
e.id for e in await get_installed_extensions(active=False, conn=conn)
|
||||
@@ -526,6 +751,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
db_versions = await get_db_versions(conn=conn)
|
||||
|
||||
installed_exts_ids = [e.id for e in installed_exts]
|
||||
user_exts_map = {e.extension: e for e in user_exts}
|
||||
|
||||
installable_exts = await InstallableExtension.get_installable_extensions(
|
||||
post_refresh_cache=account_id.is_admin_id
|
||||
@@ -554,6 +780,10 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
e.name = installed_ext.name
|
||||
e.short_description = installed_ext.short_description
|
||||
e.icon = installed_ext.icon
|
||||
if e.meta and not e.meta.permissions:
|
||||
e.meta.permissions = _load_permissions_from_config(e.id)
|
||||
if e.meta and not e.meta.payment_tags:
|
||||
e.meta.payment_tags = _load_payment_tags_from_config(e.id)
|
||||
|
||||
extension_data = [
|
||||
{
|
||||
@@ -571,6 +801,27 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
"isAvailable": ext.id in all_ext_ids,
|
||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||
"isActive": ext.id not in inactive_extensions,
|
||||
"permissions": (
|
||||
[dict(p) for p in ext.meta.permissions]
|
||||
if ext.meta and ext.meta.permissions
|
||||
else [dict(p) for p in _load_permissions_from_config(ext.id)]
|
||||
),
|
||||
"paymentTags": (
|
||||
ext.meta.payment_tags
|
||||
if ext.meta and ext.meta.payment_tags
|
||||
else _load_payment_tags_from_config(ext.id)
|
||||
),
|
||||
"kvSchema": _load_kv_schema_from_config(ext.id),
|
||||
"grantedPermissions": (
|
||||
user_ext.extra.granted_permissions
|
||||
if (user_ext := user_exts_map.get(ext.id)) and user_ext.extra
|
||||
else []
|
||||
),
|
||||
"grantedPaymentTags": (
|
||||
user_ext.extra.granted_payment_tags
|
||||
if (user_ext := user_exts_map.get(ext.id)) and user_ext.extra
|
||||
else []
|
||||
),
|
||||
"latestRelease": (
|
||||
dict(ext.meta.latest_release)
|
||||
if ext.meta and ext.meta.latest_release
|
||||
@@ -598,6 +849,162 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
return extension_data
|
||||
|
||||
|
||||
def _load_permissions_from_config(ext_id: str) -> list[ExtensionPermission]:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return []
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
permissions = config_json.get("permissions", [])
|
||||
return [ExtensionPermission(**p) for p in permissions]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _load_kv_schema_from_config(ext_id: str) -> dict:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return {}
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
schema = config_json.get("kv_schema", {})
|
||||
return schema if isinstance(schema, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _load_payment_tags_from_config(ext_id: str) -> list[str]:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return []
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
tags = config_json.get("payment_tags", [])
|
||||
return tags if isinstance(tags, list) else []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _merge_permissions(required: list[str], granted: list[str]) -> list[str]:
|
||||
merged = set()
|
||||
for perm in required or []:
|
||||
merged.add(perm)
|
||||
for perm in granted or []:
|
||||
merged.add(perm)
|
||||
return list(merged)
|
||||
|
||||
|
||||
def _ensure_payment_tags_allowed(required: list[str], granted: list[str]) -> None:
|
||||
if not required and granted:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"This extension does not declare any payment tags.",
|
||||
)
|
||||
if required and not granted:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Select at least one payment tag before enabling this extension.",
|
||||
)
|
||||
if not required or not granted:
|
||||
return
|
||||
invalid = [t for t in granted if t not in required]
|
||||
if invalid:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
f"Invalid payment tags requested: {', '.join(invalid)}",
|
||||
)
|
||||
|
||||
|
||||
def _ensure_api_permissions_available(request: Request, permissions: list[str]) -> None:
|
||||
if not permissions:
|
||||
return
|
||||
missing = []
|
||||
for perm in permissions:
|
||||
parsed = _parse_api_permission(perm)
|
||||
if not parsed:
|
||||
continue
|
||||
method, path = parsed
|
||||
if not _route_exists(request, method, path):
|
||||
missing.append(perm)
|
||||
if missing:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
f"Permissions reference missing endpoints: {', '.join(missing)}",
|
||||
)
|
||||
|
||||
|
||||
def _parse_api_permission(perm: str) -> tuple[str, str] | None:
|
||||
if not perm.startswith("api."):
|
||||
return None
|
||||
try:
|
||||
method_part, path = perm.split(":", 1)
|
||||
method = method_part.replace("api.", "").upper()
|
||||
except ValueError:
|
||||
return None
|
||||
if not path.startswith("/"):
|
||||
return None
|
||||
return method, path
|
||||
|
||||
|
||||
def _missing_api_permissions(request: Request, permissions: list[str]) -> list[str]:
|
||||
missing = []
|
||||
for perm in permissions or []:
|
||||
parsed = _parse_api_permission(perm)
|
||||
if not parsed:
|
||||
continue
|
||||
method, path = parsed
|
||||
if not _route_exists(request, method, path):
|
||||
missing.append(perm)
|
||||
return missing
|
||||
|
||||
|
||||
def _route_exists(request: Request, method: str, path: str) -> bool:
|
||||
scope = {
|
||||
"type": "http",
|
||||
"method": method,
|
||||
"path": path,
|
||||
"root_path": "",
|
||||
"headers": [],
|
||||
}
|
||||
for route in request.app.router.routes:
|
||||
methods = getattr(route, "methods", None)
|
||||
if methods and method not in methods:
|
||||
continue
|
||||
try:
|
||||
match, _ = route.matches(scope)
|
||||
except Exception as exc:
|
||||
logger.debug(f"Route match failed for {method} {path}: {exc!s}")
|
||||
continue
|
||||
if match == Match.FULL:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_wasm_extension(ext_id: str, ext: InstallableExtension) -> bool:
|
||||
if ext.meta and ext.meta.extension_type == "wasm":
|
||||
return True
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return False
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
return config_json.get("extension_type") == "wasm"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@extension_router.get(
|
||||
"/reviews/tags",
|
||||
dependencies=[Depends(check_account_exists)],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.models.misc import SimpleStatus
|
||||
from lnbits.core.models.wallets import WalletTypeInfo
|
||||
@@ -30,6 +31,11 @@ async def create_subscription(
|
||||
data: CreateFiatSubscription,
|
||||
key_type: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> FiatSubscriptionResponse:
|
||||
logger.debug(
|
||||
f"Creating subscription with provider '{provider}. '"
|
||||
f"Subscription ID '{data.subscription_id}'."
|
||||
)
|
||||
|
||||
fiat_provider = await get_fiat_provider(provider)
|
||||
if not fiat_provider:
|
||||
raise HTTPException(404, "Fiat provider not found")
|
||||
@@ -47,6 +53,12 @@ async def create_subscription(
|
||||
subscription_response = await fiat_provider.create_subscription(
|
||||
data.subscription_id, data.quantity, data.payment_options
|
||||
)
|
||||
if subscription_response.error_message:
|
||||
logger.warning(
|
||||
f"Failed to create subscription with provider '{provider}': "
|
||||
f"{subscription_response.error_message}"
|
||||
)
|
||||
|
||||
return subscription_response
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from lnbits.core.crud import (
|
||||
update_admin_settings,
|
||||
update_wallet,
|
||||
)
|
||||
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
|
||||
from lnbits.core.crud.wallets import delete_wallet_by_id
|
||||
from lnbits.core.models import (
|
||||
AccountFilters,
|
||||
@@ -73,7 +74,7 @@ async def api_get_users(
|
||||
summary="Get user by Id",
|
||||
)
|
||||
async def api_get_user(user_id: str) -> User:
|
||||
user = await get_user(user_id)
|
||||
user = await get_user(user_id, active_only=False)
|
||||
if not user:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||
return user
|
||||
@@ -197,7 +198,7 @@ async def api_users_reset_password(user_id: str) -> str:
|
||||
return f"reset_key_{reset_key_b64}"
|
||||
|
||||
|
||||
@users_router.get(
|
||||
@users_router.put(
|
||||
"/user/{user_id}/admin",
|
||||
dependencies=[Depends(check_super_user)],
|
||||
name="Give or revoke admin permsisions to a user",
|
||||
@@ -220,6 +221,43 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
||||
)
|
||||
|
||||
|
||||
@users_router.put(
|
||||
"/user/{user_id}/activate",
|
||||
name="Activate or deactivate a user",
|
||||
)
|
||||
async def api_users_toggle_activated(
|
||||
user_id: str, admin_account: Account = Depends(check_admin)
|
||||
) -> SimpleStatus:
|
||||
if user_id == settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Cannot deactivate super user.",
|
||||
)
|
||||
if user_id == admin_account.id:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="You cannot deactivate yourself.",
|
||||
)
|
||||
|
||||
if settings.is_admin_user(user_id):
|
||||
settings.lnbits_admin_users.remove(user_id)
|
||||
|
||||
user_account = await get_account(user_id, active_only=False)
|
||||
if not user_account:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="User not found.",
|
||||
)
|
||||
user_account.activated = not user_account.activated
|
||||
await update_account(user_account)
|
||||
await clear_user_id_cache(user_id)
|
||||
|
||||
return SimpleStatus(
|
||||
success=True,
|
||||
message=f"User {'activated' if user_account.activated else 'deactivated'}.",
|
||||
)
|
||||
|
||||
|
||||
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
||||
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
||||
return await get_wallets(user_id, deleted=None)
|
||||
|
||||
@@ -9,6 +9,7 @@ from fastapi import (
|
||||
)
|
||||
|
||||
from lnbits.core.crud.wallets import (
|
||||
clear_wallet_cache,
|
||||
create_wallet,
|
||||
get_wallets_paginated,
|
||||
)
|
||||
@@ -37,7 +38,6 @@ from lnbits.decorators import (
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import generate_filter_params_openapi
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
from ..crud import (
|
||||
delete_wallet,
|
||||
@@ -134,9 +134,7 @@ async def api_reset_wallet_keys(
|
||||
if not wallet or wallet.user != account_id.id:
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||
|
||||
cache.pop(f"auth:wallet:{wallet.id}")
|
||||
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
||||
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
||||
clear_wallet_cache(wallet)
|
||||
|
||||
wallet.adminkey = uuid4().hex
|
||||
wallet.inkey = uuid4().hex
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from fastapi import APIRouter, WebSocket
|
||||
|
||||
from ..crud.wallets import get_wallet_for_key
|
||||
from ..models import KeyType
|
||||
from ..services import websocket_manager
|
||||
|
||||
websocket_router = APIRouter(prefix="/api/v1/ws", tags=["Websocket"])
|
||||
@@ -11,6 +13,30 @@ async def websocket_connect(websocket: WebSocket, item_id: str) -> None:
|
||||
await websocket_manager.listen(conn)
|
||||
|
||||
|
||||
@websocket_router.websocket("/tag/{tag}")
|
||||
async def websocket_connect_tag(websocket: WebSocket, tag: str) -> None:
|
||||
api_key = websocket.headers.get("X-API-KEY") or websocket.query_params.get(
|
||||
"api-key"
|
||||
)
|
||||
if not api_key:
|
||||
await websocket.close(code=4401)
|
||||
return
|
||||
|
||||
wallet = await get_wallet_for_key(api_key)
|
||||
if not wallet:
|
||||
await websocket.close(code=4404)
|
||||
return
|
||||
|
||||
key_type = KeyType.admin if wallet.adminkey == api_key else KeyType.invoice
|
||||
if key_type not in {KeyType.admin, KeyType.invoice}:
|
||||
await websocket.close(code=4403)
|
||||
return
|
||||
|
||||
item_id = f"tag:{wallet.id}:{tag}"
|
||||
conn = await websocket_manager.connect(item_id, websocket)
|
||||
await websocket_manager.listen(conn)
|
||||
|
||||
|
||||
@websocket_router.post("/{item_id}")
|
||||
async def websocket_update_post(item_id: str, data: str):
|
||||
try:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from lnbits.extensions.wasm.wasm_host import * # noqa: F401,F403
|
||||
@@ -0,0 +1 @@
|
||||
from lnbits.extensions.wasm.wasm_host.extension_host import * # noqa: F401,F403
|
||||
@@ -0,0 +1 @@
|
||||
from lnbits.extensions.wasm.wasm_host.runner import * # noqa: F401,F403
|
||||
@@ -0,0 +1 @@
|
||||
from lnbits.extensions.wasm.wasm_host.service import * # noqa: F401,F403
|
||||
+9
-42
@@ -12,7 +12,6 @@ from typing import Any, Generic, Literal, TypeVar, get_origin
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, ValidationError, root_validator
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
@@ -56,14 +55,6 @@ def compat_timestamp_placeholder(key: str):
|
||||
return f":{key}"
|
||||
|
||||
|
||||
def get_placeholder(model: Any, field: str) -> str:
|
||||
type_ = model.__fields__[field].type_
|
||||
if type_ == datetime:
|
||||
return compat_timestamp_placeholder(field)
|
||||
else:
|
||||
return f":{field}"
|
||||
|
||||
|
||||
class Compat:
|
||||
type: str | None = "<inherited>"
|
||||
schema: str | None = "<inherited>"
|
||||
@@ -326,31 +317,7 @@ class Database(Compat):
|
||||
self.engine: AsyncEngine = create_async_engine(
|
||||
database_uri, echo=settings.debug_database
|
||||
)
|
||||
|
||||
if self.type in {POSTGRES, COCKROACH}:
|
||||
|
||||
@event.listens_for(self.engine.sync_engine, "connect")
|
||||
def register_custom_types(dbapi_connection, *_):
|
||||
def _parse_date(value) -> datetime:
|
||||
if value is None:
|
||||
value = "1970-01-01 00:00:00"
|
||||
f = "%Y-%m-%d %H:%M:%S.%f"
|
||||
if "." not in value:
|
||||
f = "%Y-%m-%d %H:%M:%S"
|
||||
# Parse and add UTC timezone info
|
||||
return datetime.strptime(value, f).replace(tzinfo=timezone.utc)
|
||||
|
||||
dbapi_connection.run_async(
|
||||
lambda connection: connection.set_type_codec(
|
||||
"TIMESTAMP",
|
||||
encoder=datetime,
|
||||
decoder=_parse_date,
|
||||
schema="pg_catalog",
|
||||
)
|
||||
)
|
||||
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
logger.trace(f"database {self.type} added for {self.name}")
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -663,7 +630,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
|
||||
placeholders = []
|
||||
keys = model_to_dict(model).keys()
|
||||
for field in keys:
|
||||
placeholders.append(get_placeholder(model, field))
|
||||
placeholders.append(f":{field}")
|
||||
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||
fields = ", ".join([f'"{key}"' for key in keys])
|
||||
values = ", ".join(placeholders)
|
||||
@@ -681,9 +648,8 @@ def update_query(
|
||||
"""
|
||||
fields = []
|
||||
for field in model_to_dict(model).keys():
|
||||
placeholder = get_placeholder(model, field)
|
||||
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||
fields.append(f'"{field}" = {placeholder}')
|
||||
fields.append(f'"{field}" = :{field}')
|
||||
query = ", ".join(fields)
|
||||
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
|
||||
|
||||
@@ -701,7 +667,12 @@ def model_to_dict(model: BaseModel) -> dict:
|
||||
if model.__fields__[key].field_info.extra.get("no_database", False):
|
||||
continue
|
||||
if isinstance(value, datetime):
|
||||
_dict[key] = value.timestamp()
|
||||
if DB_TYPE == SQLITE:
|
||||
_dict[key] = value.timestamp()
|
||||
else:
|
||||
# remove tz. postgres and cockroach TIMESTAMP is not tz aware
|
||||
# so it will throw if we dont remove the UTC.
|
||||
_dict[key] = value.replace(tzinfo=None)
|
||||
continue
|
||||
if (
|
||||
type(type_) is type(BaseModel)
|
||||
@@ -757,11 +728,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
|
||||
if DB_TYPE == SQLITE:
|
||||
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
||||
else:
|
||||
# Ensure PostgreSQL datetime values have timezone info
|
||||
if isinstance(value, datetime) and value.tzinfo is None:
|
||||
_dict[key] = value.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
_dict[key] = value
|
||||
_dict[key] = value.replace(tzinfo=timezone.utc)
|
||||
continue
|
||||
if issubclass(type_, BaseModel):
|
||||
_dict[key] = dict_to_submodel(type_, value)
|
||||
|
||||
@@ -261,6 +261,7 @@ async def check_account_id_exists(
|
||||
account_id,
|
||||
expiry=settings.auth_authentication_cache_minutes * 60,
|
||||
)
|
||||
cache.set(f"auth:user:cache_key:{sha256s(account.id)}", cache_key)
|
||||
|
||||
return account_id
|
||||
|
||||
|
||||
+63
-18
@@ -2,7 +2,7 @@ import asyncio
|
||||
import json
|
||||
import time
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -24,6 +24,8 @@ from .base import (
|
||||
FiatSubscriptionResponse,
|
||||
)
|
||||
|
||||
FiatMethod = Literal["checkout", "subscription"]
|
||||
|
||||
|
||||
class PayPalCheckoutOptions(BaseModel):
|
||||
class Config:
|
||||
@@ -34,11 +36,21 @@ class PayPalCheckoutOptions(BaseModel):
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class PayPalSubscriptionOptions(BaseModel):
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
|
||||
checking_id: str | None = None
|
||||
payment_request: str | None = None
|
||||
|
||||
|
||||
class PayPalCreateInvoiceOptions(BaseModel):
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
|
||||
fiat_method: FiatMethod = "checkout"
|
||||
checkout: PayPalCheckoutOptions | None = None
|
||||
subscription: PayPalSubscriptionOptions | None = None
|
||||
|
||||
|
||||
class PayPalWallet(FiatProvider):
|
||||
@@ -96,6 +108,14 @@ class PayPalWallet(FiatProvider):
|
||||
opts = self._parse_create_opts(extra or {})
|
||||
if opts is None:
|
||||
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options")
|
||||
if opts.fiat_method == "subscription":
|
||||
term = opts.subscription or PayPalSubscriptionOptions()
|
||||
checking_id = term.checking_id or urlsafe_short_hash()
|
||||
return FiatInvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=f"subscription_{checking_id}",
|
||||
payment_request=term.payment_request or "",
|
||||
)
|
||||
|
||||
try:
|
||||
await self._ensure_access_token()
|
||||
@@ -171,6 +191,7 @@ class PayPalWallet(FiatProvider):
|
||||
or "https://lnbits.com"
|
||||
)
|
||||
|
||||
logger.debug(f"Creating PayPal subscription with ID '{subscription_id}'")
|
||||
if not payment_options.subscription_request_id:
|
||||
payment_options.subscription_request_id = urlsafe_short_hash()
|
||||
payment_options.extra = payment_options.extra or {}
|
||||
@@ -182,7 +203,6 @@ class PayPalWallet(FiatProvider):
|
||||
await self._ensure_access_token()
|
||||
payload = {
|
||||
"plan_id": subscription_id,
|
||||
"quantity": str(quantity),
|
||||
"custom_id": self._serialize_metadata(payment_options),
|
||||
"application_context": {
|
||||
"return_url": success_url,
|
||||
@@ -205,7 +225,7 @@ class PayPalWallet(FiatProvider):
|
||||
return FiatSubscriptionResponse(
|
||||
ok=True,
|
||||
checkout_session_url=approval_url,
|
||||
subscription_request_id=payment_options.subscription_request_id,
|
||||
subscription_request_id=data.get("id"),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
@@ -219,6 +239,10 @@ class PayPalWallet(FiatProvider):
|
||||
correlation_id: str,
|
||||
**kwargs,
|
||||
) -> FiatSubscriptionResponse:
|
||||
logger.debug(
|
||||
f"Cancelling PayPal subscription '{subscription_id}'. "
|
||||
f"Correlation ID '{correlation_id}'."
|
||||
)
|
||||
try:
|
||||
await self._ensure_access_token()
|
||||
r = await self.client.post(
|
||||
@@ -240,12 +264,20 @@ class PayPalWallet(FiatProvider):
|
||||
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||
try:
|
||||
await self._ensure_access_token()
|
||||
order_id = self._normalize_paypal_id(checking_id)
|
||||
r = await self.client.get(
|
||||
f"/v2/checkout/orders/{order_id}", headers=self._auth_headers()
|
||||
)
|
||||
r.raise_for_status()
|
||||
return self._status_from_order(r.json())
|
||||
paypal_id = self._normalize_paypal_id(checking_id)
|
||||
if paypal_id.startswith("subscription_"):
|
||||
capture_id = paypal_id.replace("subscription_", "", 1)
|
||||
r = await self.client.get(
|
||||
f"v2/payments/captures/{capture_id}", headers=self._auth_headers()
|
||||
)
|
||||
r.raise_for_status()
|
||||
return self._status_from_capture(r.json())
|
||||
else:
|
||||
r = await self.client.get(
|
||||
f"/v2/checkout/orders/{paypal_id}", headers=self._auth_headers()
|
||||
)
|
||||
r.raise_for_status()
|
||||
return self._status_from_order(r.json())
|
||||
except Exception as exc:
|
||||
logger.debug(f"Error getting PayPal order status: {exc}")
|
||||
return FiatPaymentPendingStatus()
|
||||
@@ -270,6 +302,14 @@ class PayPalWallet(FiatProvider):
|
||||
return FiatPaymentFailedStatus()
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
def _status_from_capture(self, order: dict[str, Any]) -> FiatPaymentStatus:
|
||||
status = (order.get("status") or "").upper()
|
||||
if status in ["COMPLETED"]:
|
||||
return FiatPaymentSuccessStatus()
|
||||
if status in ["DECLINED", "FAILED", "CANCELLED", "CANCELED"]:
|
||||
return FiatPaymentFailedStatus()
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
def _normalize_paypal_id(self, checking_id: str) -> str:
|
||||
return (
|
||||
checking_id.replace("fiat_paypal_", "", 1)
|
||||
@@ -280,15 +320,20 @@ class PayPalWallet(FiatProvider):
|
||||
def _serialize_metadata(
|
||||
self, payment_options: FiatSubscriptionPaymentOptions
|
||||
) -> str:
|
||||
md = {
|
||||
"wallet_id": payment_options.wallet_id,
|
||||
"memo": payment_options.memo,
|
||||
"extra": payment_options.extra,
|
||||
"tag": payment_options.tag,
|
||||
"subscription_request_id": payment_options.subscription_request_id,
|
||||
}
|
||||
raw = json.dumps(md)
|
||||
return raw[:127] # PayPal custom_id limit
|
||||
meta = [
|
||||
payment_options.wallet_id,
|
||||
payment_options.tag,
|
||||
payment_options.subscription_request_id,
|
||||
]
|
||||
if payment_options.extra:
|
||||
meta.append(payment_options.extra.get("link", None))
|
||||
|
||||
# Keep custom_id within PayPal's 127-char limit
|
||||
memo_limit = 120 - len(json.dumps(meta))
|
||||
if memo_limit > 0 and payment_options.memo:
|
||||
meta.append(payment_options.memo[:memo_limit])
|
||||
|
||||
return json.dumps(meta)
|
||||
|
||||
def _parse_create_opts(
|
||||
self, raw_opts: dict[str, Any]
|
||||
|
||||
+8
-51
@@ -70,57 +70,11 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
t.env.globals["static_url_for"] = static_url_for
|
||||
t.env.globals["normalize_path"] = normalize_path
|
||||
|
||||
window_settings = {
|
||||
"AD_SPACE": settings.lnbits_ad_space,
|
||||
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
||||
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
||||
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
||||
"HIDE_API": settings.lnbits_hide_api,
|
||||
"SITE_TITLE": settings.lnbits_site_title,
|
||||
"SITE_TAGLINE": settings.lnbits_site_tagline,
|
||||
"SITE_DESCRIPTION": settings.lnbits_site_description,
|
||||
"LNBITS_ADMIN_UI": settings.lnbits_admin_ui,
|
||||
"LNBITS_AUDIT_ENABLED": settings.lnbits_audit_enabled,
|
||||
"LNBITS_AUTH_METHODS": settings.auth_allowed_methods,
|
||||
"LNBITS_AUTH_KEYCLOAK_ORG": settings.keycloak_client_custom_org,
|
||||
"LNBITS_AUTH_KEYCLOAK_ICON": settings.keycloak_client_custom_icon,
|
||||
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
|
||||
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
|
||||
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
||||
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
||||
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
||||
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
||||
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
||||
"LNBITS_APPLE_TOUCH_ICON": settings.lnbits_apple_touch_icon,
|
||||
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
||||
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
||||
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
||||
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
|
||||
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
||||
"LNBITS_VERSION": settings.version,
|
||||
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
||||
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
||||
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
|
||||
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
|
||||
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
||||
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||
"LNBITS_EXTENSIONS_REVIEWS_URL": settings.lnbits_extensions_reviews_url,
|
||||
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
||||
"has_holdinvoice": settings.has_holdinvoice,
|
||||
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
||||
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
|
||||
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
|
||||
"LNBITS_CURRENCIES": list(currencies.keys()),
|
||||
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
|
||||
"CACHE_KEY": settings.server_startup_time,
|
||||
}
|
||||
|
||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||
for key, value in window_settings.items():
|
||||
t.env.globals[key] = value
|
||||
# used in base.html
|
||||
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
|
||||
t.env.globals["LNBITS_APPLE_TOUCH_ICON"] = settings.lnbits_apple_touch_icon
|
||||
t.env.globals["SETTINGS"] = settings.to_public().dict(by_alias=True)
|
||||
t.env.globals["CURRENCIES"] = list(currencies.keys())
|
||||
|
||||
if settings.bundle_assets:
|
||||
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
||||
@@ -134,6 +88,9 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
||||
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
||||
|
||||
# backwards compatibility for extensions (tpos)
|
||||
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
|
||||
|
||||
return t
|
||||
|
||||
|
||||
|
||||
+147
-4
@@ -41,6 +41,14 @@ class UsersSettings(LNbitsSettings):
|
||||
lnbits_admin_users: list[str] = Field(default=[])
|
||||
lnbits_allowed_users: list[str] = Field(default=[])
|
||||
lnbits_allow_new_accounts: bool = Field(default=True)
|
||||
lnbits_require_user_activation: bool = Field(default=False)
|
||||
|
||||
lnbits_user_activation_by_email: bool = Field(default=False)
|
||||
lnbits_user_activation_by_payment: bool = Field(default=False)
|
||||
lnbits_user_activation_by_invitation_code: bool = Field(default=False)
|
||||
|
||||
lnbits_register_reusable_activation_code: str = Field(default="")
|
||||
lnbits_register_one_time_activation_codes: list[str] = Field(default=[])
|
||||
|
||||
@property
|
||||
def new_accounts_allowed(self) -> bool:
|
||||
@@ -252,6 +260,11 @@ class ThemesSettings(LNbitsSettings):
|
||||
)
|
||||
lnbits_show_home_page_elements: bool = Field(default=True)
|
||||
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
||||
|
||||
lnbits_wallet_featured_button_label: str | None = Field(default=None)
|
||||
lnbits_wallet_featured_button_url: str | None = Field(default=None)
|
||||
lnbits_wallet_featured_button_icon: str | None = Field(default=None)
|
||||
|
||||
lnbits_custom_badge: str | None = Field(default=None)
|
||||
lnbits_custom_badge_color: str = Field(default="warning")
|
||||
lnbits_theme_options: list[str] = Field(
|
||||
@@ -283,6 +296,10 @@ class ThemesSettings(LNbitsSettings):
|
||||
lnbits_default_border: str = Field(default="hard-border")
|
||||
lnbits_default_gradient: bool = Field(default=True)
|
||||
lnbits_default_bgimage: str | None = Field(default=None)
|
||||
lnbits_default_dark: bool = Field(default=True)
|
||||
lnbits_default_card_rounded: bool = Field(default=False)
|
||||
lnbits_default_card_gradient: bool = Field(default=False)
|
||||
lnbits_default_card_shadow: bool = Field(default=False)
|
||||
|
||||
|
||||
class OpsSettings(LNbitsSettings):
|
||||
@@ -590,10 +607,8 @@ class SparkFundingSource(LNbitsSettings):
|
||||
|
||||
|
||||
class SparkL2FundingSource(LNbitsSettings):
|
||||
spark_l2_mnemonic: str | None = Field(default=None)
|
||||
spark_l2_network: str = Field(default="MAINNET")
|
||||
spark_l2_internal_sidecar_version: str | None = Field(default="0.1.1")
|
||||
spark_l2_external_endpoint: str | None = Field(default=None)
|
||||
spark_l2_external_endpoint: str | None = Field(default="http://localhost:8765")
|
||||
spark_l2_external_api_key: str | None = Field(default=None)
|
||||
spark_l2_pay_wait_ms: int = Field(default=4000, ge=0)
|
||||
spark_l2_pay_poll_ms: int = Field(default=500, ge=0)
|
||||
@@ -990,6 +1005,7 @@ class EnvSettings(LNbitsSettings):
|
||||
enable_log_to_file: bool = Field(default=True)
|
||||
log_rotation: str = Field(default="100 MB")
|
||||
log_retention: str = Field(default="3 months")
|
||||
first_install_token: str | None = Field(default=None)
|
||||
|
||||
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||
@@ -1033,7 +1049,7 @@ class SuperUserSettings(LNbitsSettings):
|
||||
"FakeWallet",
|
||||
"LNPayWallet",
|
||||
"LNbitsWallet",
|
||||
"LightsparkSparkWallet",
|
||||
"SparkL2Wallet",
|
||||
"LnTipsWallet",
|
||||
"LndRestWallet",
|
||||
"LndWallet",
|
||||
@@ -1139,6 +1155,133 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
||||
and ext_id in self.lnbits_all_extensions_ids
|
||||
)
|
||||
|
||||
def to_public(self) -> PublicSettings:
|
||||
return PublicSettings.from_settings(self)
|
||||
|
||||
|
||||
class PublicSettings(BaseModel):
|
||||
"""
|
||||
PublicSettings is used for templating and public information.
|
||||
WARNING: do not expose private information, PublicSettings is exposed publicly.
|
||||
"""
|
||||
|
||||
allow_register: bool = Field(alias="allowRegister")
|
||||
qr_logo: str = Field(alias="qrLogo")
|
||||
site_title: str = Field(alias="siteTitle")
|
||||
site_tagline: str = Field(alias="siteTagline")
|
||||
site_description: str | None = Field(alias="siteDescription")
|
||||
auth_methods: list[str] = Field(alias="authMethods")
|
||||
keycloak_org: str | None = Field(alias="keycloakOrg")
|
||||
keycloak_icon: str | None = Field(alias="keycloakIcon")
|
||||
has_holdinvoice: bool = Field(alias="hasHoldinvoice")
|
||||
has_nodemanager: bool = Field(alias="hasNodemanager")
|
||||
show_nodemanager: bool = Field(alias="showNodemanager")
|
||||
custom_logo: str | None = Field(alias="customLogo")
|
||||
show_voidwallet: bool = Field(alias="showVoidwallet")
|
||||
version: str = Field(alias="version")
|
||||
show_home_page_elements: bool = Field(alias="showHomePageElements")
|
||||
hide_api: bool = Field(alias="hideApi")
|
||||
cache_key: str = Field(alias="cacheKey")
|
||||
webpush_pubkey: str | None = Field(alias="webpushPubkey")
|
||||
show_extensions: bool = Field(alias="showExtensions")
|
||||
show_audit: bool = Field(alias="showAudit")
|
||||
show_admin: bool = Field(alias="showAdmin")
|
||||
ad_space: list[list[str]] = Field(alias="adSpace")
|
||||
ad_space_title: str = Field(alias="adSpaceTitle")
|
||||
show_ad_space: bool = Field(alias="showAdSpace")
|
||||
custom_image: str | None = Field(alias="customImage")
|
||||
custom_badge: str | None = Field(alias="customBadge")
|
||||
custom_badge_color: str | None = Field(alias="customBadgeColor")
|
||||
service_fee: float = Field(alias="serviceFee")
|
||||
service_fee_max: int = Field(alias="serviceFeeMax")
|
||||
service_fee_wallet: str | None = Field(alias="serviceFeeWallet")
|
||||
theme_options: list[str] = Field(alias="themeOptions")
|
||||
default_reaction: str = Field(alias="defaultReaction")
|
||||
default_theme: str = Field(alias="defaultTheme")
|
||||
default_border: str = Field(alias="defaultBorder")
|
||||
default_bgimage: str | None = Field(alias="defaultBgimage")
|
||||
default_gradient: bool = Field(alias="defaultGradient")
|
||||
default_dark: bool = Field(alias="defaultDark")
|
||||
default_card_rounded: bool = Field(alias="defaultCardRounded")
|
||||
default_card_gradient: bool = Field(alias="defaultCardGradient")
|
||||
default_card_shadow: bool = Field(alias="defaultCardShadow")
|
||||
denomination: str | None = Field()
|
||||
extensions: list[str] = Field()
|
||||
allowed_currencies: list[str] = Field(alias="allowedCurrencies")
|
||||
extensions_reviews_url: str = Field(alias="extensionsReviewsUrl")
|
||||
ext_builder: bool = Field(alias="extBuilder")
|
||||
nostr_configured: bool = Field(alias="nostrConfigured")
|
||||
telegram_configured: bool = Field(alias="telegramConfigured")
|
||||
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
|
||||
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
|
||||
wallet_featured_button_icon: str | None = Field(alias="walletFeaturedButtonIcon")
|
||||
lnbits_user_activation_by_email: bool = Field(alias="userActivationByEmail")
|
||||
lnbits_user_activation_by_payment: bool = Field(alias="userActivationByPayment")
|
||||
lnbits_user_activation_by_invitation_code: bool = Field(
|
||||
alias="userActivationByInvitationCode"
|
||||
)
|
||||
has_first_install_token: bool = Field(alias="hasFirstInstallToken")
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings: Settings):
|
||||
ads = [x.split(";") for x in settings.lnbits_ad_space.split(",")]
|
||||
return cls(
|
||||
adSpace=ads,
|
||||
adSpaceTitle=settings.lnbits_ad_space_title,
|
||||
showAdSpace=settings.lnbits_ad_space_enabled and len(ads) > 0,
|
||||
allowRegister=settings.new_accounts_allowed,
|
||||
qrLogo=settings.lnbits_qr_logo,
|
||||
siteDescription=settings.lnbits_site_description,
|
||||
siteTitle=settings.lnbits_site_title,
|
||||
siteTagline=settings.lnbits_site_tagline,
|
||||
authMethods=settings.auth_allowed_methods,
|
||||
keycloakOrg=settings.keycloak_client_custom_org,
|
||||
keycloakIcon=settings.keycloak_client_custom_icon,
|
||||
hasHoldinvoice=settings.has_holdinvoice,
|
||||
hasNodemanager=settings.has_nodemanager,
|
||||
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
|
||||
customLogo=settings.lnbits_custom_logo,
|
||||
showVoidwallet=settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||
version=settings.version,
|
||||
showHomePageElements=settings.lnbits_show_home_page_elements,
|
||||
hideApi=settings.lnbits_hide_api,
|
||||
cacheKey=str(settings.server_startup_time),
|
||||
webpushPubkey=settings.lnbits_webpush_pubkey,
|
||||
showExtensions=not settings.lnbits_extensions_deactivate_all,
|
||||
showAudit=settings.lnbits_audit_enabled,
|
||||
showAdmin=settings.lnbits_admin_ui,
|
||||
customImage=settings.lnbits_custom_image,
|
||||
customBadge=settings.lnbits_custom_badge,
|
||||
customBadgeColor=settings.lnbits_custom_badge_color,
|
||||
serviceFee=settings.lnbits_service_fee,
|
||||
serviceFeeMax=settings.lnbits_service_fee_max,
|
||||
serviceFeeWallet=settings.lnbits_service_fee_wallet,
|
||||
themeOptions=settings.lnbits_theme_options,
|
||||
defaultReaction=settings.lnbits_default_reaction,
|
||||
defaultTheme=settings.lnbits_default_theme,
|
||||
defaultBorder=settings.lnbits_default_border,
|
||||
defaultGradient=settings.lnbits_default_gradient,
|
||||
defaultBgimage=settings.lnbits_default_bgimage,
|
||||
defaultDark=settings.lnbits_default_dark,
|
||||
defaultCardRounded=settings.lnbits_default_card_rounded,
|
||||
defaultCardGradient=settings.lnbits_default_card_gradient,
|
||||
defaultCardShadow=settings.lnbits_default_card_shadow,
|
||||
denomination=settings.lnbits_denomination,
|
||||
extensions=list(settings.lnbits_installed_extensions_ids),
|
||||
allowedCurrencies=settings.lnbits_allowed_currencies,
|
||||
extensionsReviewsUrl=settings.lnbits_extensions_reviews_url,
|
||||
extBuilder=settings.lnbits_extensions_builder_activate_non_admins,
|
||||
nostrConfigured=settings.is_nostr_notifications_configured(),
|
||||
telegramConfigured=settings.is_telegram_notifications_configured(),
|
||||
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
|
||||
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
|
||||
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
|
||||
userActivationByEmail=settings.lnbits_user_activation_by_email,
|
||||
userActivationByPayment=settings.lnbits_user_activation_by_payment,
|
||||
userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code,
|
||||
hasFirstInstallToken=settings.first_install_token is not None,
|
||||
)
|
||||
|
||||
|
||||
class SuperSettings(EditableSettings):
|
||||
super_user: str
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+13
-11
File diff suppressed because one or more lines are too long
@@ -220,6 +220,181 @@ body.body--dark .q-drawer {
|
||||
backdrop-filter: blur(6px) brightness(0.8);
|
||||
}
|
||||
|
||||
body.rounded-ui .q-card,
|
||||
body.rounded-ui .q-btn {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
body[data-theme=classic].card-gradient .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgba(103, 58, 183, 0.08), rgba(156, 39, 176, 0.06));
|
||||
}
|
||||
body[data-theme=classic].card-gradient .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgba(103, 58, 183, 0.06), rgba(156, 39, 176, 0.04));
|
||||
}
|
||||
|
||||
body[data-theme=classic].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgb(38.6192771084, 42.356626506, 64.7807228916), rgba(103, 58, 183, 0.07));
|
||||
}
|
||||
body[data-theme=classic].card-gradient.body--dark .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgb(36.7144578313, 40.2674698795, 61.5855421687), rgba(103, 58, 183, 0.05));
|
||||
}
|
||||
|
||||
body[data-theme=bitcoin].card-gradient .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgba(234, 97, 29, 0.08), rgba(229, 111, 53, 0.06));
|
||||
}
|
||||
body[data-theme=bitcoin].card-gradient .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgba(234, 97, 29, 0.06), rgba(229, 111, 53, 0.04));
|
||||
}
|
||||
|
||||
body[data-theme=bitcoin].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgb(54.18, 49.364, 71.036), rgba(234, 97, 29, 0.07));
|
||||
}
|
||||
body[data-theme=bitcoin].card-gradient.body--dark .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgb(51.885, 47.273, 68.027), rgba(234, 97, 29, 0.05));
|
||||
}
|
||||
|
||||
body[data-theme=freedom].card-gradient .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgba(226, 33, 86, 0.08), rgba(185, 26, 69, 0.06));
|
||||
}
|
||||
body[data-theme=freedom].card-gradient .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgba(226, 33, 86, 0.06), rgba(185, 26, 69, 0.04));
|
||||
}
|
||||
|
||||
body[data-theme=freedom].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgb(82.2051282051, 55.1948717949, 63.4153846154), rgba(226, 33, 86, 0.07));
|
||||
}
|
||||
body[data-theme=freedom].card-gradient.body--dark .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgb(79.1538461538, 53.1461538462, 61.0615384615), rgba(226, 33, 86, 0.05));
|
||||
}
|
||||
|
||||
body[data-theme=cyber].card-gradient .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgba(124, 179, 66, 0.08), rgba(85, 139, 47, 0.06));
|
||||
}
|
||||
body[data-theme=cyber].card-gradient .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgba(124, 179, 66, 0.06), rgba(85, 139, 47, 0.04));
|
||||
}
|
||||
|
||||
body[data-theme=cyber].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgb(10.2, 10.2, 10.2), rgba(124, 179, 66, 0.07));
|
||||
}
|
||||
body[data-theme=cyber].card-gradient.body--dark .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgb(7.65, 7.65, 7.65), rgba(124, 179, 66, 0.05));
|
||||
}
|
||||
|
||||
body[data-theme=mint].card-gradient .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgba(58, 183, 125, 0.08), rgba(39, 176, 101, 0.06));
|
||||
}
|
||||
body[data-theme=mint].card-gradient .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgba(58, 183, 125, 0.06), rgba(39, 176, 101, 0.04));
|
||||
}
|
||||
|
||||
body[data-theme=mint].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgb(38.6192771084, 64.7807228916, 53.5686746988), rgba(58, 183, 125, 0.07));
|
||||
}
|
||||
body[data-theme=mint].card-gradient.body--dark .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgb(36.7144578313, 61.5855421687, 50.9265060241), rgba(58, 183, 125, 0.05));
|
||||
}
|
||||
|
||||
body[data-theme=autumn].card-gradient .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgba(183, 118, 58, 0.08), rgba(176, 121, 39, 0.06));
|
||||
}
|
||||
body[data-theme=autumn].card-gradient .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgba(183, 118, 58, 0.06), rgba(176, 121, 39, 0.04));
|
||||
}
|
||||
|
||||
body[data-theme=autumn].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgb(64.7807228916, 51.0771084337, 38.6192771084), rgba(183, 118, 58, 0.07));
|
||||
}
|
||||
body[data-theme=autumn].card-gradient.body--dark .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgb(61.5855421687, 48.5578313253, 36.7144578313), rgba(183, 118, 58, 0.05));
|
||||
}
|
||||
|
||||
body[data-theme=flamingo].card-gradient .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgba(255, 0, 255, 0.08), rgba(253, 163, 253, 0.06));
|
||||
}
|
||||
body[data-theme=flamingo].card-gradient .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgba(255, 0, 255, 0.06), rgba(253, 163, 253, 0.04));
|
||||
}
|
||||
|
||||
body[data-theme=flamingo].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgb(66.176, 4.224, 66.176), rgba(255, 0, 255, 0.07));
|
||||
}
|
||||
body[data-theme=flamingo].card-gradient.body--dark .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgb(61.382, 3.918, 61.382), rgba(255, 0, 255, 0.05));
|
||||
}
|
||||
|
||||
body[data-theme=monochrome].card-gradient .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgba(73, 73, 73, 0.08), rgba(107, 107, 107, 0.06));
|
||||
}
|
||||
body[data-theme=monochrome].card-gradient .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgba(73, 73, 73, 0.06), rgba(107, 107, 107, 0.04));
|
||||
}
|
||||
|
||||
body[data-theme=monochrome].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgb(10.2, 10.2, 10.2), rgba(73, 73, 73, 0.07));
|
||||
}
|
||||
body[data-theme=monochrome].card-gradient.body--dark .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgb(7.65, 7.65, 7.65), rgba(73, 73, 73, 0.05));
|
||||
}
|
||||
|
||||
body[data-theme=salvador].card-gradient .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgba(25, 118, 210, 0.08), rgba(38, 166, 154, 0.06));
|
||||
}
|
||||
body[data-theme=salvador].card-gradient .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgba(25, 118, 210, 0.06), rgba(38, 166, 154, 0.04));
|
||||
}
|
||||
|
||||
body[data-theme=salvador].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark) {
|
||||
background-image: linear-gradient(135deg, rgb(43.9888888889, 64.2, 84.4111111111), rgba(25, 118, 210, 0.07));
|
||||
}
|
||||
body[data-theme=salvador].card-gradient.body--dark .q-drawer {
|
||||
background-image: linear-gradient(165deg, rgb(42.2416666667, 61.65, 81.0583333333), rgba(25, 118, 210, 0.05));
|
||||
}
|
||||
|
||||
body.card-shadow .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
|
||||
body.card-shadow.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
|
||||
:root {
|
||||
--size: 100px;
|
||||
--gap: 25px;
|
||||
|
||||
@@ -171,6 +171,7 @@ window.localisation.en = {
|
||||
drop_db: 'Remove Data',
|
||||
enable: 'Enable',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
pay_to_enable: 'Pay To Enable',
|
||||
enable_extension_details: 'Enable extension for current user',
|
||||
disable: 'Disable',
|
||||
@@ -178,6 +179,8 @@ window.localisation.en = {
|
||||
installed: 'Installed',
|
||||
activated: 'Activated',
|
||||
deactivated: 'Deactivated',
|
||||
activate: 'Activate',
|
||||
deactivate: 'Deactivate',
|
||||
release_notes: 'Release Notes',
|
||||
activate_extension_details: 'Make extension available/unavailable for users',
|
||||
featured: 'Featured',
|
||||
@@ -186,6 +189,8 @@ window.localisation.en = {
|
||||
only_admins_can_create_extensions:
|
||||
'Only admin accounts can create extensions',
|
||||
admin_only: 'Admin Only',
|
||||
make_user_admin: 'Make User Admin',
|
||||
revoke_admin: 'Revoke Admin',
|
||||
new_version: 'New Version',
|
||||
reviews_url: 'Reviews URL',
|
||||
reviews_url_label: 'Reviews server URL',
|
||||
@@ -280,7 +285,7 @@ window.localisation.en = {
|
||||
'Nip5 identifier to send notifications to',
|
||||
notifications_nostr_identifiers: 'Nostr Identifiers',
|
||||
notifications_nostr_identifiers_desc:
|
||||
'List of identifiers to send notifications to',
|
||||
'List of identifiers to send notifications to.',
|
||||
|
||||
notifications_telegram_config: 'Telegram Configuration',
|
||||
notifications_enable_telegram: 'Enable Telegram',
|
||||
@@ -460,6 +465,12 @@ window.localisation.en = {
|
||||
api_token_id: 'Token Id',
|
||||
toggle_gradient: 'Toggle Gradient',
|
||||
gradient_background: 'Gradient Background',
|
||||
rounded_ui: 'Rounded Cards & Buttons',
|
||||
toggle_rounded_ui: 'Toggle rounded corners for cards and buttons',
|
||||
card_gradient: 'Card Gradient',
|
||||
toggle_card_gradient: 'Toggle gradient on cards',
|
||||
card_shadow: 'Card Shadow',
|
||||
toggle_card_shadow: 'Toggle shadow on cards',
|
||||
language: 'Language',
|
||||
assets: 'Assets',
|
||||
max_asset_size_mb: 'Max Asset Size (MB)',
|
||||
@@ -518,6 +529,7 @@ window.localisation.en = {
|
||||
hash: 'Hash: ',
|
||||
welcome_lnbits: 'Welcome to LNbits',
|
||||
setup_su_account: 'Set up the Superuser account below.',
|
||||
first_install_token: 'First Install Token',
|
||||
create_ticker_converter: 'Create Currency Ticker Converter',
|
||||
enable_audit: 'Enable Audit',
|
||||
recommended: 'Recommended',
|
||||
@@ -658,6 +670,15 @@ window.localisation.en = {
|
||||
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
||||
ui_default_wallet_name: 'Default Wallet Name',
|
||||
ui_default_theme: 'Default Theme',
|
||||
wallet_featured_button_label: 'Wallet Featured Button Label',
|
||||
wallet_featured_button_label_hint:
|
||||
'Show featured button on the wallet homepage',
|
||||
wallet_featured_button_url: 'Featured Button URL',
|
||||
wallet_featured_button_url_hint:
|
||||
'On click the button will open this URL. Leave empty to hide the button.',
|
||||
wallet_featured_button_icon: 'Featured Button Icon',
|
||||
wallet_featured_button_icon_hint:
|
||||
'Icon shown on the featured button (check quasar icons)',
|
||||
lnbits_wallet: 'LNbits wallet',
|
||||
denomination: 'Denomination',
|
||||
denomination_hint: 'The name for the FakeWallet token',
|
||||
@@ -694,6 +715,25 @@ window.localisation.en = {
|
||||
allowed_users_label: 'User ID',
|
||||
allow_creation_user: 'Allow creation of new users',
|
||||
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
||||
require_user_activation: 'Require user activation',
|
||||
require_user_activation_desc:
|
||||
'New users will be activated only after they pass one of the confirmation methods. Admins can activate users manually from the admin panel.',
|
||||
reusable_activation_code: 'Reusable activation code',
|
||||
reusable_activation_code_label: 'Reusable activation code',
|
||||
reusable_activation_code_hint:
|
||||
'This activation code can be used multiple times by different users.',
|
||||
one_time_activation_code: 'One-time activation codes',
|
||||
one_time_activation_code_label: 'Add activation code',
|
||||
one_time_activation_code_hint:
|
||||
'List of one-time activation codes. Each code can be used only once, then will be reomved from the list.',
|
||||
invitation_code: 'Invitation Code',
|
||||
invitation_code_hint: 'The invitation code that you have received.',
|
||||
email: 'Email',
|
||||
email_confirmation_hint: 'Email address to send the confirmation code to.',
|
||||
nostr_identifier: 'Nostr Identifier',
|
||||
nostr_identifier_hint:
|
||||
'Nostr nip5 identifier or <npub> to send the confirmation code to.',
|
||||
|
||||
new_user_not_allowed: 'Registration is disabled.',
|
||||
start_user_impersonation: 'Impersonate this user',
|
||||
stop_user_impersonation: 'Stop User Impersonation',
|
||||
|
||||
@@ -66,7 +66,7 @@ window._lnbitsApi = {
|
||||
name: name
|
||||
})
|
||||
},
|
||||
register(username, email, password, password_repeat) {
|
||||
register(username, email, password, password_repeat, invitation_code) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/register',
|
||||
@@ -74,7 +74,8 @@ window._lnbitsApi = {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
password_repeat
|
||||
password_repeat,
|
||||
invitation_code
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -147,13 +148,16 @@ window._lnbitsApi = {
|
||||
name: name,
|
||||
wallet_type: walletType,
|
||||
...opts
|
||||
}).catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
},
|
||||
updateWallet(name, wallet) {
|
||||
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
||||
name: name
|
||||
})
|
||||
},
|
||||
updateUiCustomization(data = {}) {
|
||||
return this.request('patch', '/api/v1/auth/ui', null, data)
|
||||
},
|
||||
resetWalletKeys(wallet) {
|
||||
return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then(
|
||||
res => {
|
||||
|
||||
@@ -14,7 +14,8 @@ window.LNbits = {
|
||||
fiat_providers: data.fiat_providers || [],
|
||||
super_user: data.super_user,
|
||||
extra: data.extra ?? {},
|
||||
hasPassword: data.has_password ?? false
|
||||
hasPassword: data.has_password ?? false,
|
||||
uiCustomization: data.ui_customization || {}
|
||||
}
|
||||
const mapWallet = this.wallet
|
||||
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
||||
|
||||
@@ -35,23 +35,6 @@ window.app.component('lnbits-extension-rating', {
|
||||
|
||||
window.app.component('lnbits-manage', {
|
||||
template: '#lnbits-manage',
|
||||
computed: {
|
||||
showAdmin() {
|
||||
return this.LNBITS_ADMIN_UI
|
||||
},
|
||||
showUsers() {
|
||||
return this.LNBITS_ADMIN_UI
|
||||
},
|
||||
showNode() {
|
||||
return this.LNBITS_NODE_UI
|
||||
},
|
||||
showAudit() {
|
||||
return this.LNBITS_AUDIT_ENABLED
|
||||
},
|
||||
showExtensions() {
|
||||
return this.LNBITS_EXTENSIONS_DEACTIVATE_ALL === false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isActive(path) {
|
||||
return window.location.pathname === path
|
||||
@@ -449,8 +432,10 @@ window.app.component('username-password', {
|
||||
username: String,
|
||||
password_1: String,
|
||||
password_2: String,
|
||||
invitationCode: String,
|
||||
resetKey: String
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
oauth: [
|
||||
@@ -463,8 +448,10 @@ window.app.component('username-password', {
|
||||
password: this.password_1,
|
||||
passwordRepeat: this.password_2,
|
||||
reset_key: this.resetKey,
|
||||
keycloakOrg: LNBITS_AUTH_KEYCLOAK_ORG || 'Keycloak',
|
||||
keycloakIcon: LNBITS_AUTH_KEYCLOAK_ICON
|
||||
confirmationMethod: 'code',
|
||||
confirmationEmail: '',
|
||||
confirmationCode: this.invitationCode || '',
|
||||
showConfirmationCode: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -477,6 +464,7 @@ window.app.component('username-password', {
|
||||
this.$emit('update:userName', this.username)
|
||||
this.$emit('update:password_1', this.password)
|
||||
this.$emit('update:password_2', this.passwordRepeat)
|
||||
this.$emit('update:invitationCode', this.confirmationCode)
|
||||
this.$emit('register')
|
||||
},
|
||||
reset() {
|
||||
@@ -568,6 +556,25 @@ window.app.component('username-password', {
|
||||
computed: {
|
||||
showOauth() {
|
||||
return this.oauth.some(m => this.authMethods.includes(m))
|
||||
},
|
||||
disableRegister() {
|
||||
const usernameOK = !!this.username
|
||||
const passwordOK = !!this.password && this.password.length >= 8
|
||||
const passwordsMatch = this.password === this.passwordRepeat
|
||||
const codeOk =
|
||||
this.confirmationMethodsCount === 0 ||
|
||||
this.confirmationMethod !== 'code' ||
|
||||
this.confirmationCode.length > 0
|
||||
|
||||
return !usernameOK || !passwordOK || !passwordsMatch || !codeOk
|
||||
},
|
||||
confirmationMethodsCount() {
|
||||
const methods = [
|
||||
this.g.settings.userActivationByEmail,
|
||||
this.g.settings.userActivationByPayment,
|
||||
this.g.settings.userActivationByInvitationCode
|
||||
]
|
||||
return methods.filter(Boolean).length
|
||||
}
|
||||
},
|
||||
created() {}
|
||||
|
||||
@@ -229,46 +229,38 @@ window.app.component('lnbits-admin-funding-sources', {
|
||||
}
|
||||
],
|
||||
[
|
||||
'LightsparkSparkWallet',
|
||||
'SparkL2Wallet',
|
||||
'Spark (L2)',
|
||||
{
|
||||
spark_l2_internal_sidecar_version: {
|
||||
label: 'Internal Sidecar Version (eg: 0.1.1).',
|
||||
hint: 'If specified then the sidecar will be downloaded. Alternatively you can specify an External Sidecar endpoint in the Advanced section.',
|
||||
spark_l2_external_endpoint: {
|
||||
label: 'External Sidecar Endpoint',
|
||||
hint: 'If specified then this endpoint will be used instead of the internal sidecar. Make sure to also specify the API key if your sidecar requires authentication.',
|
||||
value: ''
|
||||
},
|
||||
spark_l2_mnemonic: {
|
||||
label: 'Mnemonic',
|
||||
hint: 'Only required if Interna Sidecar version is specified.'
|
||||
spark_l2_external_api_key: {
|
||||
label: 'External Sidecar API Key',
|
||||
hint: 'API key for authenticating with the external sidecar if it requires authentication.',
|
||||
value: ''
|
||||
},
|
||||
spark_l2_network: {
|
||||
label: 'Network',
|
||||
value: 'MAINNET'
|
||||
},
|
||||
|
||||
spark_l2_external_endpoint: {
|
||||
label: 'External Sidecar Endpoint. ',
|
||||
hint: 'If specified then this endpoint will be used instead of the internal sidecar. Make sure to also specify the API key if your sidecar requires authentication.',
|
||||
value: '',
|
||||
value: 'MAINNET',
|
||||
hint: 'The network to use for the Spark wallet.',
|
||||
advanced: true
|
||||
},
|
||||
spark_l2_external_api_key: {
|
||||
label: 'External Sidecar API Key. ',
|
||||
hint: 'API Key for authenticating with the external sidecar if it requires authentication.',
|
||||
value: '',
|
||||
advanced: true
|
||||
},
|
||||
|
||||
spark_l2_pay_wait_ms: {
|
||||
label: 'Pay Wait Time (ms)',
|
||||
label: 'Payment Wait Time (ms)',
|
||||
hint: 'The time to wait for a payment to be processed before considering it failed.',
|
||||
advanced: true
|
||||
},
|
||||
spark_l2_pay_poll_ms: {
|
||||
label: 'Pay Poll Time (ms)',
|
||||
label: 'Payment Poll Time (ms)',
|
||||
hint: 'The time to wait between polling for payment status updates.',
|
||||
advanced: true
|
||||
},
|
||||
spark_l2_stream_keepalive_ms: {
|
||||
label: 'Stream Keepalive Time (ms)',
|
||||
hint: 'The time to wait between sending keepalive messages to the Spark sidecar to keep the connection open.',
|
||||
advanced: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ window.app.component('lnbits-admin-users', {
|
||||
data() {
|
||||
return {
|
||||
formAddUser: '',
|
||||
formAddAdmin: ''
|
||||
formAddAdmin: '',
|
||||
formAddActivationCode: '',
|
||||
showReusableActivationCode: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -31,6 +33,24 @@ window.app.component('lnbits-admin-users', {
|
||||
removeAdminUser(user) {
|
||||
let admin_users = this.formData.lnbits_admin_users
|
||||
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
||||
},
|
||||
addOneTimeActivationCode() {
|
||||
const code = this.formAddActivationCode
|
||||
const activationCodes =
|
||||
this.formData.lnbits_register_one_time_activation_codes
|
||||
if (code?.length && !activationCodes.includes(code)) {
|
||||
this.formData.lnbits_register_one_time_activation_codes = [
|
||||
...activationCodes,
|
||||
code
|
||||
]
|
||||
this.formAddActivationCode = ''
|
||||
}
|
||||
},
|
||||
removeOneTimeActivationCode(code) {
|
||||
const codes = this.formData.lnbits_register_one_time_activation_codes
|
||||
this.formData.lnbits_register_one_time_activation_codes = codes.filter(
|
||||
u => u !== code
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
window.app.component('lnbits-footer', {
|
||||
template: '#lnbits-footer',
|
||||
computed: {
|
||||
version() {
|
||||
return this.LNBITS_VERSION || 'unknown version'
|
||||
},
|
||||
title() {
|
||||
return `${this.SITE_TITLE}, ${this.SITE_TAGLINE}`
|
||||
return `${this.g.settings.siteTitle}, ${this.g.settings.siteTagline}`
|
||||
},
|
||||
showFooter() {
|
||||
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||
version() {
|
||||
return this.$t('lnbits_version') + ': ' + this.g.settings.version
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,48 +1,9 @@
|
||||
window.app.component('lnbits-header', {
|
||||
template: '#lnbits-header',
|
||||
computed: {
|
||||
hasServiceFeeMax() {
|
||||
return (
|
||||
this.g.user &&
|
||||
this.LNBITS_SERVICE_FEE_MAX &&
|
||||
this.LNBITS_SERVICE_FEE_MAX > 0
|
||||
)
|
||||
},
|
||||
serviceFeeMax() {
|
||||
return this.LNBITS_SERVICE_FEE_MAX || 0
|
||||
},
|
||||
hasServiceFee() {
|
||||
return (
|
||||
this.g.user && this.LNBITS_SERVICE_FEE && this.LNBITS_SERVICE_FEE > 0
|
||||
)
|
||||
},
|
||||
serviceFee() {
|
||||
return this.LNBITS_SERVICE_FEE || 0
|
||||
},
|
||||
hasCustomBadge() {
|
||||
return this.LNBITS_CUSTOM_BADGE && this.LNBITS_CUSTOM_BADGE != ''
|
||||
},
|
||||
customBadge() {
|
||||
return this.LNBITS_CUSTOM_BADGE || ''
|
||||
},
|
||||
customBadgeColor() {
|
||||
return this.LNBITS_CUSTOM_BADGE_COLOR || ''
|
||||
},
|
||||
title() {
|
||||
return this.SITE_TITLE
|
||||
},
|
||||
titleIsLnbits() {
|
||||
return this.SITE_TITLE == 'LNbits'
|
||||
},
|
||||
customLogoUrl() {
|
||||
return this.USE_CUSTOM_LOGO || null
|
||||
},
|
||||
showAdmin() {
|
||||
return this.g.user && (this.g.user.super_user || this.g.user.admin)
|
||||
},
|
||||
showVoidwallet() {
|
||||
return this.g.user && this.VOIDWALLET == true
|
||||
},
|
||||
displayName() {
|
||||
return (
|
||||
this.g.user?.extra?.display_name ||
|
||||
@@ -69,6 +30,17 @@ window.app.component('lnbits-header', {
|
||||
window.location = '/users'
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
async handleLanguageChanged(lang) {
|
||||
try {
|
||||
await LNbits.api.updateUiCustomization({locale: lang.locale})
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Language Updated',
|
||||
caption: lang.locale
|
||||
})
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@ window.app.component('lnbits-home-logos', {
|
||||
showLogos() {
|
||||
return (
|
||||
this.g.isSatsDenomination &&
|
||||
this.SITE_TITLE == 'LNbits' &&
|
||||
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||
this.g.settings.siteTitle == 'LNbits' &&
|
||||
this.g.settings.showHomePageElements == true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
window.app.component('lnbits-language-dropdown', {
|
||||
template: '#lnbits-language-dropdown',
|
||||
computed: {
|
||||
currentLanguage() {
|
||||
return (
|
||||
this.langs.find(lang => lang.value === window.i18n.global.locale) || {
|
||||
value: 'en',
|
||||
label: 'English',
|
||||
display: '🇬🇧 EN'
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
activeLanguage(lang) {
|
||||
return window.i18n.global.locale === lang
|
||||
@@ -8,6 +19,7 @@ window.app.component('lnbits-language-dropdown', {
|
||||
this.g.locale = newValue
|
||||
window.i18n.global.locale = newValue
|
||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||
this.$emit('language-changed', newValue)
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -34,7 +34,7 @@ window.app.component('lnbits-qrcode', {
|
||||
},
|
||||
logo: {
|
||||
type: String,
|
||||
default: LNBITS_QR_LOGO
|
||||
default: window.g.settings.qrLogo || null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -9,6 +9,10 @@ window.app.component('lnbits-theme', {
|
||||
'g.disclaimerShown'(val) {
|
||||
this.$q.localStorage.setItem('lnbits.disclaimerShown', val)
|
||||
},
|
||||
'g.locale'(val) {
|
||||
this.$q.localStorage.setItem('lnbits.lang', val)
|
||||
window.i18n.global.locale = val
|
||||
},
|
||||
'g.isFiatPriority'(val) {
|
||||
this.$q.localStorage.setItem('lnbits.isFiatPriority', val)
|
||||
},
|
||||
@@ -41,6 +45,30 @@ window.app.component('lnbits-theme', {
|
||||
document.body.classList.remove('gradient-bg')
|
||||
}
|
||||
},
|
||||
'g.cardRoundedChoice'(val) {
|
||||
this.$q.localStorage.set('lnbits.cardRounded', val)
|
||||
if (val === true) {
|
||||
document.body.classList.add('rounded-ui')
|
||||
} else {
|
||||
document.body.classList.remove('rounded-ui')
|
||||
}
|
||||
},
|
||||
'g.cardGradientChoice'(val) {
|
||||
this.$q.localStorage.set('lnbits.cardGradient', val)
|
||||
if (val === true) {
|
||||
document.body.classList.add('card-gradient')
|
||||
} else {
|
||||
document.body.classList.remove('card-gradient')
|
||||
}
|
||||
},
|
||||
'g.cardShadowChoice'(val) {
|
||||
this.$q.localStorage.set('lnbits.cardShadow', val)
|
||||
if (val === true) {
|
||||
document.body.classList.add('card-shadow')
|
||||
} else {
|
||||
document.body.classList.remove('card-shadow')
|
||||
}
|
||||
},
|
||||
'g.mobileSimple'(val) {
|
||||
this.$q.localStorage.set('lnbits.mobileSimple', val)
|
||||
if (val === true) {
|
||||
@@ -113,6 +141,15 @@ window.app.component('lnbits-theme', {
|
||||
if (this.g.gradientChoice === true) {
|
||||
document.body.classList.add('gradient-bg')
|
||||
}
|
||||
if (this.g.cardRoundedChoice === true) {
|
||||
document.body.classList.add('rounded-ui')
|
||||
}
|
||||
if (this.g.cardGradientChoice === true) {
|
||||
document.body.classList.add('card-gradient')
|
||||
}
|
||||
if (this.g.cardShadowChoice === true) {
|
||||
document.body.classList.add('card-shadow')
|
||||
}
|
||||
if (this.g.bgimageChoice !== '') {
|
||||
document.body.classList.add('bg-image')
|
||||
document.body.style.setProperty(
|
||||
@@ -123,6 +160,13 @@ window.app.component('lnbits-theme', {
|
||||
if (this.g.mobileSimple === true) {
|
||||
document.body.classList.add('mobile-simple')
|
||||
}
|
||||
Object.entries(this.g.user?.uiCustomization || {}).forEach(
|
||||
([key, value]) => {
|
||||
if (key in this.g) {
|
||||
this.g[key] = value
|
||||
}
|
||||
}
|
||||
)
|
||||
this.checkUrlParams()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -71,6 +71,10 @@ window.app.component('lnbits-wallet-extra', {
|
||||
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
||||
this.g.exchangeRate
|
||||
)
|
||||
if (this.g.exchangeRate <= 0) {
|
||||
this.g.fiatTracking = false
|
||||
this.g.isFiatPriority = false
|
||||
}
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ window.app.component('lnbits-wallet-new', {
|
||||
this.g.lastWalletId = res.data.id
|
||||
this.$router.push(`/wallet/${res.data.id}`)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
+36
-32
@@ -9,17 +9,50 @@ const localStore = (key, defaultValue) => {
|
||||
}
|
||||
|
||||
window.g = Vue.reactive({
|
||||
// vars from server
|
||||
settings: SETTINGS,
|
||||
currencies: CURRENCIES,
|
||||
extensions: SETTINGS.extensions,
|
||||
allowedCurrencies: SETTINGS.allowedCurrencies,
|
||||
denomination: SETTINGS.denomination,
|
||||
isSatsDenomination: SETTINGS.denomination == 'sats',
|
||||
// local storage vars
|
||||
themeChoice: localStore('lnbits.theme', SETTINGS.defaultTheme),
|
||||
borderChoice: localStore('lnbits.border', SETTINGS.defaultBorder),
|
||||
gradientChoice: localStore('lnbits.gradientBg', SETTINGS.defaultGradient),
|
||||
cardRoundedChoice: localStore(
|
||||
'lnbits.cardRounded',
|
||||
SETTINGS.defaultCardRounded
|
||||
),
|
||||
cardGradientChoice: localStore(
|
||||
'lnbits.cardGradient',
|
||||
SETTINGS.defaultCardGradient
|
||||
),
|
||||
cardShadowChoice: localStore('lnbits.cardShadow', SETTINGS.defaultCardShadow),
|
||||
reactionChoice: localStore('lnbits.reactions', SETTINGS.defaultReaction),
|
||||
bgimageChoice: localStore(
|
||||
'lnbits.backgroundImage',
|
||||
SETTINGS.defaultBgimage || ''
|
||||
),
|
||||
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
|
||||
disclaimerShown: localStore('lnbits.disclaimerShown', false),
|
||||
isFiatPriority: localStore('lnbits.isFiatPriority', false),
|
||||
mobileSimple: localStore('lnbits.mobileSimple', true),
|
||||
walletFlip: localStore('lnbits.walletFlip', false),
|
||||
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
|
||||
darkChoice: localStore('lnbits.darkMode', SETTINGS.defaultDark),
|
||||
// cookie vars
|
||||
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
||||
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
|
||||
// frontend vars
|
||||
errorCode: null,
|
||||
errorMessage: null,
|
||||
user: null,
|
||||
wallet: null,
|
||||
isPublicPage: true,
|
||||
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
||||
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
|
||||
offline: !navigator.onLine,
|
||||
hasCamera: false,
|
||||
visibleDrawer: false,
|
||||
extensions: WINDOW_SETTINGS.EXTENSIONS,
|
||||
fiatBalance: 0,
|
||||
exchangeRate: 0,
|
||||
fiatTracking: false,
|
||||
@@ -27,35 +60,6 @@ window.g = Vue.reactive({
|
||||
walletEventListeners: [],
|
||||
updatePayments: false, // used for updating the lnbits-payment-list
|
||||
updatePaymentsHash: false, // used for closing the receive dialog
|
||||
currencies: WINDOW_SETTINGS.LNBITS_CURRENCIES ?? [],
|
||||
allowedCurrencies: WINDOW_SETTINGS.LNBITS_ALLOWED_CURRENCIES ?? [],
|
||||
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
|
||||
disclaimerShown: localStore('lnbits.disclaimerShown', false),
|
||||
isFiatPriority: localStore('lnbits.isFiatPriority', false),
|
||||
mobileSimple: localStore('lnbits.mobileSimple', true),
|
||||
walletFlip: localStore('lnbits.walletFlip', false),
|
||||
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
|
||||
darkChoice: localStore('lnbits.darkMode', true),
|
||||
themeChoice: localStore('lnbits.theme', WINDOW_SETTINGS.LNBITS_DEFAULT_THEME),
|
||||
borderChoice: localStore(
|
||||
'lnbits.border',
|
||||
WINDOW_SETTINGS.LNBITS_DEFAULT_BORDER || 'hard-border'
|
||||
),
|
||||
gradientChoice: localStore(
|
||||
'lnbits.gradientBg',
|
||||
WINDOW_SETTINGS.LNBITS_DEFAULT_GRADIENT || false
|
||||
),
|
||||
reactionChoice: localStore(
|
||||
'lnbits.reactions',
|
||||
WINDOW_SETTINGS.LNBITS_DEFAULT_REACTION || 'confettiBothSides'
|
||||
),
|
||||
bgimageChoice: localStore(
|
||||
'lnbits.backgroundImage',
|
||||
WINDOW_SETTINGS.LNBITS_DEFAULT_BGIMAGE || ''
|
||||
),
|
||||
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
|
||||
denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION,
|
||||
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats',
|
||||
scanner: null,
|
||||
newWalletType: null
|
||||
})
|
||||
|
||||
@@ -157,8 +157,7 @@ window.app.mixin({
|
||||
return {
|
||||
api: window._lnbitsApi,
|
||||
utils: window._lnbitsUtils,
|
||||
g: window.g,
|
||||
...WINDOW_SETTINGS
|
||||
g: window.g
|
||||
}
|
||||
},
|
||||
// backwards compatibility for extensions, should not be used in the future
|
||||
|
||||
@@ -39,6 +39,9 @@ window.PageAccount = {
|
||||
color: 'pink-3'
|
||||
}
|
||||
],
|
||||
defaultSiteCustomisation: {
|
||||
locale: 'en'
|
||||
},
|
||||
reactionOptions: [
|
||||
'None',
|
||||
'confettiBothSides',
|
||||
@@ -213,26 +216,18 @@ window.PageAccount = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
activeLanguage(lang) {
|
||||
return window.i18n.global.locale === lang
|
||||
},
|
||||
changeLanguage(newValue) {
|
||||
window.i18n.global.locale = newValue
|
||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||
},
|
||||
async updateAccount() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
'/api/v1/auth/update',
|
||||
null,
|
||||
{
|
||||
user_id: this.g.user.id,
|
||||
username: this.g.user.username,
|
||||
email: this.g.user.email,
|
||||
extra: this.g.user.extra
|
||||
}
|
||||
)
|
||||
const {data} = await LNbits.api.request('PATCH', '/api/v1/auth', null, {
|
||||
user_id: this.g.user.id,
|
||||
username: this.g.user.username,
|
||||
email: this.g.user.email,
|
||||
extra: this.g.user.extra
|
||||
})
|
||||
this.untouchedUser = JSON.parse(JSON.stringify(this.g.user))
|
||||
this.hasUsername = !!data.username
|
||||
Quasar.Notify.create({
|
||||
@@ -680,6 +675,37 @@ window.PageAccount = {
|
||||
l => l.name !== label.name
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
async siteCustomisationChanged(options = {}) {
|
||||
try {
|
||||
Object.entries(options || {}).forEach(([key, value]) => {
|
||||
if (key in this.g) {
|
||||
this.g[key] = value
|
||||
}
|
||||
})
|
||||
await LNbits.api.updateUiCustomization(options)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'UI Customization updated.'
|
||||
})
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
},
|
||||
resetThemeDefaults() {
|
||||
const defaults = {
|
||||
themeChoice: this.g.settings.defaultTheme,
|
||||
borderChoice: this.g.settings.defaultBorder,
|
||||
gradientChoice: this.g.settings.defaultGradient,
|
||||
bgimageChoice: this.g.settings.defaultBgimage || '',
|
||||
reactionChoice: this.g.settings.defaultReaction,
|
||||
darkChoice: this.g.settings.defaultDark,
|
||||
cardRoundedChoice: this.g.settings.defaultCardRounded,
|
||||
cardGradientChoice: this.g.settings.defaultCardGradient,
|
||||
cardShadowChoice: this.g.settings.defaultCardShadow
|
||||
}
|
||||
this.siteCustomisationChanged(defaults)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -693,7 +719,7 @@ window.PageAccount = {
|
||||
await this.getUserAssets()
|
||||
// filter out themes that are not allowed
|
||||
this.themeOptions = this.themeOptions.filter(theme =>
|
||||
this.LNBITS_THEME_OPTIONS.includes(theme.name)
|
||||
this.g.settings.themeOptions.includes(theme.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ window.PageExtensions = {
|
||||
paylinkWebsocket: null,
|
||||
searchToggle: false,
|
||||
reviewsUrl: null,
|
||||
permissionsDialog: {
|
||||
show: false,
|
||||
extension: null,
|
||||
checked: [],
|
||||
missing: [],
|
||||
tags: [],
|
||||
tagOptions: []
|
||||
},
|
||||
reviewsDialog: {
|
||||
show: false,
|
||||
extension: null,
|
||||
@@ -92,6 +100,20 @@ window.PageExtensions = {
|
||||
this.filterExtensions(this.searchTerm, val)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
permissionsAllChecked() {
|
||||
const ext = this.permissionsDialog.extension
|
||||
if (!ext || !Array.isArray(ext.permissions)) return true
|
||||
const required = ext.permissions.map(p => p.id)
|
||||
return required.every(p => this.permissionsDialog.checked.includes(p))
|
||||
},
|
||||
permissionsHasMissingEndpoints() {
|
||||
return (
|
||||
this.permissionsDialog.missing &&
|
||||
this.permissionsDialog.missing.length > 0
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterExtensions(term, tab) {
|
||||
// Filter the extensions list
|
||||
@@ -241,18 +263,49 @@ window.PageExtensions = {
|
||||
})
|
||||
},
|
||||
async enableExtensionForUser(extension) {
|
||||
if (extension.permissions && extension.permissions.length) {
|
||||
if (!extension._grantedPermissions) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'Save permissions before enabling this extension.'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if (extension.paymentTags && extension.paymentTags.length) {
|
||||
if (
|
||||
!extension._grantedPaymentTags ||
|
||||
!extension._grantedPaymentTags.length
|
||||
) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'Select payment tags before enabling this extension.'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if (extension.isPaymentRequired) {
|
||||
this.showPayToEnable(extension)
|
||||
return
|
||||
}
|
||||
this.enableExtension(extension)
|
||||
},
|
||||
async enableExtension(extension) {
|
||||
async enableExtension(extension, permissions) {
|
||||
const granted =
|
||||
permissions ||
|
||||
extension._grantedPermissions ||
|
||||
(extension.permissions ? extension.permissions.map(p => p.id) : [])
|
||||
const tags =
|
||||
extension._grantedPaymentTags ||
|
||||
(extension.paymentTags ? extension.paymentTags.slice() : [])
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/${extension.id}/enable`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
this.g.user.wallets[0].adminkey,
|
||||
granted.length || tags.length
|
||||
? {permissions: granted, payment_tags: tags}
|
||||
: null
|
||||
)
|
||||
.then(response => {
|
||||
this.g.user.extensions = this.g.user.extensions.concat([extension.id])
|
||||
@@ -287,13 +340,116 @@ window.PageExtensions = {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
showPayToEnable(extension) {
|
||||
showPayToEnable(extension, permissions) {
|
||||
this.selectedExtension = extension
|
||||
if (permissions) {
|
||||
this.selectedExtension._grantedPermissions = permissions
|
||||
}
|
||||
this.selectedExtension.payToEnable.paidAmount =
|
||||
extension.payToEnable.amount
|
||||
this.selectedExtension.payToEnable.showQRCode = false
|
||||
this.showPayToEnableDialog = true
|
||||
},
|
||||
openPermissionsDialog(extension) {
|
||||
this.permissionsDialog.extension = extension
|
||||
this.permissionsDialog.checked = extension._grantedPermissions
|
||||
? extension._grantedPermissions.slice()
|
||||
: extension.grantedPermissions
|
||||
? extension.grantedPermissions.slice()
|
||||
: []
|
||||
this.permissionsDialog.missing = []
|
||||
this.permissionsDialog.tags = extension._grantedPaymentTags
|
||||
? extension._grantedPaymentTags.slice()
|
||||
: extension.grantedPaymentTags
|
||||
? extension.grantedPaymentTags.slice()
|
||||
: []
|
||||
this.permissionsDialog.tagOptions = []
|
||||
this.permissionsDialog.show = true
|
||||
},
|
||||
cancelPermissionsDialog() {
|
||||
this.permissionsDialog.show = false
|
||||
this.permissionsDialog.extension = null
|
||||
this.permissionsDialog.checked = []
|
||||
this.permissionsDialog.missing = []
|
||||
this.permissionsDialog.tags = []
|
||||
this.permissionsDialog.tagOptions = []
|
||||
},
|
||||
async openPermissionsForExtension(extension) {
|
||||
if (!extension.permissions || !extension.permissions.length) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'This extension does not declare permissions.'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.permissionsDialog.extension = extension
|
||||
this.permissionsDialog.checked = extension._grantedPermissions
|
||||
? extension._grantedPermissions.slice()
|
||||
: extension.grantedPermissions
|
||||
? extension.grantedPermissions.slice()
|
||||
: []
|
||||
this.permissionsDialog.missing = []
|
||||
this.permissionsDialog.tags = extension._grantedPaymentTags
|
||||
? extension._grantedPaymentTags.slice()
|
||||
: extension.grantedPaymentTags
|
||||
? extension.grantedPaymentTags.slice()
|
||||
: []
|
||||
this.permissionsDialog.tagOptions = []
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/extension/${extension.id}/capabilities`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
if (data && Array.isArray(data.missing_permissions)) {
|
||||
this.permissionsDialog.missing = data.missing_permissions
|
||||
}
|
||||
if (data && Array.isArray(data.payment_tags)) {
|
||||
this.permissionsDialog.tagOptions = data.payment_tags
|
||||
}
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
this.permissionsDialog.show = true
|
||||
},
|
||||
async confirmPermissionsDialog() {
|
||||
const ext = this.permissionsDialog.extension
|
||||
const granted = this.permissionsDialog.checked.slice()
|
||||
const tags = this.permissionsDialog.tags.slice()
|
||||
this.permissionsDialog.show = false
|
||||
this.permissionsDialog.extension = null
|
||||
this.permissionsDialog.checked = []
|
||||
const missing = this.permissionsDialog.missing || []
|
||||
this.permissionsDialog.missing = []
|
||||
this.permissionsDialog.tags = []
|
||||
this.permissionsDialog.tagOptions = []
|
||||
if (missing.length) {
|
||||
Quasar.Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Missing API endpoints for one or more permissions.'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!ext) return
|
||||
ext._grantedPermissions = granted
|
||||
ext.grantedPermissions = granted
|
||||
ext._grantedPaymentTags = tags
|
||||
ext.grantedPaymentTags = tags
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/${ext.id}/permissions`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{permissions: granted, payment_tags: tags}
|
||||
)
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Permissions saved.'
|
||||
})
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
},
|
||||
updatePayToInstallData(extension) {
|
||||
LNbits.api
|
||||
.request(
|
||||
@@ -832,6 +988,16 @@ window.PageExtensions = {
|
||||
async fetchAllExtensions() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
|
||||
data.forEach(ext => {
|
||||
if (ext.grantedPermissions && ext.grantedPermissions.length) {
|
||||
ext._grantedPermissions = ext.grantedPermissions
|
||||
}
|
||||
if (ext.grantedPaymentTags && ext.grantedPaymentTags.length) {
|
||||
ext._grantedPaymentTags = ext.grantedPaymentTags
|
||||
}
|
||||
const schema = ext.kvSchema || ext.kv_schema || {}
|
||||
ext.kvSchemaKeys = Object.keys(schema)
|
||||
})
|
||||
return data
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
@@ -842,8 +1008,8 @@ window.PageExtensions = {
|
||||
},
|
||||
async created() {
|
||||
this.extensions = await this.fetchAllExtensions()
|
||||
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
|
||||
this.reviewsUrl = this.LNBITS_EXTENSIONS_REVIEWS_URL
|
||||
this.extbuilderEnabled = this.g.user.admin || this.g.settings.extBuilder
|
||||
this.reviewsUrl = this.g.settings.extensionsReviewsUrl
|
||||
|
||||
if (this.g.user.extensions.length === 0) {
|
||||
this.tab = 'all'
|
||||
|
||||
@@ -7,7 +7,8 @@ window.PageFirstInstall = {
|
||||
isPwdRepeat: true,
|
||||
username: '',
|
||||
password: '',
|
||||
passwordRepeat: ''
|
||||
passwordRepeat: '',
|
||||
firstInstallToken: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -17,20 +18,25 @@ window.PageFirstInstall = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async setPassword() {
|
||||
try {
|
||||
await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
|
||||
setPassword() {
|
||||
LNbits.api
|
||||
.request('PUT', `/api/v1/auth/first_install`, null, {
|
||||
username: this.loginData.username,
|
||||
password: this.loginData.password,
|
||||
password_repeat: this.loginData.passwordRepeat
|
||||
password_repeat: this.loginData.passwordRepeat,
|
||||
first_install_token: this.loginData.firstInstallToken
|
||||
})
|
||||
window.location.href = '/admin'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
.then(async () => {
|
||||
const res = await LNbits.api.getAuthUser()
|
||||
this.g.user = LNbits.map.user(res.data)
|
||||
this.g.isPublicPage = false
|
||||
this.$router.push('/admin')
|
||||
})
|
||||
.catch(this.utils.notifyApiError)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
document.title = 'First Install - LNbits'
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
this.loginData.firstInstallToken = params.get('token') || ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ window.PageHome = {
|
||||
email: '',
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
invitationCode: '',
|
||||
walletName: '',
|
||||
signup: false
|
||||
}
|
||||
@@ -19,40 +20,15 @@ window.PageHome = {
|
||||
showClaimLnurl() {
|
||||
return (
|
||||
this.lnurl !== '' &&
|
||||
this.allowRegister &&
|
||||
'user-id-only' in this.LNBITS_AUTH_METHODS
|
||||
this.g.settings.allowRegister &&
|
||||
'user-id-only' in this.g.settings.authMethods
|
||||
)
|
||||
},
|
||||
formatDescription() {
|
||||
return LNbits.utils.convertMarkdown(this.SITE_DESCRIPTION)
|
||||
return LNbits.utils.convertMarkdown(this.g.settings.siteDescription)
|
||||
},
|
||||
isAccessTokenExpired() {
|
||||
return this.$q.cookies.get('is_access_token_expired')
|
||||
},
|
||||
allowRegister() {
|
||||
return this.LNBITS_NEW_ACCOUNTS_ALLOWED
|
||||
},
|
||||
// TODO: this makes no sense
|
||||
hasCustomImage() {
|
||||
return this.LNBITS_CUSTOM_IMAGE
|
||||
},
|
||||
showHomepageElements() {
|
||||
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS === true
|
||||
},
|
||||
siteTitle() {
|
||||
return this.SITE_TITLE || ''
|
||||
},
|
||||
siteTagline() {
|
||||
return this.SITE_TAGLINE || ''
|
||||
},
|
||||
adsEnabled() {
|
||||
return this.AD_SPACE_ENABLED && this.AD_SPACE && this.AD_SPACE.length > 0
|
||||
},
|
||||
adsTitle() {
|
||||
return this.AD_SPACE_TITLE || ''
|
||||
},
|
||||
ads() {
|
||||
return this.AD_SPACE.map(ad => ad.split(';'))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -65,6 +41,7 @@ window.PageHome = {
|
||||
this.username = null
|
||||
this.password = null
|
||||
this.passwordRepeat = null
|
||||
this.invitationCode = null
|
||||
|
||||
this.authAction = 'register'
|
||||
this.authMethod = authMethod
|
||||
@@ -76,7 +53,8 @@ window.PageHome = {
|
||||
this.username,
|
||||
this.email,
|
||||
this.password,
|
||||
this.passwordRepeat
|
||||
this.passwordRepeat,
|
||||
this.invitationCode
|
||||
)
|
||||
this.refreshAuthUser()
|
||||
} catch (e) {
|
||||
|
||||
@@ -70,10 +70,10 @@ window.PageUsers = {
|
||||
usersTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'admin',
|
||||
name: 'activated',
|
||||
align: 'left',
|
||||
label: 'Admin',
|
||||
field: 'admin',
|
||||
label: this.$t('activated'),
|
||||
field: 'activated',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
@@ -401,7 +401,7 @@ window.PageUsers = {
|
||||
|
||||
toggleAdmin(userId) {
|
||||
LNbits.api
|
||||
.request('GET', `/users/api/v1/user/${userId}/admin`)
|
||||
.request('PUT', `/users/api/v1/user/${userId}/admin`)
|
||||
.then(() => {
|
||||
this.fetchUsers()
|
||||
Quasar.Notify.create({
|
||||
@@ -412,6 +412,19 @@ window.PageUsers = {
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
toggleUserActivated(userId) {
|
||||
LNbits.api
|
||||
.request('PUT', `/users/api/v1/user/${userId}/activate`)
|
||||
.then(res => {
|
||||
this.fetchUsers()
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: res.data.message,
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
async showAccountPage(user_id) {
|
||||
this.activeUser.showPassword = false
|
||||
this.activeUser.showUserId = false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
window._lnbitsUtils = {
|
||||
url_for(url) {
|
||||
const _url = new URL(url, window.location.origin)
|
||||
_url.searchParams.set('v', WINDOW_SETTINGS.CACHE_KEY)
|
||||
_url.searchParams.set('v', window.g.settings.cacheKey)
|
||||
return _url.toString()
|
||||
},
|
||||
loadScript(src) {
|
||||
@@ -144,7 +144,7 @@ window._lnbitsUtils = {
|
||||
return null
|
||||
}
|
||||
},
|
||||
notifyApiError(error) {
|
||||
async notifyApiError(error) {
|
||||
if (!error.response) {
|
||||
return console.error(error)
|
||||
}
|
||||
@@ -154,6 +154,10 @@ window._lnbitsUtils = {
|
||||
500: 'negative'
|
||||
}
|
||||
let messages = error.response.data.detail
|
||||
if (!messages) {
|
||||
const text = await error.response.data?.text()
|
||||
messages = this.parseJSONSafe(text)?.detail
|
||||
}
|
||||
if (messages) {
|
||||
messages = Array.isArray(messages)
|
||||
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@use 'themes';
|
||||
@use 'borders';
|
||||
@use 'background';
|
||||
@use 'cards';
|
||||
@use 'home';
|
||||
|
||||
[v-cloak] {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
@use 'sass:color';
|
||||
@use 'sass:map';
|
||||
@use 'themes';
|
||||
|
||||
$card-radius: 10px;
|
||||
|
||||
body.rounded-ui {
|
||||
.q-card,
|
||||
.q-btn {
|
||||
border-radius: $card-radius;
|
||||
}
|
||||
}
|
||||
|
||||
@each $theme, $colors in themes.$themes {
|
||||
$primary: map.get($colors, primary);
|
||||
$secondary: map.get($colors, secondary);
|
||||
$dark: map.get($colors, dark);
|
||||
|
||||
body[data-theme='#{$theme}'].card-gradient {
|
||||
.q-card:not(
|
||||
.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark
|
||||
) {
|
||||
background-image: linear-gradient(
|
||||
135deg,
|
||||
color.adjust($primary, $alpha: -0.92),
|
||||
color.adjust($secondary, $alpha: -0.94)
|
||||
);
|
||||
}
|
||||
.q-drawer {
|
||||
background-image: linear-gradient(
|
||||
165deg,
|
||||
color.adjust($primary, $alpha: -0.94),
|
||||
color.adjust($secondary, $alpha: -0.96)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
body[data-theme='#{$theme}'].card-gradient.body--dark {
|
||||
.q-card:not(
|
||||
.q-dialog .q-card,
|
||||
.lnbits__dialog-card,
|
||||
.q-dialog-plugin--dark
|
||||
) {
|
||||
background-image: linear-gradient(
|
||||
135deg,
|
||||
color.adjust($dark, $lightness: 4%),
|
||||
color.adjust($primary, $alpha: -0.93)
|
||||
);
|
||||
}
|
||||
.q-drawer {
|
||||
background-image: linear-gradient(
|
||||
165deg,
|
||||
color.adjust($dark, $lightness: 3%),
|
||||
color.adjust($primary, $alpha: -0.95)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.card-shadow {
|
||||
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
}
|
||||
|
||||
body.card-shadow.body--dark {
|
||||
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
}
|
||||
Vendored
+782
-900
File diff suppressed because it is too large
Load Diff
+10
-1
@@ -96,6 +96,11 @@ def register_invoice_listener(send_chan: asyncio.Queue, name: str | None = None)
|
||||
invoice_listeners[name] = send_chan
|
||||
|
||||
|
||||
def unregister_invoice_listener(name: str) -> None:
|
||||
if name in invoice_listeners:
|
||||
invoice_listeners.pop(name, None)
|
||||
|
||||
|
||||
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
||||
|
||||
|
||||
@@ -178,7 +183,11 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
|
||||
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
|
||||
return
|
||||
|
||||
status = await payment.check_status(skip_internal_payment_notifications=True)
|
||||
from lnbits.core.services.payments import check_payment_status
|
||||
|
||||
status = await check_payment_status(
|
||||
payment, skip_internal_payment_notifications=True
|
||||
)
|
||||
payment.fee = status.fee_msat or payment.fee
|
||||
# only overwrite preimage if status.preimage provides it
|
||||
payment.preimage = status.preimage or payment.preimage
|
||||
|
||||
@@ -70,10 +70,10 @@
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
const RENDERED_ROUTE = '{{ normalize_path(request.path) }}'
|
||||
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
|
||||
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||
window[key] = WINDOW_SETTINGS[key]
|
||||
})
|
||||
const SETTINGS = {{ SETTINGS | tojson | safe }}
|
||||
const CURRENCIES = {{ CURRENCIES | tojson | safe }}
|
||||
// backwards compatibility for extensions (tpos)
|
||||
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson | safe }}
|
||||
</script>
|
||||
<!-- vue templates -->
|
||||
{% include('components.vue') %} {% include('pages.vue') %}
|
||||
|
||||
@@ -36,7 +36,7 @@ include('components/lnbits-error.vue') %}
|
||||
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
|
||||
<q-item-label header v-text="$t('manage')"></q-item-label>
|
||||
<div v-if="g.user.admin">
|
||||
<q-item v-if="showAdmin" to="/admin">
|
||||
<q-item v-if="g.settings.showAdmin" to="/admin">
|
||||
<q-item-section side>
|
||||
<q-icon
|
||||
name="settings"
|
||||
@@ -51,7 +51,7 @@ include('components/lnbits-error.vue') %}
|
||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="showNode" to="/node">
|
||||
<q-item v-if="g.settings.showNodemanager" to="/node">
|
||||
<q-item-section side>
|
||||
<q-icon
|
||||
name="developer_board"
|
||||
@@ -66,7 +66,7 @@ include('components/lnbits-error.vue') %}
|
||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="showUsers" to="/users">
|
||||
<q-item v-if="g.settings.showAdmin" to="/users">
|
||||
<q-item-section side>
|
||||
<q-icon
|
||||
name="groups"
|
||||
@@ -81,7 +81,7 @@ include('components/lnbits-error.vue') %}
|
||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="showAudit" to="/audit">
|
||||
<q-item v-if="g.settings.showAudit" to="/audit">
|
||||
<q-item-section side>
|
||||
<q-icon
|
||||
name="playlist_add_check_circle"
|
||||
@@ -112,7 +112,7 @@ include('components/lnbits-error.vue') %}
|
||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="showExtensions" to="/extensions">
|
||||
<q-item v-if="g.settings.showExtensions" to="/extensions">
|
||||
<q-item-section side>
|
||||
<q-icon
|
||||
name="extension"
|
||||
@@ -686,7 +686,7 @@ include('components/lnbits-error.vue') %}
|
||||
dense
|
||||
filled
|
||||
v-model="walletName"
|
||||
:label="$t('name_your_wallet', {name: SITE_TITLE + ' *'})"
|
||||
:label="$t('name_your_wallet', {name: g.settings.siteTitle + ' *'})"
|
||||
></q-input>
|
||||
<q-card-actions vertical align="center" class="q-pa-none">
|
||||
<q-btn
|
||||
@@ -814,16 +814,90 @@ include('components/lnbits-error.vue') %}
|
||||
type="password"
|
||||
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
></q-input>
|
||||
<div
|
||||
v-if="confirmationMethodsCount > 1"
|
||||
class="row justify-center q-mb-md"
|
||||
>
|
||||
<q-tabs
|
||||
v-model="confirmationMethod"
|
||||
dense
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
>
|
||||
<q-tab
|
||||
v-if="g.settings.userActivationByInvitationCode"
|
||||
name="code"
|
||||
icon="confirmation_number"
|
||||
label="Code"
|
||||
></q-tab>
|
||||
<q-tab
|
||||
v-if="g.settings.userActivationByPayment"
|
||||
name="payment"
|
||||
icon="bolt"
|
||||
label="Payment"
|
||||
></q-tab>
|
||||
<q-tab
|
||||
v-if="g.settings.userActivationByEmail"
|
||||
name="email"
|
||||
icon="email"
|
||||
label="Email"
|
||||
></q-tab>
|
||||
</q-tabs>
|
||||
</div>
|
||||
<div v-if="confirmationMethodsCount > 0" class="q-mb-md">
|
||||
<q-tab-panels v-model="confirmationMethod">
|
||||
<q-tab-panel name="code" class="q-pa-none">
|
||||
<div
|
||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
||||
>
|
||||
<q-icon name="info" color="orange-4" class="q-mr-xs"></q-icon>
|
||||
You need an invitation code to register.
|
||||
</div>
|
||||
<div>
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="confirmationCode"
|
||||
:label="$t('invitation_code')"
|
||||
:type="showConfirmationCode ? 'text' : 'password'"
|
||||
:hint="$t('invitation_code_hint')"
|
||||
>
|
||||
<q-btn
|
||||
@click="showConfirmationCode = !showConfirmationCode"
|
||||
dense
|
||||
flat
|
||||
:icon="
|
||||
showConfirmationCode ? 'visibility_off' : 'visibility'
|
||||
"
|
||||
color="grey"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="payment">
|
||||
<div>payment</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="email" class="q-pa-none">
|
||||
<div>
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="confirmationEmail"
|
||||
:label="$t('email')"
|
||||
:hint="$t('email_confirmation_hint')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
|
||||
<div class="row justify-end">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="
|
||||
!password ||
|
||||
!passwordRepeat ||
|
||||
!username ||
|
||||
password !== passwordRepeat
|
||||
"
|
||||
:disable="disableRegister"
|
||||
type="submit"
|
||||
class="full-width"
|
||||
:label="$t('create_account')"
|
||||
@@ -960,9 +1034,9 @@ include('components/lnbits-error.vue') %}
|
||||
<q-avatar size="32px" class="q-mr-md">
|
||||
<q-img
|
||||
:src="
|
||||
keycloakIcon
|
||||
? keycloakIcon
|
||||
: 'lnbits/static/images/keycloak-logo.png'
|
||||
g.settings.keycloakIcon
|
||||
? g.settings.keycloakIcon
|
||||
: utils.url_for('lnbits/static/images/keycloak-logo.png')
|
||||
"
|
||||
></q-img>
|
||||
</q-avatar>
|
||||
@@ -970,7 +1044,7 @@ include('components/lnbits-error.vue') %}
|
||||
<span
|
||||
v-text="
|
||||
$t('signin_with_custom_org', {
|
||||
custom_org: keycloakOrg
|
||||
custom_org: g.settings.keycloakOrg || 'Keycloak'
|
||||
})
|
||||
"
|
||||
></span>
|
||||
|
||||
@@ -408,7 +408,6 @@
|
||||
<li><code>CHECKOUT.ORDER.APPROVED</code></li>
|
||||
<li><code>PAYMENT.CAPTURE.COMPLETED</code></li>
|
||||
<li><code>PAYMENT.SALE.COMPLETED</code></li>
|
||||
<li><code>BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED</code></li>
|
||||
</ul>
|
||||
</q-card-section>
|
||||
</q-expansion-item>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<br />
|
||||
</div>
|
||||
<div class="col">
|
||||
<div v-if="LNBITS_NODE_UI">
|
||||
<div v-if="g.settings.hasNodemanager">
|
||||
<p>
|
||||
<span v-text="$t('node_management')"></span>
|
||||
</p>
|
||||
@@ -70,7 +70,7 @@
|
||||
v-model="formData.lnbits_node_ui_transactions"
|
||||
></q-toggle>
|
||||
</div>
|
||||
<p v-if="!LNBITS_NODE_UI">
|
||||
<p v-if="!g.settings.hasNodemanager">
|
||||
<span v-text="$t('node_management_not_supported')"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -310,7 +310,13 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<q-select
|
||||
filled
|
||||
:options="[$t('second'), $t('minute'), $t('hour')]"
|
||||
:options="[
|
||||
{label: $t('second'), value: 'second'},
|
||||
{label: $t('minute'), value: 'minute'},
|
||||
{label: $t('hour'), value: 'hour'}
|
||||
]"
|
||||
emit-value
|
||||
map-options
|
||||
v-model="formData.lnbits_rate_limit_unit"
|
||||
:label="$t('time_unit')"
|
||||
></q-select>
|
||||
|
||||
@@ -86,6 +86,42 @@
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-4">
|
||||
<p>
|
||||
<span v-text="$t('wallet_featured_button_label')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_wallet_featured_button_label"
|
||||
label="Loop to Onchain"
|
||||
:hint="$t('wallet_featured_button_label_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('wallet_featured_button_url')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_wallet_featured_button_url"
|
||||
label="/boltz"
|
||||
:hint="$t('wallet_featured_button_url_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('wallet_featured_button_icon')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_wallet_featured_button_icon"
|
||||
label="bolt"
|
||||
:hint="$t('wallet_featured_button_icon_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('ui_custom_badge')"></span></p>
|
||||
@@ -221,12 +257,50 @@
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-3">
|
||||
</div>
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-12 col-sm-6 col-lg-2">
|
||||
<q-toggle
|
||||
type="bool"
|
||||
v-model="formData.lnbits_default_gradient"
|
||||
color="primary"
|
||||
label="Gradient Background"
|
||||
:label="$t('gradient_background')"
|
||||
>
|
||||
</q-toggle>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-2">
|
||||
<q-toggle
|
||||
type="bool"
|
||||
v-model="formData.lnbits_default_dark"
|
||||
color="primary"
|
||||
:label="$t('toggle_darkmode')"
|
||||
>
|
||||
</q-toggle>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-2">
|
||||
<q-toggle
|
||||
type="bool"
|
||||
v-model="formData.lnbits_default_card_rounded"
|
||||
color="primary"
|
||||
:label="$t('rounded_ui')"
|
||||
>
|
||||
</q-toggle>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-2">
|
||||
<q-toggle
|
||||
type="bool"
|
||||
v-model="formData.lnbits_default_card_gradient"
|
||||
color="primary"
|
||||
:label="$t('card_gradient')"
|
||||
>
|
||||
</q-toggle>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-2">
|
||||
<q-toggle
|
||||
type="bool"
|
||||
v-model="formData.lnbits_default_card_shadow"
|
||||
color="primary"
|
||||
:label="$t('card_shadow')"
|
||||
>
|
||||
</q-toggle>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
>
|
||||
</q-chip>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('allowed_users')"></span></p>
|
||||
@@ -57,7 +56,10 @@
|
||||
>
|
||||
</q-chip>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 q-mt-sm">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('allow_creation_user')"></q-item-label>
|
||||
@@ -76,8 +78,237 @@
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6 q-mt-sm">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('require_user_activation')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('require_user_activation_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_require_user_activation"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<div v-if="formData.lnbits_require_user_activation" class="row">
|
||||
<div class="col">
|
||||
<q-list bordered class="rounded-borders">
|
||||
<q-expansion-item header-class="text-primary text-bold">
|
||||
<template v-slot:header>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="confirmation_number" size="md"></q-icon>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section> Invitation Code </q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="row items-center">
|
||||
<q-toggle
|
||||
size="md"
|
||||
:label="
|
||||
formData.lnbits_user_activation_by_invitation_code
|
||||
? $t('enabled')
|
||||
: $t('disabled')
|
||||
"
|
||||
v-model="formData.lnbits_user_activation_by_invitation_code"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</template>
|
||||
|
||||
<q-card class="q-pb-xl">
|
||||
<q-card-section>
|
||||
<div
|
||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
||||
>
|
||||
<q-icon
|
||||
name="info"
|
||||
color="orange-4"
|
||||
size="18px"
|
||||
class="q-mr-xs"
|
||||
></q-icon>
|
||||
Users will need to provide a valid invitation code during
|
||||
registration to activate their account.
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 q-pr-sm">
|
||||
<p><span v-text="$t('reusable_activation_code')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_register_reusable_activation_code"
|
||||
:type="showReusableActivationCode ? 'text' : 'password'"
|
||||
:label="$t('reusable_activation_code_label')"
|
||||
:hint="$t('reusable_activation_code_hint')"
|
||||
>
|
||||
<q-btn
|
||||
@click="
|
||||
showReusableActivationCode = !showReusableActivationCode
|
||||
"
|
||||
dense
|
||||
flat
|
||||
:icon="
|
||||
showReusableActivationCode
|
||||
? 'visibility_off'
|
||||
: 'visibility'
|
||||
"
|
||||
color="grey"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
@click="
|
||||
utils.copyText(
|
||||
formData.lnbits_register_reusable_activation_code
|
||||
)
|
||||
"
|
||||
dense
|
||||
flat
|
||||
icon="content_copy"
|
||||
color="grey"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('one_time_activation_code')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAddActivationCode"
|
||||
@keydown.enter="addOneTimeActivationCode"
|
||||
type="text"
|
||||
:label="$t('one_time_activation_code_label')"
|
||||
:hint="$t('one_time_activation_code_hint')"
|
||||
>
|
||||
<q-btn
|
||||
@click="addOneTimeActivationCode"
|
||||
dense
|
||||
flat
|
||||
icon="add"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
<q-chip
|
||||
v-for="code in formData.lnbits_register_one_time_activation_codes"
|
||||
:key="code"
|
||||
removable
|
||||
@remove="removeOneTimeActivationCode(code)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
:label="code"
|
||||
class="ellipsis"
|
||||
>
|
||||
</q-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-expansion-item header-class="text-primary text-bold">
|
||||
<template v-slot:header>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<q-icon name="bolt" size="md"></q-icon>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section> Payment </q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="row items-center">
|
||||
<q-toggle
|
||||
size="md"
|
||||
disable
|
||||
:label="
|
||||
formData.lnbits_user_activation_by_payment
|
||||
? $t('enabled')
|
||||
: $t('disabled')
|
||||
"
|
||||
v-model="formData.lnbits_user_activation_by_payment"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</template>
|
||||
|
||||
<q-card class="q-pb-xl">
|
||||
<q-card-section>
|
||||
<div
|
||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
||||
>
|
||||
<q-icon
|
||||
name="info"
|
||||
color="orange-4"
|
||||
size="18px"
|
||||
class="q-mr-xs"
|
||||
></q-icon>
|
||||
Users will need to make a payment during registration to
|
||||
activate their account.
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item header-class="text-primary text-bold">
|
||||
<template v-slot:header>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<q-icon name="email"></q-icon>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section> Email </q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="row items-center">
|
||||
<q-toggle
|
||||
disable
|
||||
size="md"
|
||||
:label="
|
||||
formData.lnbits_user_activation_by_email
|
||||
? $t('enabled')
|
||||
: $t('disabled')
|
||||
"
|
||||
v-model="formData.lnbits_user_activation_by_email"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</template>
|
||||
|
||||
<q-card class="q-pb-xl">
|
||||
<q-card-section>
|
||||
<div
|
||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
||||
>
|
||||
<q-icon
|
||||
name="info"
|
||||
color="orange-4"
|
||||
size="18px"
|
||||
class="q-mr-xs"
|
||||
></q-icon>
|
||||
Users will receive a confirmation email.
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template id="lnbits-footer">
|
||||
<q-footer
|
||||
v-if="showFooter"
|
||||
v-if="g.settings.showHomePageElements"
|
||||
class="bg-transparent q-px-lg q-py-md"
|
||||
:class="{'text-dark': !$q.dark.isActive}"
|
||||
>
|
||||
@@ -9,7 +9,7 @@
|
||||
<q-toolbar-title class="text-caption">
|
||||
<span v-text="title"></span>
|
||||
<br />
|
||||
<small v-text="$t('lnbits_version') + ': ' + version"></small>
|
||||
<small v-text="version"></small>
|
||||
</q-toolbar-title>
|
||||
<q-space></q-space>
|
||||
<q-btn
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<template id="lnbits-header">
|
||||
<q-header bordered class="bg-marginal-bg">
|
||||
<q-banner v-if="showVoidwallet" class="bg-warning text-white" dense>
|
||||
<q-banner
|
||||
v-if="g.settings.showVoidwallet"
|
||||
class="bg-warning text-white"
|
||||
dense
|
||||
>
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="warning" color="white" />
|
||||
</template>
|
||||
@@ -24,13 +28,15 @@
|
||||
<q-toolbar-title>
|
||||
<q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
|
||||
<q-img
|
||||
v-if="customLogoUrl"
|
||||
v-if="customLogo"
|
||||
height="30px"
|
||||
alt="Logo"
|
||||
:src="customLogoUrl"
|
||||
:src="customLogo"
|
||||
></q-img>
|
||||
<span v-else-if="!titleIsLnbits"><strong>LN</strong>bits</span>
|
||||
<span v-else v-text="title"></span>
|
||||
<span v-else-if="g.settings.siteTitle == 'LNbits'"
|
||||
><strong>LN</strong>bits</span
|
||||
>
|
||||
<span v-else v-text="g.settings.siteTitle"></span>
|
||||
</q-btn>
|
||||
<q-badge v-if="g.user && g.user.super_user">Super User</q-badge>
|
||||
<q-badge v-else-if="g.user && g.user.admin">Admin User</q-badge>
|
||||
@@ -38,31 +44,31 @@
|
||||
<q-badge
|
||||
class="q-mr-md"
|
||||
v-show="$q.screen.gt.sm"
|
||||
v-if="hasCustomBadge"
|
||||
:label="customBadge"
|
||||
:color="customBadgeColor"
|
||||
v-if="g.settings.customBadge"
|
||||
:label="g.settings.customBadge"
|
||||
:color="g.settings.customBadgeColor"
|
||||
>
|
||||
</q-badge>
|
||||
|
||||
<q-badge
|
||||
v-show="$q.screen.gt.sm"
|
||||
v-if="hasServiceFee"
|
||||
v-if="g.user && g.settings.serviceFee > 0"
|
||||
color="green"
|
||||
class="q-mr-md"
|
||||
>
|
||||
<span
|
||||
v-if="hasServiceFeeMax"
|
||||
v-if="g.user && g.settings.serviceFeeMax > 0"
|
||||
v-text="
|
||||
$t('service_fee_max_badge', {
|
||||
amount: serviceFee,
|
||||
max: serviceFeeMax,
|
||||
amount: g.settings.serviceFee,
|
||||
max: g.settings.serviceFeeMax,
|
||||
denom: g.denomination
|
||||
})
|
||||
"
|
||||
></span>
|
||||
<span
|
||||
v-else
|
||||
v-text="$t('service_fee_badge', {amount: serviceFee})"
|
||||
v-text="$t('service_fee_badge', {amount: g.settings.serviceFee})"
|
||||
></span>
|
||||
<q-tooltip><span v-text="$t('service_fee_tooltip')"></span></q-tooltip>
|
||||
</q-badge>
|
||||
@@ -71,8 +77,11 @@
|
||||
<span>OFFLINE</span>
|
||||
</q-badge>
|
||||
|
||||
<lnbits-language-dropdown></lnbits-language-dropdown>
|
||||
<q-btn-dropdown v-if="g.user" flat rounded size="sm" class="q-pl-sm">
|
||||
<lnbits-language-dropdown
|
||||
@language-changed="handleLanguageChanged({locale: $event})"
|
||||
></lnbits-language-dropdown>
|
||||
|
||||
<q-btn-dropdown v-if="g.user" flat rounded size="md" class="q-pl-sm">
|
||||
<template v-slot:label>
|
||||
<q-avatar
|
||||
v-if="g.user?.extra?.picture && g.user?.extra?.picture !== ''"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template id="lnbits-language-dropdown">
|
||||
<q-btn-dropdown dense flat rounded size="sm" icon="language" class="q-pl-md">
|
||||
<q-btn-dropdown dense flat rounded size="md" class="q-pl-md">
|
||||
<template v-slot:label>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="currentLanguage.display"></q-item-label>
|
||||
<q-tooltip><span v-text="currentLanguage.label"></span></q-tooltip>
|
||||
</q-item-section>
|
||||
</template>
|
||||
<q-list v-for="(lang, index) in langs" :key="index">
|
||||
<q-item
|
||||
clickable
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
<q-expansion-item
|
||||
v-if="!HIDE_API"
|
||||
v-if="!g.settings.hideApi"
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
@@ -152,7 +152,7 @@
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
v-if="!HIDE_API"
|
||||
v-if="!g.settings.hideApi"
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
@@ -197,7 +197,7 @@
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
v-if="!HIDE_API"
|
||||
v-if="!g.settings.hideApi"
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
@@ -244,7 +244,7 @@
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
v-if="!HIDE_API"
|
||||
v-if="!g.settings.hideApi"
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
@@ -273,7 +273,7 @@
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
v-if="!HIDE_API"
|
||||
v-if="!g.settings.hideApi"
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
|
||||
@@ -258,15 +258,20 @@
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<div v-if="!g.user.email" class="row"></div>
|
||||
<div v-if="!g.user.email" class="row">
|
||||
{% if "google-auth" in LNBITS_AUTH_METHODS or
|
||||
"github-auth" in LNBITS_AUTH_METHODS %}
|
||||
<div class="col q-pa-sm text-h6">
|
||||
<div
|
||||
v-if="
|
||||
'google-auth' in g.settings.authMethods ||
|
||||
'github-auth' in g.settings.authMethods
|
||||
"
|
||||
class="col q-pa-sm text-h6"
|
||||
>
|
||||
<span v-text="$t('verify_email')"></span>:
|
||||
</div>
|
||||
{%endif%} {% if "google-auth" in LNBITS_AUTH_METHODS %}
|
||||
<div class="col q-pa-sm">
|
||||
<div
|
||||
v-if="'google-auth' in g.settings.authMethods"
|
||||
class="col q-pa-sm"
|
||||
>
|
||||
<q-btn
|
||||
:href="`/api/v1/auth/google?user_id=${g.user.id}`"
|
||||
type="a"
|
||||
@@ -284,8 +289,10 @@
|
||||
<div>Google</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
{%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
|
||||
<div class="col q-pa-sm">
|
||||
<div
|
||||
v-if="'github-auth' in g.settings.authMethods"
|
||||
class="col q-pa-sm"
|
||||
>
|
||||
<q-btn
|
||||
:href="`/api/v1/auth/github?user_id=${g.user.id}`"
|
||||
type="a"
|
||||
@@ -303,7 +310,6 @@
|
||||
<div>GitHub</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
{%endif%}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
@@ -333,6 +339,16 @@
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="g.user.extra.visible_wallet_count"
|
||||
:label="$t('visible_wallet_count')"
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
class="q-mb-md"
|
||||
></q-input>
|
||||
|
||||
<q-input
|
||||
v-model="g.user.external_id"
|
||||
:label="$t('external_id')"
|
||||
@@ -380,22 +396,11 @@
|
||||
<span v-text="$t('language')"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<lnbits-language-dropdown />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">
|
||||
<span v-text="$t('visible_wallet_count')"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-input
|
||||
v-model="g.user.extra.visible_wallet_count"
|
||||
:label="$t('visible_wallet_count')"
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
class="q-mb-md"
|
||||
></q-input>
|
||||
<lnbits-language-dropdown
|
||||
@language-changed="
|
||||
siteCustomisationChanged({locale: $event})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -407,7 +412,9 @@
|
||||
<q-btn
|
||||
v-for="theme in themeOptions"
|
||||
:key="theme.name"
|
||||
@click="g.themeChoice = theme.name"
|
||||
@click="
|
||||
siteCustomisationChanged({themeChoice: theme.name})
|
||||
"
|
||||
:color="theme.color"
|
||||
dense
|
||||
flat
|
||||
@@ -427,6 +434,9 @@
|
||||
<q-input
|
||||
v-model="g.bgimageChoice"
|
||||
:label="$t('background_image')"
|
||||
@update:model-value="
|
||||
siteCustomisationChanged({bgimageChoice: $event})
|
||||
"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('background_image')"></span
|
||||
@@ -445,6 +455,9 @@
|
||||
round
|
||||
icon="gradient"
|
||||
v-model="g.gradientChoice"
|
||||
@update:model-value="
|
||||
siteCustomisationChanged({gradientChoice: $event})
|
||||
"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('toggle_gradient')"></span
|
||||
@@ -452,6 +465,69 @@
|
||||
</q-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">
|
||||
<span v-text="$t('rounded_ui')"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-toggle
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="rounded_corner"
|
||||
v-model="g.cardRoundedChoice"
|
||||
@update:model-value="
|
||||
siteCustomisationChanged({cardRoundedChoice: $event})
|
||||
"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('toggle_rounded_ui')"></span
|
||||
></q-tooltip>
|
||||
</q-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">
|
||||
<span v-text="$t('card_gradient')"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-toggle
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="gradient"
|
||||
v-model="g.cardGradientChoice"
|
||||
@update:model-value="
|
||||
siteCustomisationChanged({cardGradientChoice: $event})
|
||||
"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('toggle_card_gradient')"></span
|
||||
></q-tooltip>
|
||||
</q-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">
|
||||
<span v-text="$t('card_shadow')"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-toggle
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="blur_on"
|
||||
v-model="g.cardShadowChoice"
|
||||
@update:model-value="
|
||||
siteCustomisationChanged({cardShadowChoice: $event})
|
||||
"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('toggle_card_shadow')"></span
|
||||
></q-tooltip>
|
||||
</q-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">
|
||||
@@ -463,6 +539,9 @@
|
||||
flat
|
||||
round
|
||||
v-model="g.darkChoice"
|
||||
@update:model-value="
|
||||
siteCustomisationChanged({darkChoice: $event})
|
||||
"
|
||||
:icon="$q.dark.isActive ? 'brightness_3' : 'wb_sunny'"
|
||||
size="sm"
|
||||
>
|
||||
@@ -481,6 +560,9 @@
|
||||
v-model="g.borderChoice"
|
||||
:options="borderOptions"
|
||||
label="Borders"
|
||||
@update:model-value="
|
||||
siteCustomisationChanged({borderChoice: $event})
|
||||
"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('border_choices')"></span
|
||||
@@ -495,7 +577,7 @@
|
||||
<div class="col-8">
|
||||
<lnbits-notifications-btn
|
||||
v-if="g.user"
|
||||
pubkey="{{ WEBPUSH_PUBKEY }}"
|
||||
pubkey="g.settings.webpushPubkey"
|
||||
></lnbits-notifications-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -508,6 +590,9 @@
|
||||
v-model="g.reactionChoice"
|
||||
:options="reactionOptions"
|
||||
label="Reactions"
|
||||
@update:model-value="
|
||||
siteCustomisationChanged({reactionChoice: $event})
|
||||
"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('payment_reactions')"></span
|
||||
@@ -515,6 +600,15 @@
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
<q-card-section>
|
||||
<q-btn
|
||||
@click="resetThemeDefaults"
|
||||
:label="$t('reset_defaults')"
|
||||
filled
|
||||
color="primary"
|
||||
class="float-right q-mb-md"
|
||||
></q-btn>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="notifications">
|
||||
<q-card-section>
|
||||
@@ -525,7 +619,7 @@
|
||||
></span>
|
||||
<br />
|
||||
<q-badge
|
||||
v-if="!LNBITS_NOSTR_CONFIGURED"
|
||||
v-if="!g.settings.nostrConfigured"
|
||||
v-text="$t('not_connected')"
|
||||
></q-badge>
|
||||
</div>
|
||||
@@ -545,7 +639,7 @@
|
||||
<span v-text="$t('notifications_chat_id')"></span>
|
||||
<br />
|
||||
<q-badge
|
||||
v-if="!LNBITS_TELEGRAM_CONFIGURED"
|
||||
v-if="!g.settings.telegramConfigured"
|
||||
v-text="$t('not_connected')"
|
||||
></q-badge>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
"
|
||||
:clickable="!!reviewsUrl"
|
||||
@click="openReviews(extension)"
|
||||
/>
|
||||
></lnbits-extension-rating>
|
||||
<q-btn-group size="xs" style="margin: 5px 0">
|
||||
<q-btn
|
||||
v-if="extension.hasFreeRelease"
|
||||
@@ -329,6 +329,19 @@
|
||||
<span v-text="$t('enable_extension_details')">
|
||||
</span> </q-tooltip
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-if="
|
||||
extension.isInstalled &&
|
||||
extension.isActive &&
|
||||
!g.user.extensions.includes(extension.id) &&
|
||||
extension.permissions &&
|
||||
extension.permissions.length
|
||||
"
|
||||
flat
|
||||
color="grey-5"
|
||||
@click="openPermissionsForExtension(extension)"
|
||||
label="Permissions"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
@click="showManageExtension(extension)"
|
||||
@@ -921,6 +934,104 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="permissionsDialog.show" position="top">
|
||||
<q-card class="q-pa-md" style="min-width: 360px; max-width: 90vw">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Permissions required</div>
|
||||
<div class="text-caption text-grey">This extension can:</div>
|
||||
<q-list v-if="permissionsDialog.extension">
|
||||
<q-item
|
||||
v-for="perm in permissionsDialog.extension.permissions"
|
||||
:key="perm.id"
|
||||
clickable
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="perm.label"></q-item-label>
|
||||
<q-item-label caption v-text="perm.description"></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-checkbox v-model="permissionsDialog.checked" :val="perm.id" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div
|
||||
v-if="
|
||||
permissionsDialog.tagOptions && permissionsDialog.tagOptions.length
|
||||
"
|
||||
class="q-mt-md"
|
||||
>
|
||||
<div class="text-caption text-grey">
|
||||
Allow this extension to listen for payment tags:
|
||||
</div>
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="tag in permissionsDialog.tagOptions"
|
||||
:key="tag"
|
||||
clickable
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="tag"></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-checkbox v-model="permissionsDialog.tags" :val="tag" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
permissionsDialog.extension &&
|
||||
permissionsDialog.extension.kvSchemaKeys &&
|
||||
permissionsDialog.extension.kvSchemaKeys.length
|
||||
"
|
||||
class="q-mt-md"
|
||||
>
|
||||
<div class="text-caption text-grey">
|
||||
Data this extension stores in its db:
|
||||
</div>
|
||||
<q-chip
|
||||
v-for="kvKey in permissionsDialog.extension.kvSchemaKeys"
|
||||
:key="kvKey"
|
||||
:label="kvKey"
|
||||
color="grey-3"
|
||||
text-color="black"
|
||||
class="q-mr-xs q-mt-xs"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="permissionsDialog.missing && permissionsDialog.missing.length"
|
||||
class="q-mt-md text-negative"
|
||||
>
|
||||
<div class="text-caption">
|
||||
Missing API endpoints required by this extension:
|
||||
</div>
|
||||
<q-chip
|
||||
v-for="perm in permissionsDialog.missing"
|
||||
:key="perm"
|
||||
:label="perm"
|
||||
color="red-2"
|
||||
text-color="black"
|
||||
class="q-mr-xs q-mt-xs"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
flat
|
||||
color="grey"
|
||||
v-text="$t('cancel')"
|
||||
@click="cancelPermissionsDialog"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
color="primary"
|
||||
:disable="!permissionsAllChecked || permissionsHasMissingEndpoints"
|
||||
label="Save"
|
||||
@click="confirmPermissionsDialog"
|
||||
></q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showExtensionDetailsDialog" position="top">
|
||||
<q-card
|
||||
v-if="selectedExtensionDetails"
|
||||
|
||||
@@ -53,6 +53,19 @@
|
||||
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
|
||||
/> </template
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
v-if="g.settings.hasFirstInstallToken"
|
||||
v-model.trim="loginData.firstInstallToken"
|
||||
:type="loginData.isPwd ? 'password' : 'text'"
|
||||
:label="$t('first_install_token')"
|
||||
><template v-slot:append>
|
||||
<q-icon
|
||||
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="loginData.isPwd = !loginData.isPwd"
|
||||
/> </template
|
||||
></q-input>
|
||||
<q-btn
|
||||
@click="setPassword()"
|
||||
unelevated
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<div class="home row justify-center items-center">
|
||||
<div
|
||||
class="full-width content-center"
|
||||
:style="`max-width: ${hasCustomImage ? '850' : '600'}px; min-height: 55vh;`"
|
||||
:style="`max-width: ${g.settings.customImage ? '850' : '600'}px; min-height: 55vh;`"
|
||||
>
|
||||
<div v-if="showHomepageElements" class="row q-mb-md">
|
||||
<div v-if="g.settings.showHomepageElements" class="row q-mb-md">
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<h5 v-text="siteTitle" class="q-my-none"></h5>
|
||||
<h5 v-text="g.settings.siteTitle" class="q-my-none"></h5>
|
||||
<template v-if="$q.screen.gt.sm">
|
||||
<h6 class="q-my-sm" v-text="siteTagline"></h6>
|
||||
<h6 class="q-my-sm" v-text="g.settings.siteTagline"></h6>
|
||||
<p class="q-my-sm" v-html="formatDescription"></p>
|
||||
</template>
|
||||
</div>
|
||||
@@ -31,7 +31,10 @@
|
||||
<div class="row">
|
||||
<div
|
||||
class="col-12"
|
||||
:class="{'col-sm-7': hasCustomImage, 'col-lg-6': hasCustomImage}"
|
||||
:class="{
|
||||
'col-sm-7': g.settings.customImage,
|
||||
'col-lg-6': g.settings.customImage
|
||||
}"
|
||||
>
|
||||
<div v-if="showClaimLnurl">
|
||||
<q-card-section>
|
||||
@@ -54,12 +57,13 @@
|
||||
<div v-else>
|
||||
<username-password
|
||||
v-if="authMethod != 'user-id-only'"
|
||||
:allowed_new_users="allowRegister"
|
||||
:auth-methods="LNBITS_AUTH_METHODS"
|
||||
:allowed_new_users="g.settings.allowRegister"
|
||||
:auth-methods="g.settings.authMethods"
|
||||
:auth-action="authAction"
|
||||
v-model:user-name="username"
|
||||
v-model:password_1="password"
|
||||
v-model:password_2="passwordRepeat"
|
||||
v-model:invitation-code="invitationCode"
|
||||
v-model:reset-key="reset_key"
|
||||
@login="login"
|
||||
@register="register"
|
||||
@@ -70,7 +74,7 @@
|
||||
v-if="authAction !== 'reset'"
|
||||
>
|
||||
<p
|
||||
v-if="authAction === 'login' && allowRegister"
|
||||
v-if="authAction === 'login' && g.settings.allowRegister"
|
||||
class="q-mb-none"
|
||||
>
|
||||
Not registered?
|
||||
@@ -82,7 +86,9 @@
|
||||
>
|
||||
</p>
|
||||
<p
|
||||
v-else-if="authAction === 'login' && !allowRegister"
|
||||
v-else-if="
|
||||
authAction === 'login' && !g.settings.allowRegister
|
||||
"
|
||||
class="q-mb-none"
|
||||
>
|
||||
<span v-text="$t('new_user_not_allowed')"></span>
|
||||
@@ -101,7 +107,7 @@
|
||||
</username-password>
|
||||
<user-id-only
|
||||
v-if="authMethod == 'user-id-only'"
|
||||
:allowed_new_users="allowRegister"
|
||||
:allowed_new_users="g.settings.allowRegister"
|
||||
v-model:usr="usr"
|
||||
v-model:wallet="walletName"
|
||||
:auth-action="authAction"
|
||||
@@ -114,16 +120,20 @@
|
||||
</user-id-only>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasCustomImage" class="col-sm-5 col-lg-6 gt-xs">
|
||||
<div v-if="g.settings.customImage" class="col-sm-5 col-lg-6 gt-xs">
|
||||
<div class="full-height flex flex-center q-pa-lg">
|
||||
<q-img :src="hasCustomImage" :ratio="1" width="250px"></q-img>
|
||||
<q-img
|
||||
:src="g.settings.customImage"
|
||||
:ratio="1"
|
||||
width="250px"
|
||||
></q-img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div v-if="showHomepageElements">
|
||||
<div v-if="g.settings.showHomepageElements">
|
||||
<div class="flex flex-center q-gutter-md q-py-md">
|
||||
<q-btn
|
||||
outline
|
||||
@@ -148,10 +158,14 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="adsEnabled"
|
||||
v-if="g.settings.showAdSpace"
|
||||
class="justify-center col-10 q-my-lg row q-col-gutter-sm"
|
||||
>
|
||||
<a :href="ad[0]" v-for="ad in g.ads" class="lnbits-ad col-12 col-md-4">
|
||||
<a
|
||||
:href="ad[0]"
|
||||
v-for="ad in g.settings.adSpace"
|
||||
class="lnbits-ad col-12 col-md-4"
|
||||
>
|
||||
<q-img v-if="$q.dark.isActive" :src="ad[1]"></q-img>
|
||||
<q-img v-else :src="ad[2]"></q-img>
|
||||
</a>
|
||||
|
||||
@@ -218,6 +218,26 @@
|
||||
:label="$t('update_account')"
|
||||
class="q-ml-md"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
v-if="activeUser.data.id"
|
||||
outline
|
||||
color="primary"
|
||||
class="q-ml-md"
|
||||
>
|
||||
<q-toggle
|
||||
size="xs"
|
||||
color="secondary"
|
||||
v-model="activeUser.data.admin"
|
||||
@update:model-value="toggleAdmin(activeUser.data.id)"
|
||||
:label="
|
||||
activeUser.data.admin
|
||||
? $t('revoke_admin')
|
||||
: $t('make_user_admin')
|
||||
"
|
||||
>
|
||||
</q-toggle>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
@click="createUser()"
|
||||
@@ -590,9 +610,9 @@
|
||||
<q-btn
|
||||
@click="showAccountPage(props.row.id)"
|
||||
round
|
||||
icon="edit"
|
||||
:icon="props.row.is_admin ? 'admin_panel_settings' : 'edit'"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
:color="props.row.is_admin ? 'primary' : 'secondary'"
|
||||
class="q-ml-xs"
|
||||
>
|
||||
<q-tooltip>
|
||||
@@ -605,10 +625,18 @@
|
||||
size="xs"
|
||||
v-if="!props.row.is_super_user"
|
||||
color="secondary"
|
||||
v-model="props.row.is_admin"
|
||||
@update:model-value="toggleAdmin(props.row.id)"
|
||||
v-model="props.row.activated"
|
||||
@update:model-value="toggleUserActivated(props.row.id)"
|
||||
>
|
||||
<q-tooltip>Toggle Admin</q-tooltip>
|
||||
<q-tooltip
|
||||
><span
|
||||
v-text="
|
||||
props.row.activated
|
||||
? $t('deactivate')
|
||||
: $t('activate')
|
||||
"
|
||||
></span
|
||||
></q-tooltip>
|
||||
</q-toggle>
|
||||
<q-btn
|
||||
round
|
||||
|
||||
@@ -171,6 +171,19 @@
|
||||
><span v-text="$t('camera_tooltip')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
<div
|
||||
v-if="g.settings.walletFeaturedButtonUrl"
|
||||
class="float-right q-mt-sm q-ml-sm"
|
||||
>
|
||||
<q-btn
|
||||
color="primary"
|
||||
:label="g.settings.walletFeaturedButtonLabel"
|
||||
:icon="g.settings.walletFeaturedButtonIcon || undefined"
|
||||
size="sm"
|
||||
:to="g.settings.walletFeaturedButtonUrl"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
<lnbits-update-balance
|
||||
v-if="$q.screen.gt.md"
|
||||
:wallet_id="this.g.wallet.id"
|
||||
@@ -199,10 +212,10 @@
|
||||
@send-lnurl="handleSendLnurl"
|
||||
:chart-config="chartConfig"
|
||||
></lnbits-wallet-extra>
|
||||
<q-card class="lnbits-wallet-ads" v-if="AD_SPACE_ENABLED">
|
||||
<q-card class="lnbits-wallet-ads" v-if="g.settings.showAdSpace">
|
||||
<q-card-section class="text-subtitle1">
|
||||
<span v-text="AD_SPACE_TITLE"></span>
|
||||
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.ads">
|
||||
<span v-text="g.settings.adSpaceTitle"></span>
|
||||
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.settings.adSpace">
|
||||
<q-img class="q-mb-xs" v-if="$q.dark.isActive" :src="ad[1]"></q-img>
|
||||
<q-img class="q-mb-xs" v-else :src="ad[2]"></q-img>
|
||||
</a>
|
||||
@@ -276,7 +289,7 @@
|
||||
></q-input>
|
||||
</div>
|
||||
<q-input
|
||||
v-if="has_holdinvoice"
|
||||
v-if="g.settings.hasHoldinvoice"
|
||||
filled
|
||||
dense
|
||||
v-model="receive.data.payment_hash"
|
||||
|
||||
@@ -45,10 +45,8 @@
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
|
||||
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||
window[key] = WINDOW_SETTINGS[key]
|
||||
})
|
||||
const SETTINGS = {{ SETTINGS | tojson | safe }}
|
||||
const CURRENCIES = {{ CURRENCIES | tojson | safe }}
|
||||
</script>
|
||||
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
|
||||
for url in INCLUDED_JS %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user