Compare commits

..
Author SHA1 Message Date
dni ⚡ c820456500 feat: add openapi-generators for sdk's 2025-04-10 12:29:41 +02:00
175 changed files with 7004 additions and 10552 deletions
+5 -13
View File
@@ -40,7 +40,7 @@ PORT=5000
######################################
# 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"
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet"
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
@@ -101,16 +101,10 @@ ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN
# BoltzWallet
BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
# HEXSTRING instead of path also possible
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
# HEXSTRING instead of path also possible
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroon" # or HEXSTRING
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert" # or HEXSTRING
BOLTZ_CLIENT_WALLET="lnbits"
# StrikeWallet
STRIKE_API_ENDPOINT=https://api.strike.me/v1
STRIKE_API_KEY=YOUR_STRIKE_API_KEY
# ZBDWallet
ZBD_API_ENDPOINT=https://api.zebedee.io/v0/
ZBD_API_KEY=ZBD_ACCESS_TOKEN
@@ -169,16 +163,14 @@ GITHUB_CLIENT_SECRET=""
KEYCLOAK_CLIENT_ID=""
KEYCLOAK_CLIENT_SECRET=""
KEYCLOAK_DISCOVERY_URL=""
KEYCLOAK_CLIENT_CUSTOM_ORG=""
KEYCLOAK_CLIENT_CUSTOM_ICON=""
######################################
# uvicorn variable, allow https behind a proxy
# uvicorn variable, uncomment to allow https behind a proxy
# IMPORTANT: this also needs the webserver to be configured to forward the headers
# http://docs.lnbits.org/guide/installation.html#running-behind-an-apache2-reverse-proxy-over-https
FORWARDED_ALLOW_IPS="*"
# FORWARDED_ALLOW_IPS="*"
# Server security, rate limiting ips, blocked ips, allowed ips
LNBITS_RATE_LIMIT_NO="200"
+6 -1
View File
@@ -5,6 +5,9 @@ inputs:
description: "Python Version"
required: true
default: "3.10"
poetry-version:
description: "Poetry Version"
default: "1.8.5"
node-version:
description: "Node Version"
default: "20.x"
@@ -24,8 +27,10 @@ runs:
# cache poetry install via pip
cache: "pip"
- name: Set up Poetry
- name: Set up Poetry ${{ inputs.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ inputs.poetry-version }}
- name: Setup a local virtual environment (if no poetry.toml file)
shell: bash
+1
View File
@@ -36,6 +36,7 @@ LNBITS_DATA_FOLDER="${LNBITS_DATA_FOLDER:-$LAUNCH_DIR/lnbits/database}"
LNBITS_EXTENSIONS_PATH="${LNBITS_EXTENSIONS_PATH:-$LAUNCH_DIR/lnbits/extensions}"
export LNBITS_DATA_FOLDER
export LNBITS_EXTENSIONS_PATH
export LNBITS_ADMIN_UI=true
# Define the LNbits URL
URL="http://0.0.0.0:5000"
+2
View File
@@ -88,6 +88,8 @@ jobs:
strategy:
matrix:
python-version: ["3.10"]
poetry-version: ["1.5.1"]
uses: ./.github/workflows/jmeter.yml
with:
python-version: ${{ matrix.python-version }}
poetry-version: ${{ matrix.poetry-version }}
-11
View File
@@ -51,14 +51,3 @@ jobs:
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Build and push boltz
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.boltz
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
+5
View File
@@ -8,6 +8,11 @@ on:
required: true
default: "3.10"
type: string
poetry-version:
description: "Poetry Version"
required: true
default: "1.5.1"
type: string
jobs:
jmeter:
-1
View File
@@ -25,7 +25,6 @@ on:
jobs:
nix:
if: false # temporarly disable nix support until the `poetry2nix` issue is resolved
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
+2 -1
View File
@@ -7,7 +7,6 @@ __pycache__
.mypy_cache
.vscode
*-lock.json
.python-version
*.egg
*.egg-info
@@ -43,6 +42,8 @@ lnbits/static/bundle.min.css.old
lnbits/static/bundle-components.min.js.old
lnbits/upgrades
docker
generated
openapi.json*
# Nix
*result*
+5 -8
View File
@@ -4,14 +4,13 @@ RUN apt-get clean
RUN apt-get update
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
RUN curl -sSL https://install.python-poetry.org | python3 -
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
ENV PATH="/root/.local/bin:$PATH"
WORKDIR /app
# Only copy the files required to install the dependencies
COPY pyproject.toml poetry.lock ./
RUN touch README.md
RUN mkdir data
@@ -20,8 +19,7 @@ ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
ARG POETRY_INSTALL_ARGS="--only main"
RUN poetry install --no-root ${POETRY_INSTALL_ARGS}
RUN poetry install --only main
FROM python:3.12-slim-bookworm
@@ -34,7 +32,7 @@ RUN apt-get update && apt-get -y upgrade && \
apt-get -y install postgresql-client-14 postgresql-client-common && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
RUN curl -sSL https://install.python-poetry.org | python3 -
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
ENV PATH="/root/.local/bin:$PATH"
ENV POETRY_NO_INTERACTION=1 \
@@ -48,12 +46,11 @@ WORKDIR /app
COPY . .
COPY --from=builder /app/.venv .venv
ARG POETRY_INSTALL_ARGS="--only main"
RUN poetry install ${POETRY_INSTALL_ARGS}
RUN poetry install --only main
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000
CMD ["sh", "-c", "poetry run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
CMD ["sh", "-c", "poetry run lnbits --port $LNBITS_PORT --host $LNBITS_HOST"]
-30
View File
@@ -1,30 +0,0 @@
FROM boltz/boltz-client:latest AS boltz
FROM lnbits/lnbits:latest
COPY --from=boltz /bin/boltzd /bin/boltzcli /usr/local/bin/
RUN ls -l /usr/local/bin/boltzd
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y netcat-openbsd
# Reinstall dependencies just in case (needed for CMD usage)
ARG POETRY_INSTALL_ARGS="--only main"
RUN poetry install ${POETRY_INSTALL_ARGS}
# LNbits + boltzd configuration
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
ENV FUNDING_SOURCE_MAX_RETRIES=10
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"
ENV BOLTZ_CLIENT_WALLET="lnbits"
EXPOSE 5000
# Entrypoint to start boltzd and LNbits
COPY dockerboltz.sh /dockerboltz.sh
RUN chmod +x /dockerboltz.sh
CMD ["/dockerboltz.sh"]
+7 -3
View File
@@ -99,9 +99,13 @@ openapi:
curl -s http://0.0.0.0:5003/openapi.json | poetry run openapi-spec-validator --errors=all -
# kill -9 %1
bak:
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres
#
generate:
rm -rf generated
mkdir generated
rm -f openapi.json
wget localhost:5000/openapi.json
npm run generate
rm openapi.json
sass:
npm run sass
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
set -e
boltzd --standalone --referralId lnbits &
# Capture boltzd PID to monitor if needed
BOLTZ_PID=$!
# Wait for boltzd to start
for i in {1..10}; do
if nc -z localhost 9002; then
echo "boltzd is up!"
break
fi
echo "Waiting for boltzd to start..."
sleep 1
done
# Optional: check if still not up
if ! nc -z localhost 9002; then
echo "boltzd did not start successfully."
exit 1
fi
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
exec poetry run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
-30
View File
@@ -1,30 +0,0 @@
# LNbits Funding Sources Comparison Table
LNbits can use a number of different Lightning Network funding source.
There may be trade-offs between the funding sources used, for example funding LNbits using Strike requires the user to KYC themselves and has some
privacy compromises versus running your own LND node. However the technical barrier to entry of using Strike is lower than using LND.
The table below offers a comparison of the different Lightning Network funding sources that can be used with LNbits.
## LNbits Lightning Network Funding Sources Comparison Table
| **Funding Source** | **Custodial Type** | **KYC Required** | **Technical Knowledge Needed** | **Node Hosting Required** | **Privacy Level** | **Liquidity Management** | **Ease of Setup** | **Maintenance Effort** | **Cost Implications** | **Scalability** | **Notes** |
| -------------------------- | ------------------ | ------------------- | ------------------------------ | ------------------------- | ----------------- | ------------------------ | ----------------- | ---------------------- | -------------------------------------------- | --------------- | ---------------------------------------------------------------- |
| LND (gRPC) | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | gRPC interface for LND; suitable for advanced integrations. |
| CoreLightning (CLN) | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Requires setting up and managing your own CLN node. |
| Phoenixd | Self-custodial | ❌ | Medium | ❌ | Medium | Automatic | Moderate | Low | Minimal fees | Medium | Mobile wallet backend; suitable for mobile integrations. |
| Nostr Wallet Connect (NWC) | Custodial | Depends on provider | Low | ❌ | Variable | Provider-managed | Easy | Low | May incur fees | Medium | Connects via Nostr protocol; depends on provider's policies. |
| Boltz | Self-custodial | ❌ | Medium | ❌ | Medium | Provider-managed | Moderate | Moderate | Minimal fees | Medium | Uses submarine swaps; connects to Boltz client. |
| LND (REST) | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | REST interface for LND; suitable for web integrations. |
| CoreLightning REST | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | REST interface for CLN; suitable for web integrations. |
| LNbits (another instance) | Custodial | Depends on host | Low | ❌ | Variable | Provider-managed | Easy | Low | May incur hosting fees | Medium | Connects to another LNbits instance; depends on host's policies. |
| Alby | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Browser extension wallet; suitable for web users. |
| Breez SDK | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Moderate | Low | Minimal fees | Medium | SDK for integrating Breez wallet functionalities. |
| OpenNode | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for merchants. |
| 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. |
| 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. |
+7 -43
View File
@@ -18,7 +18,7 @@ Go to [releases](https://github.com/lnbits/lnbits/releases) and pull latest AppI
sudo apt-get install libfuse2
wget $(curl -s https://api.github.com/repos/lnbits/lnbits/releases/latest | jq -r '.assets[] | select(.name | endswith(".AppImage")) | .browser_download_url') -O LNbits-latest.AppImage
chmod +x LNbits-latest.AppImage
LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most system settings are now in the admin UI, but pass additional .env variables here
./LNbits-latest.AppImage --host 0.0.0.0 --port 5000
```
LNbits will create a folder for db and extension files in the folder the AppImage runs from.
@@ -78,24 +78,11 @@ poetry run lnbits-cli --help
```sh
cd lnbits
# Stop LNbits with `ctrl + x` or with service manager
# sudo systemctl stop lnbits
# Update LNbits
git pull --rebase
# Check your poetry version with
poetry env list
# If version is less 3.12, update it by running
poetry env use python3.12
poetry env remove python3.9
poetry env list
# Run install and start LNbits with
# Stop LNbits with `ctrl + x`
git pull
# Keep your poetry install up to date, this can be done with `poetry self update`
poetry install --only main
poetry run lnbits
# use LNbits admin UI Extensions page function "Update All" do get extensions onto proper level
# Start LNbits with `poetry run lnbits`
```
## Option 2: Install script (on Debian/Ubuntu)
@@ -150,7 +137,7 @@ SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/ln
## Option 4: Docker
Use latest version from Docker Hub.
use latest version from docker hub
```sh
docker pull lnbits/lnbits
@@ -159,15 +146,7 @@ mkdir data
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
```
The LNbits Docker image comes with no extensions installed. User-installed extensions will be stored by default in a container directory.
It is recommended to point the `LNBITS_EXTENSIONS_PATH` environment variable to a directory that is mapped to a Docker volume. This way, the extensions will not be reinstalled when the container is destroyed.
Example:
```sh
docker run ... -e "LNBITS_EXTENSIONS_PATH='/app/data/extensions'" --volume ${PWD}/data/:/app/data ...
```
Build the image yourself.
build the image yourself
```sh
git clone https://github.com/lnbits/lnbits.git
@@ -178,12 +157,6 @@ mkdir data
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
```
You can optionally override the arguments that are passed to `poetry install` during the build process by setting the Docker build argument named `POETRY_INSTALL_ARGS`. For example, to enable the Breez funding source, build the Docker image with the command:
```sh
docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .
```
## Option 5: Fly.io
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
@@ -591,12 +564,3 @@ docker run --detach --publish 5000:5000 --name lnbits -e "LNBITS_BACKEND_WALLET_
```
Finally you can access your lnbits on your machine at port 5000.
### FreeBSD notes
Currently there is an issue with secp256k1 0.14.0 on FreeBSD. Thanks to @GitKalle
1. Install package `py311-secp256k1` with `pkg install py311-secp256k1`
2. Change version in `pyproject.toml` from 0.14.0 to 0.13.2
3. Rewrite `poetry.lock` file with command `poetry lock`
4. Follow install instruction with Poetry
+1 -3
View File
@@ -10,8 +10,6 @@ LNbits can run on top of many Lightning Network funding sources with more being
A backend wallet can be configured using the following LNbits environment variables:
You can [compare the LNbits compatible Lightning Network funding sources here](wallets.md).
### CoreLightning
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
@@ -53,7 +51,7 @@ You can also use an AES-encrypted macaroon (more info) instead by using
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
To encrypt your macaroon, run `poetry run lnbits-cli encrypt macaroon`.
To encrypt your macaroon, run `poetry run python lnbits/wallets/macaroon/macaroon.py`.
### LNbits
+6 -6
View File
@@ -6,15 +6,15 @@ if [ ! -d lnbits/data ]; then
# Update package list and install prerequisites non-interactively
sudo apt update -y
sudo apt install -y software-properties-common
# Add the deadsnakes PPA repository non-interactively
sudo add-apt-repository -y ppa:deadsnakes/ppa
# Install Python 3.10 and distutils non-interactively
sudo apt install -y python3.10 python3.10-distutils
# Install Python 3.9 and distutils non-interactively
sudo apt install -y python3.9 python3.9-distutils
# Install Poetry
curl -sSL https://install.python-poetry.org | python3.10 -
curl -sSL https://install.python-poetry.org | python3.9 -
# Add Poetry to PATH for the current session
export PATH="/home/$USER/.local/bin:$PATH"
@@ -51,4 +51,4 @@ export LNBITS_ADMIN_UI=true
export HOST=0.0.0.0
# Run LNbits
poetry run lnbits
poetry run lnbits
+8 -5
View File
@@ -9,13 +9,16 @@ from .decorators import (
from .exceptions import InvoiceError, PaymentError
__all__ = [
"InvoiceError",
"PaymentError",
# decorators
"require_admin_key",
"require_invoice_key",
"check_admin",
"check_super_user",
"check_user_exists",
"create_invoice",
# services
"pay_invoice",
"require_admin_key",
"require_invoice_key",
"create_invoice",
# exceptions
"PaymentError",
"InvoiceError",
]
+3
View File
@@ -65,6 +65,7 @@ from .middleware import (
add_ip_block_middleware,
add_ratelimit_middleware,
)
from .requestvars import g
from .tasks import (
check_pending_payments,
internal_invoice_listener,
@@ -170,6 +171,8 @@ def create_app() -> FastAPI:
name="library",
)
g().base_url = f"http://{settings.host}:{settings.port}"
app.add_middleware(
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
)
-73
View File
@@ -3,7 +3,6 @@ import importlib
import sys
import time
from functools import wraps
from getpass import getpass
from pathlib import Path
from typing import Optional
from uuid import uuid4
@@ -41,9 +40,7 @@ from lnbits.core.views.extension_api import (
api_uninstall_extension,
)
from lnbits.settings import settings
from lnbits.utils.crypto import AESCipher
from lnbits.wallets.base import Wallet
from lnbits.wallets.macaroon import load_macaroon
def coro(f):
@@ -82,20 +79,6 @@ def extensions():
"""
@lnbits_cli.group()
def encrypt():
"""
Encryption commands
"""
@lnbits_cli.group()
def decrypt():
"""
Decryption commands
"""
def get_super_user() -> Optional[str]:
"""Get the superuser"""
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
@@ -496,62 +479,6 @@ async def extensions_uninstall(
return False, str(ex)
@encrypt.command("macaroon")
def encrypt_macaroon():
"""Encrypts a macaroon (LND wallets)"""
_macaroon = getpass("Enter macaroon: ")
try:
macaroon = load_macaroon(_macaroon)
except Exception as ex:
click.echo(f"Error loading macaroon: {ex}")
return
key = getpass("Enter encryption key: ")
aes = AESCipher(key.encode())
try:
encrypted_macaroon = aes.encrypt(bytes.fromhex(macaroon))
except Exception as ex:
click.echo(f"Error encrypting macaroon: {ex}")
return
click.echo("Encrypted macaroon: ")
click.echo(encrypted_macaroon)
@encrypt.command("aes")
@click.option("-p", "--payload", required=True, help="Payload to encrypt.")
@click.option(
"-u", "--urlsafe", is_flag=True, required=False, help="Urlsafe b64encode."
)
def encrypt_aes(payload: str, urlsafe: bool = False):
"""AES encrypts a payload"""
key = getpass("Enter encryption key: ")
aes = AESCipher(key.encode())
try:
encrypted = aes.encrypt(payload.encode(), urlsafe=urlsafe)
except Exception as ex:
click.echo(f"Error encrypting payload: {ex}")
return
click.echo("Encrypted payload: ")
click.echo(encrypted)
@decrypt.command("aes")
@click.option("-p", "--payload", required=True, help="Payload to decrypt.")
@click.option(
"-u", "--urlsafe", is_flag=True, required=False, help="Urlsafe b64decode."
)
def decrypt_aes(payload: str, urlsafe: bool = False):
"""AES decrypts a payload"""
key = getpass("Enter encryption key: ")
aes = AESCipher(key.encode())
try:
decrypted = aes.decrypt(payload, urlsafe=urlsafe)
except Exception as ex:
click.echo(f"Error decrypting payload: {ex}")
return
click.echo("Decrypted payload: ")
click.echo(decrypted)
def main():
"""main function"""
lnbits_cli()
-4
View File
@@ -5,9 +5,7 @@ from .views.admin_api import admin_router
from .views.api import api_router
from .views.audit_api import audit_router
from .views.auth_api import auth_router
from .views.callback_api import callback_router
from .views.extension_api import extension_router
from .views.fiat_api import fiat_router
# this compat is needed for usermanager extension
from .views.generic import generic_router
@@ -36,12 +34,10 @@ def init_core_routers(app: FastAPI):
app.include_router(wallet_router)
app.include_router(api_router)
app.include_router(websocket_router)
app.include_router(callback_router)
app.include_router(tinyurl_router)
app.include_router(webpush_router)
app.include_router(users_router)
app.include_router(audit_router)
app.include_router(fiat_router)
__all__ = ["core_app", "core_app_extra", "db"]
+63 -56
View File
@@ -43,7 +43,6 @@ from .settings import (
delete_admin_settings,
get_admin_settings,
get_super_settings,
reset_core_settings,
update_admin_settings,
update_super_user,
)
@@ -85,78 +84,86 @@ from .webpush import (
)
__all__ = [
"DateTrunc",
"check_internal",
"create_account",
"create_admin_settings",
# audit
"create_audit_entry",
"create_installed_extension",
"create_payment",
"create_tinyurl",
"create_user_extension",
"create_wallet",
"create_webpush_subscription",
"delete_account",
"delete_accounts_no_wallets",
"delete_admin_settings",
"delete_dbversion",
"delete_expired_invoices",
"delete_installed_extension",
"delete_tinyurl",
"delete_unused_wallets",
"delete_wallet",
"delete_wallet_by_id",
"delete_wallet_payment",
"delete_webpush_subscription",
"delete_webpush_subscriptions",
"drop_extension_db",
"force_delete_wallet",
"get_account",
"get_account_by_email",
"get_account_by_pubkey",
"get_account_by_username",
"get_account_by_username_or_email",
"get_accounts",
"get_admin_settings",
# db_versions
"get_db_version",
"get_db_versions",
"update_migration_version",
"delete_dbversion",
# extensions
"create_installed_extension",
"create_user_extension",
"delete_installed_extension",
"drop_extension_db",
"get_installed_extension",
"get_installed_extensions",
"get_user_active_extensions_ids",
"get_user_extension",
"update_installed_extension",
"update_installed_extension_state",
"update_user_extension",
"get_user_extensions",
# payments
"DateTrunc",
"check_internal",
"create_payment",
"delete_expired_invoices",
"delete_wallet_payment",
"get_latest_payments_by_extension",
"get_payment",
"get_payments",
"get_payments_history",
"get_payments_paginated",
"get_standalone_payment",
"get_super_settings",
"get_tinyurl",
"get_tinyurl_by_url",
"get_total_balance",
"get_user",
"get_user_access_control_lists",
"get_user_active_extensions_ids",
"get_user_extension",
"get_user_extensions",
"get_user_from_account",
"get_wallet",
"get_wallet_for_key",
"get_wallet_payment",
"get_wallets",
"get_webpush_subscription",
"get_webpush_subscriptions_for_user",
"is_internal_status_success",
"mark_webhook_sent",
"remove_deleted_wallets",
"reset_core_settings",
"update_account",
"update_admin_settings",
"update_installed_extension",
"update_installed_extension_state",
"update_migration_version",
"update_payment",
"update_payment_checking_id",
"update_payment_extra",
# settings
"create_admin_settings",
"delete_admin_settings",
"get_admin_settings",
"get_super_settings",
"update_admin_settings",
"update_super_user",
"update_user_extension",
# tinyurl
"create_tinyurl",
"delete_tinyurl",
"get_tinyurl",
"get_tinyurl_by_url",
# users
"create_account",
"delete_account",
"delete_accounts_no_wallets",
"get_account",
"get_account_by_email",
"get_account_by_pubkey",
"get_account_by_username",
"get_account_by_username_or_email",
"get_accounts",
"get_user",
"get_user_from_account",
"get_user_access_control_lists",
"update_account",
# wallets
"create_wallet",
"delete_unused_wallets",
"delete_wallet",
"delete_wallet_by_id",
"force_delete_wallet",
"get_total_balance",
"get_wallet",
"get_wallet_for_key",
"get_wallets",
"remove_deleted_wallets",
"update_wallet",
# webpush
"create_webpush_subscription",
"delete_webpush_subscription",
"delete_webpush_subscriptions",
"get_webpush_subscription",
"get_webpush_subscriptions_for_user",
]
+11 -46
View File
@@ -1,7 +1,7 @@
from time import time
from typing import Any, Optional
from typing import Any, Optional, Tuple
from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids
from lnbits.core.crud.wallets import get_total_balance, get_wallet
from lnbits.core.db import db
from lnbits.core.models import PaymentState
from lnbits.db import Connection, DateTrunc, Filters, Page
@@ -95,7 +95,6 @@ async def get_latest_payments_by_extension(
async def get_payments_paginated(
*,
wallet_id: Optional[str] = None,
user_id: Optional[str] = None,
complete: bool = False,
pending: bool = False,
failed: bool = False,
@@ -122,9 +121,6 @@ async def get_payments_paginated(
if wallet_id:
values["wallet_id"] = wallet_id
clause.append("wallet_id = :wallet_id")
elif user_id:
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
clause.append(only_user_wallets)
if complete and pending:
clause.append(
@@ -362,19 +358,12 @@ async def get_payments_history(
async def get_payment_count_stats(
field: PaymentCountField,
filters: Optional[Filters[PaymentFilters]] = None,
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> list[PaymentCountStat]:
if not filters:
filters = Filters()
extra_stmts = []
if user_id:
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
extra_stmts.append(only_user_wallets)
clause = filters.where(extra_stmts)
clause = filters.where()
data = await (conn or db).fetchall(
query=f"""
SELECT {field} as field, count(*) as total
@@ -392,26 +381,18 @@ async def get_payment_count_stats(
async def get_daily_stats(
filters: Optional[Filters[PaymentFilters]] = None,
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
) -> Tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
if not filters:
filters = Filters()
in_where_stmts = ["(apipayments.status = 'success' AND apipayments.amount > 0)"]
out_where_stmts = [
"(apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)"
]
if user_id:
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
in_where_stmts.append(only_user_wallets)
out_where_stmts.append(only_user_wallets)
in_clause = filters.where(in_where_stmts)
out_clause = filters.where(out_where_stmts)
in_clause = filters.where(
["(apipayments.status = 'success' AND apipayments.amount > 0)"]
)
out_clause = filters.where(
["(apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)"]
)
date_trunc = db.datetime_grouping("day")
query = """
SELECT {date_trunc} date,
@@ -442,7 +423,6 @@ async def get_daily_stats(
async def get_wallets_stats(
filters: Optional[Filters[PaymentFilters]] = None,
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> list[PaymentWalletStats]:
@@ -461,10 +441,6 @@ async def get_wallets_stats(
)
""",
]
if user_id:
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
where_stmts.append(only_user_wallets)
clauses = filters.where(where_stmts)
data = await (conn or db).fetchall(
@@ -532,7 +508,7 @@ async def is_internal_status_success(
return payment.status == PaymentState.SUCCESS.value
async def mark_webhook_sent(payment_hash: str, status: str) -> None:
async def mark_webhook_sent(payment_hash: str, status: int) -> None:
await db.execute(
"""
UPDATE apipayments SET webhook_status = :status
@@ -540,14 +516,3 @@ async def mark_webhook_sent(payment_hash: str, status: str) -> None:
""",
{"status": status, "hash": payment_hash},
)
async def _only_user_wallets_statement(
user_id: str, conn: Optional[Connection] = None
) -> str:
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
"no-wallets-for-user"
]
# wallet ids are safe to use in sql queries
wallet_ids_str = [f"'{w}'" for w in wallet_ids]
return f""" wallet_id IN ({", ".join(wallet_ids_str)}) """
+1 -17
View File
@@ -61,23 +61,7 @@ async def update_super_user(super_user: str) -> SuperSettings:
async def delete_admin_settings(tag: Optional[str] = "core") -> None:
await db.execute(
"DELETE FROM system_settings WHERE tag = :tag",
{"tag": tag},
)
async def reset_core_settings() -> None:
await db.execute(
"""
DELETE FROM system_settings WHERE tag = 'core'
AND id NOT IN (
'super_user',
'lnbits_webpush_pubkey',
'lnbits_webpush_privkey'
)
""",
)
await db.execute("DELETE FROM settings WHERE tag = :tag", {"tag": tag})
async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSettings:
+8 -14
View File
@@ -68,7 +68,6 @@ async def get_accounts(
accounts.username,
accounts.email,
accounts.pubkey,
accounts.external_id,
SUM(COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)) as balance_msat,
@@ -129,8 +128,8 @@ async def get_account_by_username(
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 username = :username",
{"username": username},
Account,
)
@@ -139,8 +138,8 @@ async def get_account_by_pubkey(
pubkey: str, conn: Optional[Connection] = None
) -> Optional[Account]:
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
{"pubkey": pubkey.lower()},
"SELECT * FROM accounts WHERE pubkey = :pubkey",
{"pubkey": pubkey},
Account,
)
@@ -151,8 +150,8 @@ async def get_account_by_email(
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 email = :email",
{"email": email},
Account,
)
@@ -161,11 +160,8 @@ async def get_account_by_username_or_email(
username_or_email: str, conn: Optional[Connection] = None
) -> Optional[Account]:
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE LOWER(email) = :value or LOWER(username) = :value
""",
{"value": username_or_email.lower()},
"SELECT * FROM accounts WHERE email = :value or username = :value",
{"value": username_or_email},
Account,
)
@@ -187,7 +183,6 @@ async def get_user_from_account(
email=account.email,
username=account.username,
pubkey=account.pubkey,
external_id=account.external_id,
extra=account.extra,
created_at=account.created_at,
updated_at=account.updated_at,
@@ -195,7 +190,6 @@ async def get_user_from_account(
wallets=wallets,
admin=account.is_admin,
super_user=account.is_super_user,
fiat_providers=account.fiat_providers,
has_password=account.password_hash is not None,
)
+1 -39
View File
@@ -4,8 +4,7 @@ from typing import Optional
from uuid import uuid4
from lnbits.core.db import db
from lnbits.core.models.wallets import WalletsFilters
from lnbits.db import Connection, Filters, Page
from lnbits.db import Connection
from lnbits.settings import settings
from ..models import Wallet
@@ -136,43 +135,6 @@ async def get_wallets(
)
async def get_wallets_paginated(
user_id: str,
deleted: Optional[bool] = None,
filters: Optional[Filters[WalletsFilters]] = None,
conn: Optional[Connection] = None,
) -> Page[Wallet]:
if deleted is None:
deleted = False
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
return await (conn or db).fetch_page(
"""
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0) AS balance_msat FROM wallets
""",
where=where,
values={"user": user_id, "deleted": deleted},
filters=filters,
model=Wallet,
)
async def get_wallets_ids(
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
) -> list[str]:
where = "AND deleted = :deleted" if deleted is not None else ""
result: list[dict] = await (conn or db).fetchall(
f"""
SELECT id FROM wallets
WHERE "user" = :user {where}
""",
{"user": user_id, "deleted": deleted},
)
return [row["id"] for row in result]
async def get_wallets_count():
result = await db.execute("SELECT COUNT(*) as count FROM wallets")
row = result.mappings().first()
-12
View File
@@ -711,15 +711,3 @@ async def m031_add_color_and_icon_to_wallets(db: Connection):
Adds icon and color columns to wallets.
"""
await db.execute("ALTER TABLE wallets ADD COLUMN extra TEXT")
async def m032_add_external_id_to_accounts(db: Connection):
"""
Adds external_id column to accounts.
Used for external account linking.
"""
await db.execute("ALTER TABLE accounts ADD COLUMN external_id TEXT")
async def m033_update_payment_table(db: Connection):
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
+30 -22
View File
@@ -48,54 +48,62 @@ from .wallets import BaseWallet, CreateWallet, KeyType, Wallet, WalletTypeInfo
from .webpush import CreateWebPushSubscription, WebPushSubscription
__all__ = [
"AccessTokenPayload",
"Account",
"AccountFilters",
"AccountOverview",
# audit
"AuditEntry",
"AuditFilters",
# lnurl
"CreateLnurl",
"CreateLnurlAuth",
"PayLnurlWData",
# misc
"BalanceDelta",
"BaseWallet",
"Callback",
"ConversionData",
"CoreAppExtra",
"CreateInvoice",
"CreateLnurl",
"CreateLnurlAuth",
"CreatePayment",
"CreateUser",
"CreateWallet",
"CreateWebPushSubscription",
"DbVersion",
"SimpleStatus",
# payments
"CreateInvoice",
"CreatePayment",
"DecodePayment",
"KeyType",
"LoginUsernamePassword",
"LoginUsr",
"PayInvoice",
"PayLnurlWData",
"Payment",
"PaymentCountField",
"PaymentCountStat",
"PaymentDailyStats",
"PaymentsStatusCount",
"PaymentWalletStats",
"PaymentExtra",
"PaymentFilters",
"PaymentHistoryPoint",
"PaymentState",
"PaymentWalletStats",
"PaymentsStatusCount",
"RegisterUser",
"ResetUserPassword",
"SimpleStatus",
# tinyurl
"TinyURL",
# users
"AccessTokenPayload",
"Account",
"AccountFilters",
"AccountOverview",
"UserAcls",
"CreateUser",
"RegisterUser",
"LoginUsernamePassword",
"LoginUsr",
"ResetUserPassword",
"UpdateBalance",
"UpdateSuperuserPassword",
"UpdateUser",
"UpdateUserPassword",
"UpdateUserPubkey",
"User",
"UserAcls",
"UserExtra",
# wallets
"BaseWallet",
"CreateWallet",
"KeyType",
"Wallet",
"WalletTypeInfo",
# webpush
"CreateWebPushSubscription",
"WebPushSubscription",
]
+3 -55
View File
@@ -8,13 +8,6 @@ from fastapi import Query
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 (
@@ -67,13 +60,11 @@ class Payment(BaseModel):
amount: int
fee: int
bolt11: str
# payment_request: str | None
fiat_provider: str | None = None
status: str = PaymentState.PENDING
memo: str | None = None
expiry: datetime | None = None
webhook: str | None = None
webhook_status: str | None = None
webhook_status: int | None = None
preimage: str | None = None
tag: str | None = None
extension: str | None = None
@@ -116,23 +107,14 @@ class Payment(BaseModel):
@property
def is_internal(self) -> bool:
return self.checking_id.startswith("internal_") or self.checking_id.startswith(
"fiat_"
)
return self.checking_id.startswith("internal_")
async def check_status(
self, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
async def check_status(self) -> 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:
@@ -141,39 +123,6 @@ class Payment(BaseModel):
status = await funding_source.get_invoice_status(self.checking_id)
return status
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()
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
class PaymentFilters(FilterModel):
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
@@ -257,7 +206,6 @@ class CreateInvoice(BaseModel):
webhook: str | None = None
bolt11: str | None = None
lnurl_callback: str | None = None
fiat_provider: str | None = None
@validator("unit")
@classmethod
+2 -1
View File
@@ -1,4 +1,5 @@
"""Keycloak SSO Login Helper"""
"""Keycloak SSO Login Helper
"""
from typing import Optional
+3 -29
View File
@@ -9,12 +9,7 @@ from pydantic import BaseModel, Field
from lnbits.core.models.misc import SimpleItem
from lnbits.db import FilterModel
from lnbits.helpers import (
is_valid_email_address,
is_valid_external_id,
is_valid_pubkey,
is_valid_username,
)
from lnbits.helpers import is_valid_email_address, is_valid_pubkey, is_valid_username
from lnbits.settings import settings
from .wallets import Wallet
@@ -32,9 +27,6 @@ class UserExtra(BaseModel):
# - "google | github | ...": the user was created using an SSO provider
provider: str | None = "lnbits" # auth provider
# how many wallets are shown in the user interface
visible_wallet_count: int | None = 10
class EndpointAccess(BaseModel):
path: str
@@ -98,7 +90,6 @@ class UserAcls(BaseModel):
class Account(BaseModel):
id: str
external_id: str | None = None # for external account linking
username: str | None = None
password_hash: str | None = None
pubkey: str | None = None
@@ -110,13 +101,11 @@ class Account(BaseModel):
is_super_user: bool = Field(default=False, no_database=True)
is_admin: bool = Field(default=False, no_database=True)
fiat_providers: list[str] = Field(default=[], no_database=True)
def __init__(self, **data):
super().__init__(**data)
self.is_super_user = settings.is_super_user(self.id)
self.is_admin = settings.is_admin_user(self.id)
self.fiat_providers = settings.get_fiat_providers_for_user(self.id)
def hash_password(self, password: str) -> str:
"""sets and returns the hashed password"""
@@ -138,11 +127,6 @@ class Account(BaseModel):
raise ValueError("Invalid email.")
if self.pubkey and not is_valid_pubkey(self.pubkey):
raise ValueError("Invalid pubkey.")
if self.external_id and not is_valid_external_id(self.external_id):
raise ValueError(
"Invalid external id. Max length is 256 characters. "
"Space and newlines are not allowed."
)
user_uuid4 = UUID(hex=self.id, version=4)
if user_uuid4.hex != self.id:
raise ValueError("User ID is not valid UUID4 hex string.")
@@ -156,14 +140,7 @@ class AccountOverview(Account):
class AccountFilters(FilterModel):
__search_fields__ = [
"user",
"email",
"username",
"pubkey",
"external_id",
"wallet_id",
]
__search_fields__ = ["user", "email", "username", "pubkey", "wallet_id"]
__sort_fields__ = [
"balance_msat",
"email",
@@ -177,7 +154,6 @@ class AccountFilters(FilterModel):
user: str | None = None
username: str | None = None
pubkey: str | None = None
external_id: str | None = None
wallet_id: str | None = None
@@ -188,12 +164,10 @@ class User(BaseModel):
email: str | None = None
username: str | None = None
pubkey: str | None = None
external_id: str | None = None # for external account linking
extensions: list[str] = []
wallets: list[Wallet] = []
admin: bool = False
super_user: bool = False
fiat_providers: list[str] = []
has_password: bool = False
extra: UserExtra = UserExtra()
@@ -230,13 +204,13 @@ class CreateUser(BaseModel):
password: str | None = Query(default=None, min_length=8, max_length=50)
password_repeat: str | None = Query(default=None, min_length=8, max_length=50)
pubkey: str = Query(default=None, max_length=64)
external_id: str = Query(default=None, max_length=256)
extensions: list[str] | None = None
extra: UserExtra | None = None
class UpdateUser(BaseModel):
user_id: str
email: str | None = Query(default=None)
username: str | None = Query(default=..., min_length=2, max_length=20)
extra: UserExtra | None = None
-12
View File
@@ -9,7 +9,6 @@ from enum import Enum
from ecdsa import SECP256k1, SigningKey
from pydantic import BaseModel, Field
from lnbits.db import FilterModel
from lnbits.helpers import url_for
from lnbits.lnurl import encode as lnurl_encode
from lnbits.settings import settings
@@ -26,7 +25,6 @@ class BaseWallet(BaseModel):
class WalletExtra(BaseModel):
icon: str = "flash_on"
color: str = "primary"
pinned: bool = False
class Wallet(BaseModel):
@@ -85,13 +83,3 @@ class KeyType(Enum):
class WalletTypeInfo:
key_type: KeyType
wallet: Wallet
class WalletsFilters(FilterModel):
__search_fields__ = ["id", "name", "currency"]
__sort_fields__ = ["id", "name", "currency", "created_at", "updated_at"]
id: str | None
name: str | None
currency: str | None
+19 -20
View File
@@ -8,15 +8,11 @@ from .payments import (
calculate_fiat_amounts,
check_transaction_status,
check_wallet_limits,
create_fiat_invoice,
create_invoice,
create_wallet_invoice,
fee_reserve,
fee_reserve_total,
get_payments_daily_stats,
pay_invoice,
service_fee,
update_pending_payment,
update_pending_payments,
update_wallet_balance,
)
@@ -34,33 +30,36 @@ from .users import (
from .websockets import websocket_manager, websocket_updater
__all__ = [
# funding source
"get_balance_delta",
"switch_to_voidwallet",
# lnurl
"redeem_lnurl_withdraw",
"perform_lnurlauth",
# notifications
"enqueue_notification",
"send_payment_notification",
# payments
"calculate_fiat_amounts",
"check_admin_settings",
"check_transaction_status",
"check_wallet_limits",
"check_webpush_settings",
"create_fiat_invoice",
"create_invoice",
"create_user_account",
"create_user_account_no_ckeck",
"create_wallet_invoice",
"enqueue_notification",
"fee_reserve",
"fee_reserve_total",
"get_balance_delta",
"get_payments_daily_stats",
"pay_invoice",
"perform_lnurlauth",
"redeem_lnurl_withdraw",
"send_payment_notification",
"service_fee",
"switch_to_voidwallet",
"update_cached_settings",
"update_pending_payment",
"update_pending_payments",
"update_wallet_balance",
# settings
"check_webpush_settings",
"update_cached_settings",
# users
"check_admin_settings",
"create_user_account",
"create_user_account_no_ckeck",
"update_user_account",
"update_user_extensions",
"update_wallet_balance",
# websockets
"websocket_manager",
"websocket_updater",
]
-189
View File
@@ -1,189 +0,0 @@
import hashlib
import hmac
import time
from typing import Optional
from loguru import logger
from lnbits.core.crud import get_wallet
from lnbits.core.crud.payments import create_payment, get_standalone_payment
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.settings import settings
async def handle_fiat_payment_confirmation(
payment: Payment, conn: Optional[Connection] = None
):
try:
await _credit_fiat_service_fee_wallet(payment, conn=conn)
except Exception as e:
logger.warning(e)
try:
await _debit_fiat_service_faucet_wallet(payment, conn=conn)
except Exception as e:
logger.warning(e)
async def _credit_fiat_service_fee_wallet(
payment: Payment, conn: Optional[Connection] = None
):
fiat_provider_name = payment.fiat_provider
if not fiat_provider_name:
return
if payment.fee == 0:
return
limits = settings.get_fiat_provider_limits(fiat_provider_name)
if not limits:
return
if not limits.service_fee_wallet_id:
return
memo = (
f"Service fee for fiat payment of "
f"{abs(payment.sat)} sats. "
f"Provider: {fiat_provider_name}. "
f"Wallet: '{payment.wallet_id}'."
)
create_payment_model = CreatePayment(
wallet_id=limits.service_fee_wallet_id,
bolt11=payment.bolt11,
payment_hash=payment.payment_hash,
amount_msat=abs(payment.fee),
memo=memo,
)
await create_payment(
checking_id=f"service_fee_{payment.payment_hash}",
data=create_payment_model,
status=PaymentState.SUCCESS,
conn=conn,
)
async def _debit_fiat_service_faucet_wallet(
payment: Payment, conn: Optional[Connection] = None
):
fiat_provider_name = payment.fiat_provider
if not fiat_provider_name:
return
limits = settings.get_fiat_provider_limits(fiat_provider_name)
if not limits:
return
if not limits.service_faucet_wallet_id:
return
faucet_wallet = await get_wallet(limits.service_faucet_wallet_id, conn=conn)
if not faucet_wallet:
raise ValueError(
f"Fiat provider '{fiat_provider_name}' faucet wallet not found."
)
memo = (
f"Faucet payment of {abs(payment.sat)} sats. "
f"Provider: {fiat_provider_name}. "
f"Wallet: '{payment.wallet_id}'."
)
create_payment_model = CreatePayment(
wallet_id=limits.service_faucet_wallet_id,
bolt11=payment.bolt11,
payment_hash=payment.payment_hash,
amount_msat=-abs(payment.amount),
memo=memo,
extra=payment.extra,
)
await create_payment(
checking_id=f"internal_fiat_{fiat_provider_name}_"
f"faucet_{payment.payment_hash}",
data=create_payment_model,
status=PaymentState.SUCCESS,
conn=conn,
)
async def handle_stripe_event(event: dict):
event_id = event.get("id")
event_object = event.get("data", {}).get("object", {})
object_type = event_object.get("object")
payment_hash = event_object.get("metadata", {}).get("payment_hash")
logger.debug(
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
f" Payment hash: '{payment_hash}'."
)
if not payment_hash:
logger.warning("Stripe event does not contain a payment hash.")
return
payment = await get_standalone_payment(payment_hash)
if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.")
return
await payment.check_fiat_status()
def check_stripe_signature(
payload: bytes,
sig_header: Optional[str],
secret: Optional[str],
tolerance_seconds=300,
):
if not sig_header:
logger.warning("Stripe-Signature header is missing.")
raise ValueError("Stripe-Signature header is missing.")
if not secret:
logger.warning("Stripe webhook signing secret is not set.")
raise ValueError("Stripe webhook cannot be verified.")
# Split the Stripe-Signature header
items = dict(i.split("=") for i in sig_header.split(","))
timestamp = int(items["t"])
signature = items["v1"]
# Check timestamp tolerance
if abs(time.time() - timestamp) > tolerance_seconds:
logger.warning("Timestamp outside tolerance.")
logger.debug(
f"Current time: {time.time()}, "
f"Timestamp: {timestamp}, "
f"Tolerance: {tolerance_seconds} seconds"
)
raise ValueError("Timestamp outside tolerance." f"Timestamp: {timestamp}")
signed_payload = f"{timestamp}.{payload.decode()}"
# Compute HMAC SHA256 using the webhook secret
computed_signature = hmac.new(
key=secret.encode(), msg=signed_payload.encode(), digestmod=hashlib.sha256
).hexdigest()
# Compare signatures using constant time comparison
if hmac.compare_digest(computed_signature, signature) is not True:
logger.warning("Stripe signature verification failed.")
raise ValueError("Stripe signature verification failed.")
async def test_connection(provider: str) -> SimpleStatus:
"""
Test the connection to Stripe by checking if the API key is valid.
This function should be called when setting up or testing the Stripe integration.
"""
fiat_provider = await get_fiat_provider(provider)
status = await fiat_provider.status()
if status.error_message:
return SimpleStatus(
success=False,
message=f"Cconnection test failed: {status.error_message}",
)
return SimpleStatus(
success=True,
message="Connection test successful." f" Balance: {status.balance}.",
)
+2 -1
View File
@@ -1,4 +1,5 @@
import asyncio
from typing import Tuple
import httpx
from loguru import logger
@@ -47,7 +48,7 @@ async def send_nostr_dm(
return dm_event.to_dict()
async def fetch_nip5_details(identifier: str) -> tuple[str, list[str]]:
async def fetch_nip5_details(identifier: str) -> Tuple[str, list[str]]:
identifier, domain = identifier.split("@")
if not identifier or not domain:
raise ValueError("Invalid NIP5 identifier")
+13 -15
View File
@@ -4,7 +4,7 @@ import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from http import HTTPStatus
from typing import Optional
from typing import Optional, Tuple
import httpx
from loguru import logger
@@ -186,7 +186,7 @@ def is_message_type_enabled(message_type: NotificationType) -> bool:
def _notification_message_to_text(
notification_message: NotificationMessage,
) -> tuple[str, str]:
) -> Tuple[str, str]:
message_type = notification_message.message_type.value
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
try:
@@ -205,23 +205,24 @@ async def dispatch_webhook(payment: Payment):
logger.debug("sending webhook", payment.webhook)
if not payment.webhook:
return await mark_webhook_sent(payment.payment_hash, "-1")
return await mark_webhook_sent(payment.payment_hash, -1)
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
data = payment.dict()
try:
check_callback_url(payment.webhook)
r = await client.post(payment.webhook, json=payment.json(), timeout=40)
r = await client.post(payment.webhook, json=data, timeout=40)
r.raise_for_status()
await mark_webhook_sent(payment.payment_hash, str(r.status_code))
await mark_webhook_sent(payment.payment_hash, r.status_code)
except httpx.HTTPStatusError as exc:
await mark_webhook_sent(payment.payment_hash, str(exc.response.status_code))
await mark_webhook_sent(payment.payment_hash, exc.response.status_code)
logger.warning(
f"webhook returned a bad status_code: {exc.response.status_code} "
f"while requesting {exc.request.url!r}."
)
except httpx.RequestError:
await mark_webhook_sent(payment.payment_hash, "-1")
await mark_webhook_sent(payment.payment_hash, -1)
logger.warning(f"Could not send webhook to {payment.webhook}")
@@ -229,21 +230,18 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
try:
await send_ws_payment_notification(wallet, payment)
except Exception as e:
logger.error(f"Error sending websocket payment notification {e!s}")
logger.error("Error sending websocket payment notification", e)
try:
send_chat_payment_notification(wallet, payment)
except Exception as e:
logger.error(f"Error sending chat payment notification {e!s}")
logger.error("Error sending chat payment notification", e)
try:
await send_payment_push_notification(wallet, payment)
except Exception as e:
logger.error(f"Error sending push payment notification {e!s}")
logger.error("Error sending push payment notification", e)
try:
if payment.webhook and not payment.webhook_status:
await dispatch_webhook(payment)
except Exception as e:
logger.error(f"Error dispatching webhook: {e!s}")
if payment.webhook and not payment.webhook_status:
await dispatch_webhook(payment)
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
+66 -268
View File
@@ -1,10 +1,8 @@
import asyncio
import json
import time
from datetime import datetime, timedelta, timezone
from typing import Optional
import httpx
from bolt11 import Bolt11, MilliSatoshi, Tags
from bolt11 import decode as bolt11_decode
from bolt11 import encode as bolt11_encode
@@ -13,14 +11,11 @@ from loguru import logger
from lnbits.core.crud.payments import get_daily_stats
from lnbits.core.db import db
from lnbits.core.models import PaymentDailyStats, PaymentFilters
from lnbits.core.models.payments import CreateInvoice
from lnbits.db import Connection, Filters
from lnbits.decorators import check_user_extension_access
from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.fiat import get_fiat_provider
from lnbits.helpers import check_callback_url
from lnbits.settings import settings
from lnbits.tasks import create_task, internal_invoice_queue_put
from lnbits.tasks import create_task
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
from lnbits.wallets import fake_wallet, get_funding_source
@@ -49,9 +44,6 @@ from ..models import (
)
from .notifications import send_payment_notification
payment_lock = asyncio.Lock()
wallets_payments_lock: dict[str, asyncio.Lock] = {}
async def pay_invoice(
*,
@@ -87,132 +79,12 @@ async def pay_invoice(
extra=extra,
)
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
service_fee_memo = f"""
Service fee for payment of {abs(payment.sat)} sats.
Wallet: '{wallet.name}' ({wallet.id})."""
payment = await _pay_invoice(wallet, create_payment_model, conn)
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
await _credit_service_fee_wallet(payment, service_fee_memo, new_conn)
await _credit_service_fee_wallet(payment, new_conn)
return payment
async def create_fiat_invoice(
wallet_id: str, invoice_data: CreateInvoice, conn: Optional[Connection] = None
):
fiat_provider_name = invoice_data.fiat_provider
if not fiat_provider_name:
raise ValueError("Fiat provider is required for fiat invoices.")
if not settings.is_fiat_provider_enabled(fiat_provider_name):
raise ValueError(
f"Fiat provider '{fiat_provider_name}' is not enabled.",
)
if invoice_data.unit == "sat":
raise ValueError("Fiat provider cannot be used with satoshis.")
amount_sat = await fiat_amount_as_satoshis(invoice_data.amount, invoice_data.unit)
await _check_fiat_invoice_limits(amount_sat, fiat_provider_name, conn)
invoice_data.internal = True # use FakeWallet for fiat invoices
if not invoice_data.memo:
invoice_data.memo = settings.lnbits_site_title + f" ({fiat_provider_name})"
internal_payment = await create_wallet_invoice(wallet_id, invoice_data)
fiat_provider = await get_fiat_provider(fiat_provider_name)
fiat_invoice = await fiat_provider.create_invoice(
amount=invoice_data.amount,
payment_hash=internal_payment.payment_hash,
currency=invoice_data.unit,
memo=invoice_data.memo,
)
if fiat_invoice.failed:
logger.warning(fiat_invoice.error_message)
internal_payment.status = PaymentState.FAILED
await update_payment(internal_payment, conn=conn)
raise ValueError(
f"Cannot create payment request for '{fiat_provider_name}'.",
)
internal_payment.fee = -abs(
service_fee_fiat(internal_payment.msat, fiat_provider_name)
)
internal_payment.fiat_provider = fiat_provider_name
internal_payment.extra["fiat_checking_id"] = fiat_invoice.checking_id
# todo: move to payent
internal_payment.extra["fiat_payment_request"] = fiat_invoice.payment_request
new_checking_id = (
f"fiat_{fiat_provider_name}_"
f"{fiat_invoice.checking_id or internal_payment.checking_id}"
)
await update_payment(internal_payment, new_checking_id, conn=conn)
internal_payment.checking_id = new_checking_id
return internal_payment
async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
description_hash = b""
unhashed_description = b""
memo = data.memo or settings.lnbits_site_title
if data.description_hash or data.unhashed_description:
if data.description_hash:
try:
description_hash = bytes.fromhex(data.description_hash)
except ValueError as exc:
raise ValueError(
"'description_hash' must be a valid hex string"
) from exc
if data.unhashed_description:
try:
unhashed_description = bytes.fromhex(data.unhashed_description)
except ValueError as exc:
raise ValueError(
"'unhashed_description' must be a valid hex string",
) from exc
# do not save memo if description_hash or unhashed_description is set
memo = ""
payment = await create_invoice(
wallet_id=wallet_id,
amount=data.amount,
memo=memo,
currency=data.unit,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=data.expiry,
extra=data.extra,
webhook=data.webhook,
internal=data.internal,
)
# lnurl_response is not saved in the database
if data.lnurl_callback:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
check_callback_url(data.lnurl_callback)
r = await client.get(
data.lnurl_callback,
params={"pr": payment.bolt11},
timeout=10,
)
if r.is_error:
payment.extra["lnurl_response"] = r.text
else:
resp = json.loads(r.text)
if resp["status"] != "OK":
payment.extra["lnurl_response"] = resp["reason"]
else:
payment.extra["lnurl_response"] = True
except (httpx.ConnectError, httpx.RequestError) as ex:
logger.error(ex)
payment.extra["lnurl_response"] = False
return payment
return payment
async def create_invoice(
@@ -260,29 +132,29 @@ async def create_invoice(
status="failed",
)
payment_response = await funding_source.create_invoice(
(
ok,
checking_id,
payment_request,
error_message,
) = await funding_source.create_invoice(
amount=amount_sat,
memo=invoice_memo,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=expiry or settings.lightning_invoice_expiry,
)
if (
not payment_response.ok
or not payment_response.payment_request
or not payment_response.checking_id
):
if not ok or not payment_request or not checking_id:
raise InvoiceError(
message=payment_response.error_message or "unexpected backend error.",
status="pending",
error_message or "unexpected backend error.", status="pending"
)
invoice = bolt11_decode(payment_response.payment_request)
invoice = bolt11_decode(payment_request)
create_payment_model = CreatePayment(
wallet_id=wallet_id,
bolt11=payment_response.payment_request,
bolt11=payment_request,
payment_hash=invoice.payment_hash,
preimage=payment_response.preimage,
amount_msat=amount_sat * 1000,
expiry=invoice.expiry_date,
memo=memo,
@@ -291,7 +163,7 @@ async def create_invoice(
)
payment = await create_payment(
checking_id=payment_response.checking_id,
checking_id=checking_id,
data=create_payment_model,
conn=conn,
)
@@ -327,7 +199,6 @@ def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
def fee_reserve(amount_msat: int, internal: bool = False) -> int:
amount_msat = abs(amount_msat)
return settings.fee_reserve(amount_msat, internal)
@@ -347,26 +218,6 @@ def service_fee(amount_msat: int, internal: bool = False) -> int:
return 0
def service_fee_fiat(amount_msat: int, fiat_provider_name: str) -> int:
"""
Calculate the service fee for a fiat provider based on the amount in msat.
Return the fee in msat.
"""
limits = settings.get_fiat_provider_limits(fiat_provider_name)
if not limits:
return 0
amount_msat = abs(amount_msat)
fee_max = limits.service_max_fee_sats * 1000
if not limits.service_fee_wallet_id:
return 0
fee_percentage = int(amount_msat / 100 * limits.service_fee_percent)
if fee_max > 0 and fee_percentage > fee_max:
return fee_max
else:
return fee_percentage
async def update_wallet_balance(
wallet: Wallet,
amount: int,
@@ -425,7 +276,10 @@ async def update_wallet_balance(
)
payment.status = PaymentState.SUCCESS
await update_payment(payment, conn=conn)
await internal_invoice_queue_put(payment.checking_id)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(payment.checking_id)
async def check_wallet_limits(
@@ -541,9 +395,8 @@ async def check_transaction_status(
async def get_payments_daily_stats(
filters: Filters[PaymentFilters],
user_id: Optional[str] = None,
) -> list[PaymentDailyStats]:
data_in, data_out = await get_daily_stats(filters, user_id=user_id)
data_in, data_out = await get_daily_stats(filters)
balance_total: float = 0
_none = PaymentDailyStats(date=datetime.now(timezone.utc))
@@ -556,16 +409,13 @@ async def get_payments_daily_stats(
data: list[PaymentDailyStats] = []
def _tz(dt: datetime) -> datetime:
return dt.replace(tzinfo=timezone.utc)
start_date = min(_tz(data_in[0].date), _tz(data_out[0].date))
end_date = max(_tz(data_in[-1].date), _tz(data_out[-1].date))
start_date = min(data_in[0].date, data_out[0].date)
end_date = max(data_in[-1].date, data_out[-1].date)
delta = timedelta(days=1)
while start_date <= end_date:
data_in_point = next((x for x in data_in if _tz(x.date) == start_date), _none)
data_out_point = next((x for x in data_out if _tz(x.date) == start_date), _none)
data_in_point = next((x for x in data_in if x.date == start_date), _none)
data_out_point = next((x for x in data_out if x.date == start_date), _none)
balance_total += data_in_point.balance + data_out_point.balance
data.append(
@@ -587,27 +437,11 @@ async def get_payments_daily_stats(
return data
async def _pay_invoice(
wallet_id: str,
create_payment_model: CreatePayment,
conn: Optional[Connection] = None,
):
async with payment_lock:
if wallet_id not in wallets_payments_lock:
wallets_payments_lock[wallet_id] = asyncio.Lock()
async with wallets_payments_lock[wallet_id]:
# get the wallet again to make sure we have the latest balance
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet:
raise PaymentError(
f"Could not fetch wallet '{wallet_id}'.", status="failed"
)
payment = await _pay_internal_invoice(wallet, create_payment_model, conn)
if not payment:
payment = await _pay_external_invoice(wallet, create_payment_model, conn)
return payment
async def _pay_invoice(wallet, create_payment_model, conn):
payment = await _pay_internal_invoice(wallet, create_payment_model, conn)
if not payment:
payment = await _pay_external_invoice(wallet, create_payment_model, conn)
return payment
async def _pay_internal_invoice(
@@ -642,15 +476,12 @@ async def _pay_internal_invoice(
):
raise PaymentError("Invalid invoice. Bolt11 changed.", status="failed")
fee_reserve_total_msat = fee_reserve_total(amount_msat, internal=True)
fee_reserve_total_msat = fee_reserve_total(abs(amount_msat), internal=True)
create_payment_model.fee = abs(fee_reserve_total_msat)
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
raise PaymentError("Insufficient balance.", status="failed")
# release the preimage
create_payment_model.preimage = internal_invoice.preimage
internal_id = f"internal_{create_payment_model.payment_hash}"
logger.debug(f"creating temporary internal payment with id {internal_id}")
payment = await create_payment(
@@ -688,8 +519,6 @@ async def _pay_external_invoice(
fee_reserve_total_msat = fee_reserve_total(amount_msat, internal=False)
if wallet.balance_msat < abs(amount_msat):
raise PaymentError("Insufficient balance.", status="failed")
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
raise PaymentError(
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
@@ -724,28 +553,41 @@ async def _pay_external_invoice(
logger.debug(f"payment timeout, {checking_id} is still pending")
return payment
# payment failed
if (
payment_response.checking_id is None
or payment_response.ok is False
or payment_response.checking_id != checking_id
):
if payment_response.checking_id and payment_response.checking_id != checking_id:
logger.warning(
f"backend sent unexpected checking_id (expected: {checking_id} got:"
f" {payment_response.checking_id})"
)
if payment_response.checking_id and payment_response.ok is not False:
# payment.ok can be True (paid) or None (pending)!
logger.debug(f"updating payment {checking_id}")
payment.status = (
PaymentState.SUCCESS
if payment_response.ok is True
else PaymentState.PENDING
)
payment.fee = -(abs(payment_response.fee_msat or 0) + abs(service_fee_msat))
payment.preimage = payment_response.preimage
await update_payment(payment, payment_response.checking_id, conn=conn)
payment.checking_id = payment_response.checking_id
if payment.success:
await send_payment_notification(wallet, payment)
logger.success(f"payment successful {payment_response.checking_id}")
elif payment_response.checking_id is None and payment_response.ok is False:
# payment failed
logger.debug(f"payment failed {checking_id}, {payment_response.error_message}")
payment.status = PaymentState.FAILED
await update_payment(payment, conn=conn)
message = payment_response.error_message or "without an error message."
raise PaymentError(f"Payment failed: {message}", status="failed")
# payment.ok can be True (paid) or None (pending)!
payment.status = (
PaymentState.SUCCESS if payment_response.ok is True else PaymentState.PENDING
)
payment.fee = -(abs(payment_response.fee_msat or 0) + abs(service_fee_msat))
payment.preimage = payment_response.preimage
await update_payment(payment, payment_response.checking_id, conn=conn)
payment.checking_id = payment_response.checking_id
if payment.success:
await send_payment_notification(wallet, payment)
logger.success(f"payment successful {payment_response.checking_id}")
raise PaymentError(
f"Payment failed: {payment_response.error_message}"
or "Payment failed, but backend didn't give us an error message.",
status="failed",
)
else:
logger.warning(
"didn't receive checking_id from backend, payment may be stuck in"
f" database: {checking_id}"
)
return payment
@@ -834,7 +676,7 @@ def _validate_payment_request(
async def _credit_service_fee_wallet(
payment: Payment, memo: str, conn: Optional[Connection] = None
payment: Payment, conn: Optional[Connection] = None
):
service_fee_msat = service_fee(payment.amount, internal=payment.is_internal)
if not settings.lnbits_service_fee_wallet or not service_fee_msat:
@@ -845,7 +687,7 @@ async def _credit_service_fee_wallet(
bolt11=payment.bolt11,
payment_hash=payment.payment_hash,
amount_msat=abs(service_fee_msat),
memo=memo,
memo="Service fee",
)
await create_payment(
checking_id=f"service_fee_{payment.payment_hash}",
@@ -853,47 +695,3 @@ async def _credit_service_fee_wallet(
status=PaymentState.SUCCESS,
conn=conn,
)
async def _check_fiat_invoice_limits(
amount_sat: int, fiat_provider_name: str, conn: Optional[Connection] = None
):
limits = settings.get_fiat_provider_limits(fiat_provider_name)
if not limits:
raise ValueError(
f"Fiat provider '{fiat_provider_name}' does not have limits configured.",
)
min_amount_sat = limits.service_min_amount_sats
if min_amount_sat and (amount_sat < min_amount_sat):
raise ValueError(
f"Minimum amount is {min_amount_sat} " f"sats for '{fiat_provider_name}'.",
)
max_amount_sats = limits.service_max_amount_sats
if max_amount_sats and (amount_sat > max_amount_sats):
raise ValueError(
f"Maximum amount is {max_amount_sats} " f"sats for '{fiat_provider_name}'.",
)
if limits.service_max_fee_sats > 0 or limits.service_fee_percent > 0:
if not limits.service_fee_wallet_id:
raise ValueError(
f"Fiat provider '{fiat_provider_name}' service fee wallet missing.",
)
fees_wallet = await get_wallet(limits.service_fee_wallet_id, conn=conn)
if not fees_wallet:
raise ValueError(
f"Fiat provider '{fiat_provider_name}' service fee wallet not found.",
)
if limits.service_faucet_wallet_id:
faucet_wallet = await get_wallet(limits.service_faucet_wallet_id, conn=conn)
if not faucet_wallet:
raise ValueError(
f"Fiat provider '{fiat_provider_name}' faucet wallet not found.",
)
if faucet_wallet.balance < amount_sat:
raise ValueError(
f"The amount exceeds the '{fiat_provider_name}'"
"faucet wallet balance.",
)
+1 -2
View File
@@ -1,7 +1,6 @@
import asyncio
import traceback
from collections.abc import Coroutine
from typing import Callable
from typing import Callable, Coroutine
from loguru import logger
@@ -1,287 +0,0 @@
<q-tab-panel name="fiat_providers">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('fiat_providers')"></span>
<q-btn
round
flat
@click="hideInputsToggle()"
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
></q-btn>
</h6>
<div 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-avatar>
<img
:src="'{{ static_url_for('static', 'images/stripe_logo.ico') }}'"
/>
</q-avatar>
</q-item-section>
<q-item-section> Stripe </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
size="md"
:label="$t('enabled')"
v-model="formData.stripe_enabled"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-expansion-item :label="$t('api')" default-opened>
<q-card-section class="q-pa-md">
<q-input
filled
type="text"
v-model="formData.stripe_api_endpoint"
:label="$t('endpoint')"
></q-input>
<q-input
filled
class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'"
v-model="formData.stripe_api_secret_key"
:label="$t('secret_key')"
></q-input>
<q-input
filled
class="q-mt-md"
type="text"
v-model="formData.stripe_payment_success_url"
:label="$t('callback_success_url')"
:hint="$t('callback_success_url_hint')"
></q-input>
</q-card-section>
<q-card-section class="q-pa-md">
<div class="row">
<div class="col">
<q-btn
outline
color="grey"
class="float-right"
:label="$t('check_connection')"
@click="checkFiatProvider('stripe')"
></q-btn>
</div>
</div>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('webhook')" default-opened>
<q-card-section>
<span v-text="$t('webhook_stripe_description')"></span>
</q-card-section>
<q-card-section>
<q-input
filled
class="q-mt-md"
type="text"
disable
v-model="formData.stripe_payment_webhook_url"
:label="$t('webhook_url')"
:hint="$t('webhook_url_hint')"
></q-input>
<q-input
filled
class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'"
v-model="formData.stripe_webhook_signing_secret"
:label="$t('signing_secret')"
:hint="$t('signing_secret_hint')"
></q-input>
</q-card-section>
<q-card-section>
<span v-text="$t('webhook_events_list')"></span>
<ul>
<li><code>checkout.session.async_payment_failed</code></li>
<li><code>checkout.session.async_payment_succeeded</code></li>
<li><code>checkout.session.completed</code></li>
<li><code>checkout.session.expired</code></li>
</ul>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('service_fee')">
<q-card-section>
<div class="row">
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
type="number"
min="0"
v-model="formData.stripe_limits.service_fee_percent"
@update:model-value="touchSettings()"
:label="$t('service_fee_label')"
:hint="$t('service_fee_hint')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
type="number"
min="0"
v-model="formData.stripe_limits.service_max_fee_sats"
@update:model-value="touchSettings()"
:label="$t('service_fee_max')"
:hint="$t('service_fee_max_hint')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
type="text"
v-model="formData.stripe_limits.service_fee_wallet_id"
@update:model-value="touchSettings()"
:label="$t('fee_wallet_label')"
:hint="$t('fee_wallet_hint')"
></q-input>
</div>
</div>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('amount_limits')">
<q-card-section>
<div class="row">
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
type="number"
min="0"
v-model="formData.stripe_limits.service_min_amount_sats"
@update:model-value="touchSettings()"
:label="$t('min_incoming_payment_amount')"
:hint="$t('min_incoming_payment_amount_desc')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
type="number"
min="0"
v-model="formData.stripe_limits.service_max_amount_sats"
@update:model-value="touchSettings()"
:label="$t('max_incoming_payment_amount')"
:hint="$t('max_incoming_payment_amount_desc')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
v-model="formData.stripe_limits.service_faucet_wallet_id"
@update:model-value="touchSettings()"
:label="$t('faucest_wallet_id')"
:hint="$t('faucest_wallet_id_hint')"
></q-input>
</div>
</div>
<q-item>
<q-item-section>
<q-item-label v-text="$t('faucest_wallet')"></q-item-label>
<q-item-label caption>
<ul>
<li>
<span
v-text="$t('faucest_wallet_desc_1', {provider: 'stripe'})"
></span>
</li>
<li>
<span
v-text="$t('faucest_wallet_desc_2', {provider: 'stripe'})"
></span>
</li>
<li>
<span v-text="$t('faucest_wallet_desc_3')"></span>
</li>
<li>
<span
v-text="$t('faucest_wallet_desc_4', {provider: 'stripe'})"
></span>
</li>
<li>
<span v-text="$t('faucest_wallet_desc_5')"></span>
</li>
</ul>
<br />
</q-item-label>
</q-item-section>
</q-item>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('allowed_users')">
<q-card-section>
<q-input
filled
v-model="formAddStripeUser"
@keydown.enter="addAllowedUser"
type="text"
:label="$t('allowed_users_label')"
:hint="$t('allowed_users_hint_feature', {feature: 'Stripe'})"
>
<q-btn
@click="addStripeAllowedUser"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="user in formData.stripe_limits.allowed_users"
@update:model-value="touchSettings()"
:key="user"
removable
@remove="removeStripeAllowedUser(user)"
color="primary"
text-color="white"
:label="user"
class="ellipsis"
>
</q-chip>
</div>
</q-card-section>
</q-expansion-item>
</q-card>
</q-expansion-item>
<q-separator />
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-avatar>
<img
:src="'{{ static_url_for('static', 'images/square_logo.png') }}'"
/>
</q-avatar>
</q-item-section>
<q-item-section> Square </q-item-section>
<q-item-section side>
<div class="row items-center">Disabled</div>
</q-item-section>
</template>
<q-card>
<q-card-section> Coming Soon </q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</div>
</div>
</q-tab-panel>
+3 -19
View File
@@ -132,8 +132,8 @@
>
<strong class="q-my-none q-mb-sm">Keycloak Auth</strong>
<div class="row q-col-gutter-sm q-col-gutter-y-md">
<div class="col-md-4 col-sm-12">
<div class="row">
<div class="col-md-4 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.keycloak_discovery_url"
@@ -141,7 +141,7 @@
>
</q-input>
</div>
<div class="col-md-4 col-sm-12">
<div class="col-md-4 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.keycloak_client_id"
@@ -159,22 +159,6 @@
>
</q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.keycloak_client_custom_org"
:label="$t('auth_keycloak_custom_org_label')"
>
</q-input>
</div>
<div class="col-md-8 col-sm-12">
<q-input
filled
v-model="formData.keycloak_client_custom_icon"
:label="$t('auth_keycloak_custom_icon_label')"
>
</q-input>
</div>
</div>
</q-card-section>
<q-separator></q-separator>
+17 -28
View File
@@ -115,13 +115,6 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('exchanges')"></span></q-tooltip
></q-tab>
<q-tab
name="fiat_providers"
icon="credit_score"
:label="$q.screen.gt.sm ? $t('fiat_providers') : null"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('fiat_providers')"></span></q-tooltip
></q-tab>
<q-tab
name="users"
icon="group"
@@ -178,27 +171,23 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
<template v-slot:after>
<q-form name="settings_form" id="settings_form">
<q-scroll-area style="height: 100vh">
<q-tab-panels
v-model="tab"
animated
swipeable
vertical
scroll
transition-prev="jump-up"
transition-next="jump-up"
>
{% include "admin/_tab_funding.html" %} {% include
"admin/_tab_users.html" %} {% include "admin/_tab_server.html"
%} {% include "admin/_tab_exchange_providers.html" %}{% include
"admin/_tab_fiat_providers.html" %} {% include
"admin/_tab_extensions.html" %} {% include
"admin/_tab_notifications.html" %} {% include
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
%}{% include "admin/_tab_audit.html"%}{% include
"admin/_tab_library.html"%}
</q-tab-panels>
</q-scroll-area>
<q-tab-panels
v-model="tab"
animated
swipeable
vertical
transition-prev="jump-up"
transition-next="jump-up"
>
{% include "admin/_tab_funding.html" %} {% include
"admin/_tab_users.html" %} {% include "admin/_tab_server.html" %}
{% include "admin/_tab_exchange_providers.html" %} {% include
"admin/_tab_extensions.html" %} {% include
"admin/_tab_notifications.html" %} {% include
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
%}{% include "admin/_tab_audit.html"%}{% include
"admin/_tab_library.html"%}
</q-tab-panels>
</q-form>
</template>
</q-splitter>
-22
View File
@@ -48,17 +48,6 @@
class="cursor-pointer q-ml-sm"
@click="copyText(wallet.adminkey)"
></q-icon>
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
<q-popup-proxy>
<div class="q-pa-md">
<lnbits-qrcode
:value="wallet.adminkey"
:options="{ width: 250 }"
class="rounded-borders"
></lnbits-qrcode>
</div>
</q-popup-proxy>
</q-icon>
</div>
</q-item-section>
</q-item>
@@ -81,17 +70,6 @@
class="cursor-pointer q-ml-sm"
@click="copyText(wallet.inkey)"
></q-icon>
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
<q-popup-proxy>
<div class="q-pa-md">
<lnbits-qrcode
:value="wallet.inkey"
:options="{ width: 250 }"
class="rounded-borders"
></lnbits-qrcode>
</div>
</q-popup-proxy>
</q-icon>
</div>
</q-item-section>
</q-item>
+28 -61
View File
@@ -272,21 +272,10 @@
class="q-mb-md"
>
</q-input>
<q-input
v-model="user.external_id"
:label="$t('external_id')"
filled
dense
readonly
class="q-mb-md"
>
</q-input>
<q-input
v-model="user.extra.picture"
:label="$t('picture')"
filled
dense
class="q-mb-md"
>
</q-input>
@@ -346,111 +335,95 @@
</q-btn-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="user.extra.visible_wallet_count"
:label="$t('visible_wallet_count')"
filled
dense
type="number"
class="q-mb-md"
></q-input>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('color_scheme')"></span>
</div>
<div class="col-8">
<q-btn
v-if="allowedThemes.includes('classic')"
v-if="g.allowedThemes.includes('classic')"
dense
flat
@click="changeTheme('classic')"
@click="themeChoiceFunc('classic')"
icon="circle"
color="deep-purple"
size="md"
><q-tooltip>classic</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('bitcoin')"
v-if="g.allowedThemes.includes('bitcoin')"
dense
flat
@click="changeTheme('bitcoin')"
@click="themeChoiceFunc('bitcoin')"
icon="circle"
color="deep-orange"
size="md"
><q-tooltip>bitcoin</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('mint')"
v-if="g.allowedThemes.includes('mint')"
dense
flat
@click="changeTheme('mint')"
@click="themeChoiceFunc('mint')"
icon="circle"
color="green"
size="md"
><q-tooltip>mint</q-tooltip> </q-btn
><q-btn
v-if="allowedThemes.includes('autumn')"
v-if="g.allowedThemes.includes('autumn')"
dense
flat
@click="changeTheme('autumn')"
@click="themeChoiceFunc('autumn')"
icon="circle"
color="brown"
size="md"
><q-tooltip>autumn</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('monochrome')"
v-if="g.allowedThemes.includes('monochrome')"
dense
flat
@click="changeTheme('monochrome')"
@click="themeChoiceFunc('monochrome')"
icon="circle"
color="grey"
size="md"
><q-tooltip>monochrome</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('salvador')"
v-if="g.allowedThemes.includes('salvador')"
dense
flat
@click="changeTheme('salvador')"
@click="themeChoiceFunc('salvador')"
icon="circle"
color="blue-10"
size="md"
><q-tooltip>elSalvador</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('freedom')"
v-if="g.allowedThemes.includes('freedom')"
dense
flat
@click="changeTheme('freedom')"
@click="themeChoiceFunc('freedom')"
icon="circle"
color="pink-13"
size="md"
><q-tooltip>freedom</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('cyber')"
v-if="g.allowedThemes.includes('cyber')"
dense
flat
@click="changeTheme('cyber')"
@click="themeChoiceFunc('cyber')"
icon="circle"
color="light-green-9"
size="md"
><q-tooltip>cyber</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('flamingo')"
v-if="g.allowedThemes.includes('flamingo')"
dense
flat
@click="changeTheme('flamingo')"
@click="themeChoiceFunc('flamingo')"
icon="circle"
color="pink-3"
size="md"
@@ -464,9 +437,9 @@
</div>
<div class="col-8">
<q-input
v-model="bgimageChoice"
v-model="bgimageSelection"
:label="$t('background_image')"
@update:model-value="applyBackgroundImage"
@update:model-value="bgimageChoiceFunc"
>
<q-tooltip
><span v-text="$t('background_image')"></span
@@ -479,18 +452,19 @@
<span v-text="$t('gradient_background')"></span>
</div>
<div class="col-8">
<q-toggle
<q-btn
dense
flat
round
@click="toggleGradient"
icon="gradient"
size="sm"
v-model="gradientChoice"
@update:model-value="applyGradient"
>
<q-tooltip
><span v-text="$t('toggle_gradient')"></span
></q-tooltip>
</q-toggle>
</q-btn>
</div>
</div>
@@ -499,11 +473,10 @@
<span v-text="$t('toggle_darkmode')"></span>
</div>
<div class="col-8">
<q-toggle
<q-btn
dense
flat
round
v-model="darkChoice"
@click="toggleDarkMode"
:icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'"
size="sm"
@@ -511,7 +484,7 @@
<q-tooltip
><span v-text="$t('toggle_darkmode')"></span
></q-tooltip>
</q-toggle>
</q-btn>
</div>
</div>
<div class="row q-mb-md">
@@ -520,7 +493,7 @@
</div>
<div class="col-8">
<q-select
v-model="borderChoice"
v-model="borderSelection"
:options="borderOptions"
label="Borders"
@update:model-value="applyBorder"
@@ -548,7 +521,7 @@
</div>
<div class="col-8">
<q-select
v-model="reactionChoice"
v-model="reactionSelection"
:options="reactionOptions"
label="Reactions"
@update:model-value="reactionChoiceFunc"
@@ -559,12 +532,6 @@
</q-select>
</div>
</div>
<q-separator></q-separator>
<div class="row q-mb-md q-mt-md">
<q-btn @click="updateAccount" unelevated color="primary">
<span v-text="$t('update_account')"></span>
</q-btn>
</div>
</q-tab-panel>
<q-tab-panel name="api_acls">
<div class="row q-mb-md">
+14 -12
View File
@@ -59,6 +59,9 @@
}
}
</style>
<script>
const ALLOWED_REGISTER = {{ LNBITS_NEW_ACCOUNTS_ALLOWED | tojson }};
</script>
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
{% endblock %} {% block page_container %}
<q-page-container>
@@ -148,7 +151,7 @@
<username-password
v-if="authMethod != 'user-id-only'"
:allowed_new_users="allowedRegister"
:auth-methods="LNBITS_AUTH_METHODS"
:auth-methods="{{ LNBITS_AUTH_METHODS }}"
:auth-action="authAction"
v-model:user-name="username"
v-model:password_1="password"
@@ -166,12 +169,11 @@
v-if="authAction === 'login' && allowedRegister"
class="q-mb-none"
>
Not registered?
<a
href="#"
Not registered? Create an
<span
class="text-secondary cursor-pointer"
@click.prevent="showRegister('username-password')"
>Create an Account</a
@click="showRegister('username-password')"
>Account</span
>
</p>
<p
@@ -180,15 +182,15 @@
>
<span v-text="$t('new_user_not_allowed')"></span>
</p>
<p v-else-if="authAction === 'register'" class="q-mb-none">
<span v-text="$t('existing_account_question')"></span>
<a
href="#"
class="text-secondary cursor-pointer q-ml-sm"
@click.prevent="showLogin('username-password')"
<span
class="text-secondary cursor-pointer"
@click="showLogin('username-password')"
v-text="$t('login')"
></a>
></span>
</p>
</div>
</username-password>
@@ -250,7 +252,7 @@
></q-btn>
</div>
</div>
{% if AD_SPACE_ENABLED and AD_SPACE %}
{% if AD_SPACE %}
<div class="q-pt-md full-width">
<div class="row justify-center q-mb-xl">
<div class="full-width text-center">
+30 -127
View File
@@ -5,7 +5,7 @@
{% block scripts %} {{ window_vars(user, wallet) }}{% endblock %} {% block page
%}
<div class="row q-col-gutter-md">
{% if HIDE_API and AD_SPACE_ENABLED and AD_SPACE %}
{% if HIDE_API and AD_SPACE %}
<div class="col-12 col-md-8 q-gutter-y-md">
{% elif HIDE_API %}
<div class="col-12 q-gutter-y-md">
@@ -31,7 +31,7 @@
<q-card-section style="height: 130px">
<div class="row q-gutter-md">
<div
v-if="isSatsDenomination"
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
class="col-1"
style="max-width: 30px"
>
@@ -211,12 +211,7 @@
</q-card>
<div id="hiddenQrCodeContainer" style="display: none">
<lnbits-qrcode
v-if="receive.fiatPaymentReq"
:value="receive.fiatPaymentReq"
></lnbits-qrcode>
<lnbits-qrcode
v-else
:value="'lightning:' + (this.receive.paymentReq || '').toUpperCase()"
:value="'lightning:' + this.receive.paymentReq"
></lnbits-qrcode>
</div>
</div>
@@ -247,23 +242,6 @@
{{ SITE_TITLE }} Wallet:
<strong><em v-text="g.wallet.name"></em></strong>
</div>
<q-space></q-space>
<div class="float-right">
<q-btn
@click="updateWallet({ pinned: !g.wallet.extra.pinned })"
round
class="float-right"
:color="g.wallet.extra.pinned ? 'primary' : 'grey-5'"
text-color="black"
size="sm"
icon="push_pin"
style="transform: rotate(30deg)"
>
<q-tooltip
><span v-text="$t('pin_wallet')"></span
></q-tooltip>
</q-btn>
</div>
</div>
</q-card-section>
<q-card-section class="q-pa-none">
@@ -490,7 +468,7 @@
</q-list>
</q-card-section>
</q-card>
{% endif %} {% if AD_SPACE_ENABLED and AD_SPACE %}
{% endif %} {% if AD_SPACE %}
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
@@ -624,30 +602,14 @@
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% else %}
<div class="row">
<div class="col-10">
<q-select
filled
dense
v-model="receive.unit"
type="text"
:label="$t('unit')"
:options="receive.units"
></q-select>
</div>
<div class="col-2">
<q-btn
v-if="g.fiatTracking"
@click="swapBalancePriority"
class="float-right"
color="primary"
flat
dense
icon="swap_vert"
></q-btn>
</div>
</div>
<q-select
filled
dense
v-model="receive.unit"
type="text"
:label="$t('unit')"
:options="receive.units"
></q-select>
<q-input
ref="setAmount"
filled
@@ -665,59 +627,9 @@
<q-input
filled
dense
type="textarea"
rows="2"
v-model.trim="receive.data.memo"
:label="$t('memo')"
></q-input>
<div v-if="g.user.fiat_providers?.length" class="q-mt-md">
<q-list bordered dense class="rounded-borders">
<q-item-label dense header>
<span v-text="$t('select_payment_provider')"></span>
</q-item-label>
<q-separator></q-separator>
<q-item
:active="!receive.fiatProvider"
@click="receive.fiatProvider = ''"
active-class="bg-teal-1 text-grey-8 text-weight-bold"
clickable
v-ripple
>
<q-item-section avatar>
<q-avatar square>
<img
:src="'{{ static_url_for('static', 'images/logos/lnbits.png') }}'"
/>
</q-avatar>
</q-item-section>
<q-item-section>
<span
v-text="$t('pay_with', {provider: 'Lightning Network'})"
></span>
</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item
:active="receive.fiatProvider === 'stripe'"
@click="receive.fiatProvider = 'stripe'"
active-class="bg-teal-1 text-grey-8 text-weight-bold"
clickable
v-ripple
>
<q-item-section avatar>
<q-avatar>
<img
:src="'{{ static_url_for('static', 'images/stripe_logo.ico') }}'"
/>
</q-avatar>
</q-item-section>
<q-item-section>
<span v-text="$t('pay_with', {provider: 'Stripe'})"></span>
</q-item-section>
</q-item>
</q-list>
</div>
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn
unelevated
@@ -727,7 +639,7 @@
>
<span
v-if="receive.lnurl"
v-text="`${$t('withdraw_from')} ${receive.lnurl.domain}`"
v-text="$t('withdraw_from') + receive.lnurl.domain"
></span>
<span v-else v-text="$t('create_invoice')"></span>
</q-btn>
@@ -751,14 +663,7 @@
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<div class="text-center q-mb-lg">
<a
v-if="receive.fiatPaymentReq"
:href="receive.fiatPaymentReq"
target="_blank"
>
<div v-html="invoiceQrCode"></div>
</a>
<a v-else :href="'lightning:' + receive.paymentReq">
<a :href="'lightning:' + receive.paymentReq">
<div v-html="invoiceQrCode"></div>
</a>
</div>
@@ -769,27 +674,25 @@
<h5 v-if="receive.unit != 'sat'" class="q-mt-none q-mb-sm">
<span v-text="formattedSatAmount"></span>
</h5>
<div v-if="!receive.fiatPaymentReq">
<q-chip v-if="hasNfc" outline square color="positive">
<q-avatar
icon="nfc"
color="positive"
text-color="white"
></q-avatar>
<span v-text="$t('nfc_supported')"></span>
</q-chip>
<span
v-else
class="text-caption text-grey"
v-text="$t('nfc_not_supported')"
></span>
</div>
<q-chip v-if="hasNfc" outline square color="positive">
<q-avatar
icon="nfc"
color="positive"
text-color="white"
></q-avatar>
<span v-text="$t('nfc_supported')"></span>
</q-chip>
<span
v-else
class="text-caption text-grey"
v-text="$t('nfc_not_supported')"
></span>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(receive.fiatPaymentReq || receive.paymentReq)"
@click="copyText(receive.paymentReq)"
:label="$t('copy_invoice')"
></q-btn>
<q-btn
@@ -1058,8 +961,8 @@
dense
v-model.number="parse.data.amount"
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
:mask="parse.data.unit == 'sat' ? '#' : ''"
:step="parse.data.unit == 'sat' ? '1': '0.01'"
:mask="parse.data.unit != 'sat' ? '#.##' : '#'"
:step="parse.data.unit != 'sat' ? '0.01' : '1'"
fill-mask="0"
reverse-fill-mask
:min="parse.lnurlpay.minSendable / 1000"
-165
View File
@@ -1,165 +0,0 @@
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm q-pl-lg">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<q-btn
@click="showAddWalletDialog.show = true"
:label="$t('add_wallet')"
color="primary"
>
</q-btn>
</div>
<div class="float-left">
<q-input
:label="$t('search_wallets')"
dense
class="float-right q-pr-xl"
v-model="walletsTable.search"
>
<template v-slot:before>
<q-icon name="search"> </q-icon>
</template>
<template v-slot:append>
<q-icon
v-if="walletsTable.search !== ''"
name="close"
@click="walletsTable.search = ''"
class="cursor-pointer"
>
</q-icon>
</template>
</q-input>
</div>
</div>
</div>
</q-card>
</div>
</div>
<div>
<div>
<div>
<q-table
grid
grid-header
flat
bordered
:rows="wallets"
:columns="walletsTable.columns"
v-model:pagination="walletsTable.pagination"
:loading="walletsTable.loading"
@request="getUserWallets"
row-key="id"
:filter="filter"
hide-header
>
<template v-slot:item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
<q-card
class="q-ma-sm cursor-pointer wallet-list-card"
style="text-decoration: none"
@click="goToWallet(props.row.id)"
>
<q-card-section>
<div class="row items-center">
<q-avatar
size="lg"
:text-color="$q.dark.isActive ? 'black' : 'grey-3'"
:color="props.row.extra.color"
:icon="props.row.extra.icon"
>
</q-avatar>
<div
class="text-h6 q-pl-md ellipsis"
class="text-bold"
v-text="props.row.name"
></div>
<q-space> </q-space>
<q-btn
v-if="props.row.extra.pinned"
round
color="primary"
text-color="black"
size="xs"
icon="push_pin"
class="float-right"
style="transform: rotate(30deg)"
></q-btn>
</div>
<div class="row items-center q-pt-sm">
<h6 class="q-my-none ellipsis full-width">
<strong
v-text="formatBalance(props.row.balance_msat / 1000)"
></strong>
</h6>
</div>
</q-card-section>
<q-separator />
<q-card-section class="text-left">
<small>
<strong>
<span v-text="$t('currency')"></span>
</strong>
<span v-text="props.row.currency || 'sat'"></span>
</small>
<br />
<small>
<strong>
<span v-text="$t('id')"></span>
:
</strong>
<span v-text="props.row.id"></span>
</small>
</q-card-section>
</q-card>
</div>
</template>
</q-table>
</div>
</div>
</div>
<q-dialog
v-model="showAddWalletDialog.show"
persistent
@hide="showAddWalletDialog = {show: false}"
>
<q-card style="min-width: 350px">
<q-card-section>
<div class="text-h6">
<span v-text="$t('wallet_name')"></span>
</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-input
dense
v-model="showAddWalletDialog.name"
autofocus
@keyup.enter="submitAddWallet()"
></q-input>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat :label="$t('cancel')" v-close-popup></q-btn>
<q-btn
flat
:label="$t('add_wallet')"
v-close-popup
@click="submitAddWallet()"
></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
{% endblock %}
+8 -50
View File
@@ -232,57 +232,15 @@
/>
</template>
</q-input>
<q-btn
<q-select
v-else-if="['status'].includes(col.name)"
flat
dense
:label="$q.screen.gt.md ? 'Status' : null"
icon="filter_alt"
color="grey"
class="text-capitalize"
>
<q-menu anchor="top right" self="top start">
<q-item dense>
<q-checkbox
v-model="statusFilters.success"
@click="handleFilterChanged"
label="Success Payments"
></q-checkbox>
</q-item>
<q-item dense>
<q-checkbox
v-model="statusFilters.pending"
@click="handleFilterChanged"
label="Pending Payments"
></q-checkbox>
</q-item>
<q-item dense>
<q-checkbox
v-model="statusFilters.failed"
@click="handleFilterChanged"
label="Failed Payments"
></q-checkbox>
</q-item>
<q-separator></q-separator>
<q-item dense>
<q-checkbox
v-model="statusFilters.incoming"
@click="handleFilterChanged"
label="Incoming Payments"
></q-checkbox>
</q-item>
<q-item dense>
<q-checkbox
v-model="statusFilters.outgoing"
@click="handleFilterChanged"
label="Outgoing Payments"
></q-checkbox>
</q-item>
</q-menu>
<q-tooltip>
<span v-text="$t('filter_payments')"></span>
</q-tooltip>
</q-btn>
v-model="searchData[col.name]"
:options="searchOptions[col.name]"
@update:model-value="searchPaymentsBy()"
:label="col.label"
clearable
style="width: 100px"
></q-select>
<q-select
v-else-if="['tag'].includes(col.name)"
v-model="searchData[col.name]"
+2 -11
View File
@@ -61,7 +61,7 @@
:label="$t('set_password')"
v-model="activeUser.setPassword"
>
<q-tooltip v-text="$t('set_password_tooltip')"></q-tooltip>
<q-tooltip>Toggle Admin</q-tooltip>
</q-toggle>
<q-input
@@ -105,12 +105,11 @@
<q-input
v-model="activeUser.data.pubkey"
:label="'Nostr '+ $t('pubkey')"
:label="$t('pubkey')"
filled
dense
class="q-mb-md"
>
<q-tooltip v-text="$t('nostr_pubkey_tooltip')"></q-tooltip>
</q-input>
<q-input
v-model="activeUser.data.email"
@@ -147,14 +146,6 @@
class="q-mb-md"
>
</q-input>
<q-input
v-model="activeUser.data.external_id"
:label="$t('external_id')"
filled
dense
class="q-mb-md"
>
</q-input>
<q-input
v-model="activeUser.data.extra.picture"
:label="$t('picture')"
+13 -30
View File
@@ -15,36 +15,19 @@
</q-card>
</div>
<div v-else-if="activeWallet.show">
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row">
<div class="q-pa-xs">
<q-btn
icon="arrow_back_ios"
@click="backToUsersPage()"
:label="$t('back')"
></q-btn>
</div>
<div class="q-pa-xs">
<q-btn
@click="createWalletDialog.show = true"
:label="$t('create_new_wallet')"
color="primary"
></q-btn>
</div>
<div class="q-pa-xs">
<q-btn
@click="deleteAllUserWallets(activeWallet.userId)"
:label="$t('delete_all_wallets')"
icon="delete"
color="negative"
></q-btn>
</div>
</div>
</div>
</q-card>
<div class="row q-mb-lg">
<div class="col">
<q-btn
icon="arrow_back_ios"
@click="backToUsersPage()"
:label="$t('back')"
></q-btn>
<q-btn
@click="createWalletDialog.show = true"
:label="$t('create_new_wallet')"
color="primary"
class="q-ml-md"
></q-btn>
</div>
</div>
<q-card class="q-pa-md">
+2 -2
View File
@@ -29,7 +29,7 @@ from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
from lnbits.tasks import invoice_listeners
from .. import core_app_extra
from ..crud import get_admin_settings, reset_core_settings, update_admin_settings
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
admin_router = APIRouter(tags=["Admin UI"], prefix="/admin")
file_upload = File(...)
@@ -113,7 +113,7 @@ async def api_reset_settings(field_name: str):
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
enqueue_notification(NotificationType.settings_update, {"username": user.username})
await reset_core_settings()
await delete_admin_settings()
server_restart.set()
+6 -12
View File
@@ -130,20 +130,14 @@ async def api_lnurlscan(
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
check_callback_url(url)
try:
r = await client.get(url, timeout=5)
r.raise_for_status()
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 404:
raise HTTPException(HTTPStatus.NOT_FOUND, "Not found") from exc
r = await client.get(url, timeout=5)
r.raise_for_status()
if r.is_error:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={
"domain": domain,
"message": "failed to get parameters",
},
) from exc
detail={"domain": domain, "message": "failed to get parameters"},
)
try:
data = json.loads(r.text)
except json.decoder.JSONDecodeError as exc:
+32 -24
View File
@@ -74,7 +74,7 @@ async def get_auth_user(user: User = Depends(check_user_exists)) -> User:
async def login(data: LoginUsernamePassword) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException(
HTTPStatus.FORBIDDEN, "Login by 'Username and Password' not allowed."
HTTPStatus.UNAUTHORIZED, "Login by 'Username and Password' not allowed."
)
account = await get_account_by_username_or_email(data.username)
if not account or not account.verify_password(data.password):
@@ -85,7 +85,9 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
@auth_router.post("/nostr", description="Login via Nostr")
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.")
raise HTTPException(
HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed."
)
event = _nostr_nip98_event(request)
account = await get_account_by_pubkey(event["pubkey"])
if not account:
@@ -102,7 +104,7 @@ async def nostr_login(request: Request) -> JSONResponse:
async def login_usr(data: LoginUsr) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
raise HTTPException(
HTTPStatus.FORBIDDEN,
HTTPStatus.UNAUTHORIZED,
"Login by 'User ID' not allowed.",
)
account = await get_account(data.usr)
@@ -110,7 +112,7 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
if account.is_admin:
raise HTTPException(
HTTPStatus.FORBIDDEN, "Admin users cannot login with user id only."
HTTPStatus.UNAUTHORIZED, "Admin users cannot login with user id only."
)
return _auth_success_response(account.username, account.id, account.email)
@@ -240,7 +242,7 @@ async def login_with_sso_provider(
provider_sso = _new_sso(provider)
if not provider_sso:
raise HTTPException(
HTTPStatus.FORBIDDEN,
HTTPStatus.UNAUTHORIZED,
f"Login by '{provider}' not allowed.",
)
@@ -255,7 +257,7 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons
provider_sso = _new_sso(provider)
if not provider_sso:
raise HTTPException(
HTTPStatus.FORBIDDEN,
HTTPStatus.UNAUTHORIZED,
f"Login by '{provider}' not allowed.",
)
@@ -283,7 +285,7 @@ async def logout() -> JSONResponse:
async def register(data: RegisterUser) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException(
HTTPStatus.FORBIDDEN,
HTTPStatus.UNAUTHORIZED,
"Register by 'Username and Password' not allowed.",
)
@@ -373,7 +375,7 @@ async def update_password(
async def reset_password(data: ResetUserPassword) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException(
HTTPStatus.FORBIDDEN, "Auth by 'Username and Password' not allowed."
HTTPStatus.UNAUTHORIZED, "Auth by 'Username and Password' not allowed."
)
assert data.password == data.password_repeat, "Passwords do not match."
@@ -411,13 +413,23 @@ async def update(
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
if data.username and not is_valid_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
if data.email != user.email:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"Email mismatch.",
)
if (
data.username
and user.username != data.username
and await get_account_by_username(data.username)
):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
if (
data.email
and data.email != user.email
and await get_account_by_email(data.email)
):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Email already exists.")
account = await get_account(user.id)
if not account:
@@ -425,6 +437,8 @@ async def update(
if data.username:
account.username = data.username
if data.email:
account.email = data.email
if data.extra:
account.extra = data.extra
@@ -435,7 +449,7 @@ async def update(
@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")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "This is not your first install")
account = await get_account(settings.super_user)
if not account:
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
@@ -458,10 +472,10 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
if verified_user_id:
if account:
raise HTTPException(HTTPStatus.FORBIDDEN, "Email already used.")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Email already used.")
account = await get_account(verified_user_id)
if not account:
raise HTTPException(HTTPStatus.FORBIDDEN, "Cannot verify user email.")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Cannot verify user email.")
redirect_path = "/account"
if account:
@@ -485,12 +499,9 @@ def _auth_success_response(
sub=username or "", usr=user_id, email=email, auth_time=int(time())
)
access_token = create_access_token(data=payload.dict())
max_age = settings.auth_token_expire_minutes * 60
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
response.set_cookie(
"cookie_access_token", access_token, httponly=True, max_age=max_age
)
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
response.set_cookie("cookie_access_token", access_token, httponly=True)
response.set_cookie("is_lnbits_user_authorized", "true")
response.delete_cookie("is_access_token_expired")
return response
@@ -510,12 +521,9 @@ def _auth_api_token_response(
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
access_token = create_access_token(data=payload.dict())
max_age = settings.auth_token_expire_minutes * 60
response = RedirectResponse(path)
response.set_cookie(
"cookie_access_token", access_token, httponly=True, max_age=max_age
)
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
response.set_cookie("cookie_access_token", access_token, httponly=True)
response.set_cookie("is_lnbits_user_authorized", "true")
response.delete_cookie("is_access_token_expired")
return response
@@ -566,10 +574,10 @@ def _find_auth_provider_class(provider: str) -> Callable:
def _nostr_nip98_event(request: Request) -> dict:
auth_header = request.headers.get("Authorization")
if not auth_header:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr Auth header missing.")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Nostr Auth header missing.")
scheme, token = auth_header.split()
if scheme.lower() != "nostr":
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid Authorization scheme.")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid Authorization scheme.")
event = None
try:
event_json = base64.b64decode(token.encode("ascii"))
-35
View File
@@ -1,35 +0,0 @@
from fastapi import APIRouter, Request
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.services.fiat_providers import (
check_stripe_signature,
handle_stripe_event,
)
from lnbits.settings import settings
callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
@callback_router.post("/{provider_name}")
async def api_generic_webhook_handler(
provider_name: str, request: Request
) -> SimpleStatus:
if provider_name.lower() == "stripe":
payload = await request.body()
sig_header = request.headers.get("Stripe-Signature")
check_stripe_signature(
payload, sig_header, settings.stripe_webhook_signing_secret
)
event = await request.json()
await handle_stripe_event(event)
return SimpleStatus(
success=True,
message=f"Callback received successfully from '{provider_name}'.",
)
return SimpleStatus(
success=False,
message=f"Unknown fiat provider '{provider_name}'.",
)
-18
View File
@@ -1,18 +0,0 @@
from http import HTTPStatus
from fastapi import APIRouter, Depends
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.services.fiat_providers import test_connection
from lnbits.decorators import check_admin
fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
@fiat_router.put(
"/check/{provider}",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_admin)],
)
async def api_test_fiat_provider(provider: str) -> SimpleStatus:
return await test_connection(provider)
+3 -22
View File
@@ -1,5 +1,5 @@
from http import HTTPStatus
from typing import Annotated, Optional, Union
from typing import Annotated, List, Optional, Union
from urllib.parse import urlencode, urlparse
import httpx
@@ -70,7 +70,7 @@ async def robots():
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
async def extensions(request: Request, user: User = Depends(check_user_exists)):
installed_exts: list[InstallableExtension] = await get_installed_extensions()
installed_exts: List[InstallableExtension] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts]
installable_exts = await InstallableExtension.get_installable_extensions()
@@ -216,25 +216,6 @@ async def account(
)
@generic_router.get(
"/wallets",
response_class=HTMLResponse,
description="show wallets page",
)
async def wallets(
request: Request,
user: User = Depends(check_user_exists),
):
return template_renderer().TemplateResponse(
request,
"core/wallets.html",
{
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@generic_router.get("/service-worker.js")
async def service_worker(request: Request):
return template_renderer().TemplateResponse(
@@ -421,7 +402,7 @@ async def audit_index(request: Request, user: User = Depends(check_admin)):
@generic_router.get("/payments", response_class=HTMLResponse)
async def payments_index(request: Request, user: User = Depends(check_user_exists)):
async def payments_index(request: Request, user: User = Depends(check_admin)):
return template_renderer().TemplateResponse(
"payments/index.html",
{
+5 -8
View File
@@ -1,5 +1,5 @@
from http import HTTPStatus
from typing import Optional
from typing import List, Optional
import httpx
from fastapi import APIRouter, Body, Depends, HTTPException
@@ -91,7 +91,7 @@ async def api_get_info(
@node_router.get("/channels")
async def api_get_channels(
node: Node = Depends(require_node),
) -> Optional[list[NodeChannel]]:
) -> Optional[List[NodeChannel]]:
return await node.get_channels()
@@ -121,7 +121,7 @@ async def api_delete_channel(
output_index: Optional[int],
force: bool = False,
node: Node = Depends(require_node),
) -> Optional[list[NodeChannel]]:
) -> Optional[List[NodeChannel]]:
return await node.close_channel(
short_id,
(
@@ -170,7 +170,7 @@ async def api_get_invoices(
@node_router.get("/peers")
async def api_get_peers(node: Node = Depends(require_node)) -> list[NodePeerInfo]:
async def api_get_peers(node: Node = Depends(require_node)) -> List[NodePeerInfo]:
return await node.get_peers()
@@ -212,10 +212,7 @@ async def api_get_1ml_stats(node: Node = Depends(require_node)) -> Optional[Node
r = await client.get(url=f"https://1ml.com/node/{node_id}/json", timeout=15)
try:
r.raise_for_status()
data = r.json()
if "noderank" not in data:
return None
return data["noderank"]
return r.json()["noderank"]
except httpx.HTTPStatusError as exc:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Node not found on 1ml.com"
+103 -57
View File
@@ -2,7 +2,7 @@ import json
import ssl
from http import HTTPStatus
from math import ceil
from typing import Optional
from typing import List, Optional
from urllib.parse import urlparse
import httpx
@@ -34,11 +34,17 @@ from lnbits.core.models import (
PaymentFilters,
PaymentHistoryPoint,
PaymentWalletStats,
Wallet,
)
from lnbits.core.models.users import User
from lnbits.core.services.payments import (
get_payments_daily_stats,
update_pending_payment,
)
from lnbits.db import Filters, Page
from lnbits.decorators import (
WalletTypeInfo,
check_admin,
check_user_exists,
parse_filters,
require_admin_key,
@@ -62,12 +68,9 @@ from ..crud import (
get_wallet_for_key,
)
from ..services import (
create_fiat_invoice,
create_wallet_invoice,
create_invoice,
fee_reserve_total,
get_payments_daily_stats,
pay_invoice,
update_pending_payment,
update_pending_payments,
)
@@ -79,7 +82,7 @@ payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
name="Payment List",
summary="get list of payments",
response_description="list of payments",
response_model=list[Payment],
response_model=List[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments(
@@ -98,7 +101,7 @@ async def api_payments(
@payment_router.get(
"/history",
name="Get payments history",
response_model=list[PaymentHistoryPoint],
response_model=List[PaymentHistoryPoint],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_history(
@@ -113,50 +116,36 @@ async def api_payments_history(
@payment_router.get(
"/stats/count",
name="Get payments history for all users",
response_model=list[PaymentCountStat],
dependencies=[Depends(check_admin)],
response_model=List[PaymentCountStat],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_counting_stats(
count_by: PaymentCountField = Query("tag"),
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
user: User = Depends(check_user_exists),
):
if user.admin:
# admin user can see payments from all wallets
for_user_id = None
else:
# regular user can only see payments from their wallets
for_user_id = user.id
return await get_payment_count_stats(count_by, filters=filters, user_id=for_user_id)
return await get_payment_count_stats(count_by, filters)
@payment_router.get(
"/stats/wallets",
name="Get payments history for all users",
response_model=list[PaymentWalletStats],
dependencies=[Depends(check_admin)],
response_model=List[PaymentWalletStats],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_wallets_stats(
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
user: User = Depends(check_user_exists),
):
if user.admin:
# admin user can see payments from all wallets
for_user_id = None
else:
# regular user can only see payments from their wallets
for_user_id = user.id
return await get_wallets_stats(filters, user_id=for_user_id)
return await get_wallets_stats(filters)
@payment_router.get(
"/stats/daily",
name="Get payments history per day",
response_model=list[PaymentDailyStats],
response_model=List[PaymentDailyStats],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_daily_stats(
@@ -164,13 +153,22 @@ async def api_payments_daily_stats(
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
):
if user.admin:
# admin user can see payments from all wallets
for_user_id = None
else:
# regular user can only see payments from their wallets
for_user_id = user.id
return await get_payments_daily_stats(filters, user_id=for_user_id)
if not user.admin:
exc = HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Missing wallet id.",
)
wallet_filter = next(
(f for f in filters.filters if f.field == "wallet_id"), None
)
if not wallet_filter:
raise exc
wallet_id = list((wallet_filter.values or {}).values())
if len(wallet_id) == 0:
raise exc
if not user.get_wallet(wallet_id[0]):
raise exc
return await get_payments_daily_stats(filters)
@payment_router.get(
@@ -196,6 +194,69 @@ async def api_payments_paginated(
return page
async def _api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
description_hash = b""
unhashed_description = b""
memo = data.memo or settings.lnbits_site_title
if data.description_hash or data.unhashed_description:
if data.description_hash:
try:
description_hash = bytes.fromhex(data.description_hash)
except ValueError as exc:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'description_hash' must be a valid hex string",
) from exc
if data.unhashed_description:
try:
unhashed_description = bytes.fromhex(data.unhashed_description)
except ValueError as exc:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'unhashed_description' must be a valid hex string",
) from exc
# do not save memo if description_hash or unhashed_description is set
memo = ""
payment = await create_invoice(
wallet_id=wallet.id,
amount=data.amount,
memo=memo,
currency=data.unit,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=data.expiry,
extra=data.extra,
webhook=data.webhook,
internal=data.internal,
)
# lnurl_response is not saved in the database
if data.lnurl_callback:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
check_callback_url(data.lnurl_callback)
r = await client.get(
data.lnurl_callback,
params={"pr": payment.bolt11},
timeout=10,
)
if r.is_error:
payment.extra["lnurl_response"] = r.text
else:
resp = json.loads(r.text)
if resp["status"] != "OK":
payment.extra["lnurl_response"] = resp["reason"]
else:
payment.extra["lnurl_response"] = True
except (httpx.ConnectError, httpx.RequestError) as ex:
logger.error(ex)
payment.extra["lnurl_response"] = False
return payment
@payment_router.get(
"/all/paginated",
name="Payment List",
@@ -203,21 +264,13 @@ async def api_payments_paginated(
response_description="list of payments",
response_model=Page[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
dependencies=[Depends(check_admin)],
)
async def api_all_payments_paginated(
filters: Filters = Depends(parse_filters(PaymentFilters)),
user: User = Depends(check_user_exists),
):
if user.admin:
# admin user can see payments from all wallets
for_user_id = None
else:
# regular user can only see payments from their wallets
for_user_id = user.id
return await get_payments_paginated(
filters=filters,
user_id=for_user_id,
)
@@ -243,7 +296,6 @@ async def api_payments_create(
invoice_data: CreateInvoice,
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> Payment:
wallet_id = wallet.wallet.id
if invoice_data.out is True and wallet.key_type == KeyType.admin:
if not invoice_data.bolt11:
raise HTTPException(
@@ -251,24 +303,21 @@ async def api_payments_create(
detail="Missing BOLT11 invoice",
)
payment = await pay_invoice(
wallet_id=wallet_id,
wallet_id=wallet.wallet.id,
payment_request=invoice_data.bolt11,
extra=invoice_data.extra,
)
return payment
if invoice_data.out:
elif not invoice_data.out:
# invoice key
return await _api_payments_create_invoice(invoice_data, wallet.wallet)
else:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invoice (or Admin) key required.",
)
# If the payment is not outgoing, we can create a new invoice.
if invoice_data.fiat_provider:
return await create_fiat_invoice(wallet_id, invoice_data)
return await create_wallet_invoice(wallet_id, invoice_data)
@payment_router.get("/fee-reserve")
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
@@ -378,9 +427,6 @@ async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
return {"paid": True, "preimage": payment.preimage, "details": payment}
return {"paid": True, "preimage": payment.preimage}
if payment.failed:
return {"paid": False, "status": "failed", "details": payment}
try:
status = await payment.check_status()
except Exception:
+3 -27
View File
@@ -2,7 +2,7 @@ import base64
import json
import time
from http import HTTPStatus
from typing import Optional
from typing import List, Optional
from uuid import uuid4
import shortuuid
@@ -100,7 +100,6 @@ async def api_create_user(data: CreateUser) -> CreateUser:
username=data.username,
email=data.email,
pubkey=data.pubkey,
external_id=data.external_id,
extra=data.extra,
)
account.validate_fields()
@@ -133,7 +132,6 @@ async def api_update_user(
username=data.username,
email=data.email,
pubkey=data.pubkey,
external_id=data.external_id,
extra=data.extra or UserExtra(),
)
await update_user_account(account)
@@ -150,7 +148,7 @@ async def api_update_user(
async def api_users_delete_user(
user_id: str, user: User = Depends(check_admin)
) -> SimpleStatus:
wallets = await get_wallets(user_id, deleted=False)
wallets = await get_wallets(user_id)
if len(wallets) > 0:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
@@ -216,7 +214,7 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
async def api_users_get_user_wallet(user_id: str) -> List[Wallet]:
return await get_wallets(user_id)
@@ -259,28 +257,6 @@ async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleSta
return SimpleStatus(success=True, message="Wallet is already active.")
@users_router.delete(
"/user/{user_id}/wallets",
name="Delete all wallets for user",
summary="Soft delete (only sets a flag) all user wallets.",
)
async def api_users_delete_all_user_wallet(user_id: str) -> SimpleStatus:
if user_id == settings.super_user:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Action not allowed.",
)
wallets = await get_wallets(user_id, deleted=False)
for wallet in wallets:
await delete_wallet(user_id=user_id, wallet_id=wallet.id)
return SimpleStatus(
success=True,
message=f"Deleted '{len(wallets)}' wallets. ",
)
@users_router.delete(
"/user/{user_id}/wallet/{wallet}",
name="Delete wallet by id",
-27
View File
@@ -9,18 +9,13 @@ from fastapi import (
HTTPException,
)
from lnbits.core.crud.wallets import get_wallets_paginated
from lnbits.core.models import CreateWallet, KeyType, User, Wallet
from lnbits.core.models.wallets import WalletsFilters
from lnbits.db import Filters, Page
from lnbits.decorators import (
WalletTypeInfo,
check_user_exists,
parse_filters,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import generate_filter_params_openapi
from ..crud import (
create_wallet,
@@ -43,26 +38,6 @@ async def api_wallet(key_info: WalletTypeInfo = Depends(require_invoice_key)):
return res
@wallet_router.get(
"/paginated",
name="Wallet List",
summary="get paginated list of user wallets",
response_description="list of user wallets",
response_model=Page[Wallet],
openapi_extra=generate_filter_params_openapi(WalletsFilters),
)
async def api_wallets_paginated(
user: User = Depends(check_user_exists),
filters: Filters = Depends(parse_filters(WalletsFilters)),
):
page = await get_wallets_paginated(
user_id=user.id,
filters=filters,
)
return page
@wallet_router.put("/{new_name}")
async def api_update_wallet_name(
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
@@ -99,7 +74,6 @@ async def api_update_wallet(
icon: Optional[str] = Body(None),
color: Optional[str] = Body(None),
currency: Optional[str] = Body(None),
pinned: Optional[bool] = Body(None),
key_info: WalletTypeInfo = Depends(require_admin_key),
) -> Wallet:
wallet = await get_wallet(key_info.wallet.id)
@@ -108,7 +82,6 @@ async def api_update_wallet(
wallet.name = name or wallet.name
wallet.extra.icon = icon or wallet.extra.icon
wallet.extra.color = color or wallet.extra.color
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
wallet.currency = currency if currency is not None else wallet.currency
await update_wallet(wallet)
return wallet
+7 -20
View File
@@ -554,13 +554,9 @@ class Filters(BaseModel, Generic[TFilterModel]):
for page_filter in self.filters:
where_stmts.append(page_filter.statement)
if self.search and self.model and self.model.__search_fields__:
# Use `COALESCE` to handle `NULL` values and `||`
# for cross-database compatible string concatenation
_fields = self.model.__search_fields__
search_expr = " || ".join(
f"COALESCE(CAST({field} AS TEXT), '')" for field in _fields
where_stmts.append(
f"lower(concat({', '.join(self.model.__search_fields__)})) LIKE :search"
)
where_stmts.append(f"lower({search_expr}) LIKE :search")
if where_stmts:
return "WHERE " + " AND ".join(where_stmts)
@@ -580,7 +576,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
for key, value in page_filter.values.items():
values[key] = value
if self.search and self.model:
values["search"] = f"%{self.search.lower()}%"
values["search"] = f"%{self.search}%"
return values
@@ -648,9 +644,9 @@ def model_to_dict(model: BaseModel) -> dict:
def dict_to_submodel(model: type[TModel], value: dict | str) -> TModel | None:
"""convert a dictionary or JSON string to a Pydantic model"""
if isinstance(value, str):
if value == "null" or value == "":
if value == "null":
return None
_subdict = _safe_load_json(value)
_subdict = json.loads(value)
elif isinstance(value, dict):
_subdict = value
@@ -674,7 +670,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
type_ = model.__fields__[key].type_
outertype_ = model.__fields__[key].outer_type_
if get_origin(outertype_) is list:
_items = _safe_load_json(value) if isinstance(value, str) else value
_items = json.loads(value) if isinstance(value, str) else value
_dict[key] = [
dict_to_submodel(type_, v) if issubclass(type_, BaseModel) else v
for v in _items
@@ -695,7 +691,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
# TODO: remove this when all sub models are migrated to Pydantic
# NOTE: this is for type dict on BaseModel, (used in Payment class)
if type_ is dict and value:
_dict[key] = _safe_load_json(value)
_dict[key] = json.loads(value)
continue
_dict[key] = value
continue
@@ -703,12 +699,3 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
if isinstance(_model, BaseModel):
_model.__init__(**_dict) # type: ignore
return _model
def _safe_load_json(value: str) -> dict:
try:
return json.loads(value)
except json.JSONDecodeError:
# DB is corrupted if it gets here
logger.error(f"Failed to decode JSON: '{value}'")
return {}
+7 -20
View File
@@ -1,5 +1,5 @@
from http import HTTPStatus
from typing import Annotated, Literal, Optional, Union
from typing import Annotated, Literal, Optional, Type, Union
import jwt
from fastapi import Cookie, Depends, Query, Request, Security
@@ -28,7 +28,7 @@ from lnbits.core.models import (
WalletTypeInfo,
)
from lnbits.db import Connection, Filter, Filters, TFilterModel
from lnbits.helpers import normalize_path, path_segments
from lnbits.helpers import path_segments
from lnbits.settings import AuthMethods, settings
oauth2_scheme = OAuth2PasswordBearer(
@@ -102,7 +102,7 @@ class KeyChecker(SecurityBase):
request.scope["user_id"] = wallet.user
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invalid adminkey.",
)
@@ -165,7 +165,7 @@ async def check_user_exists(
r.scope["user_id"] = account.id
if not settings.is_user_allowed(account.id):
raise HTTPException(HTTPStatus.FORBIDDEN, "User not allowed.")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not allowed.")
user = await get_user_from_account(account)
if not user:
@@ -201,7 +201,7 @@ async def access_token_payload(
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
raise HTTPException(
HTTPStatus.FORBIDDEN, "User not authorized. No admin privileges."
HTTPStatus.UNAUTHORIZED, "User not authorized. No admin privileges."
)
if not user.has_password:
raise HTTPException(
@@ -214,7 +214,7 @@ async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
if user.id != settings.super_user:
raise HTTPException(
HTTPStatus.FORBIDDEN, "User not authorized. No super user privileges."
HTTPStatus.UNAUTHORIZED, "User not authorized. No super user privileges."
)
if not user.has_password:
raise HTTPException(
@@ -223,7 +223,7 @@ async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) ->
return user
def parse_filters(model: type[TFilterModel]):
def parse_filters(model: Type[TFilterModel]):
"""
Parses the query params as filters.
:param model: model used for validation of filter values
@@ -346,16 +346,3 @@ async def _check_account_api_access(
raise HTTPException(HTTPStatus.FORBIDDEN, "Path not allowed.")
if not endpoint.supports_method(method):
raise HTTPException(HTTPStatus.FORBIDDEN, "Method not allowed.")
def url_for_interceptor(original_method):
def normalize_url(self, *args, **kwargs):
url = original_method(self, *args, **kwargs)
return url.replace(path=normalize_path(url.path))
return normalize_url
# Upgraded extensions modify the path.
# This interceptor ensures that the path is normalized.
Request.url_for = url_for_interceptor(Request.url_for) # type: ignore[method-assign]
+3 -9
View File
@@ -7,7 +7,6 @@ from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, RedirectResponse, Response
from loguru import logger
from shortuuid import uuid
from lnbits.settings import settings
@@ -26,10 +25,6 @@ class InvoiceError(Exception):
self.status = status
class UnsupportedError(Exception):
pass
def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
@@ -76,11 +71,10 @@ def register_exception_handlers(app: FastAPI):
async def exception_handler(request: Request, exc: Exception):
etype, _, tb = sys.exc_info()
traceback.print_exception(etype, exc, tb)
exception_id = uuid()
logger.error(f"Exception ID: {exception_id}\n{exc!s}")
logger.error(f"Exception: {exc!s}")
return render_html_error(request, exc) or JSONResponse(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
content={"detail": f"Unexpected error! ID: {exception_id}"},
content={"detail": str(exc)},
)
@app.exception_handler(AssertionError)
@@ -151,7 +145,7 @@ def register_exception_handlers(app: FastAPI):
status_code = HTTPStatus.NOT_FOUND
message: str = "Page not found."
if settings.is_ready_to_install_extension_id(path):
if path in settings.lnbits_all_extensions_ids:
status_code = HTTPStatus.FORBIDDEN
message = f"Extension '{path}' not installed. Ask the admin to install it."
-46
View File
@@ -1,46 +0,0 @@
from __future__ import annotations
import importlib
from enum import Enum
from lnbits.fiat.base import FiatProvider
from lnbits.settings import settings
from .stripe import StripeWallet
fiat_module = importlib.import_module("lnbits.fiat")
class FiatProviderType(Enum):
stripe = "StripeWallet"
async def get_fiat_provider(name: str) -> FiatProvider:
if name not in FiatProviderType.__members__:
raise ValueError(f"Fiat provider '{name}' is not supported.")
fiat_provider = fiat_providers.get(name)
if fiat_provider:
status = await fiat_provider.status(only_check_settings=True)
if status.error_message:
await fiat_provider.cleanup()
del fiat_providers[name]
else:
return fiat_provider
fiat_providers[name] = _init_fiat_provider(FiatProviderType[name])
return fiat_providers[name]
def _init_fiat_provider(fiat_provider: FiatProviderType) -> FiatProvider:
if not settings.is_fiat_provider_enabled(fiat_provider.name):
raise ValueError(f"Fiat provider '{fiat_provider.name}' not enabled.")
provider_constructor = getattr(fiat_module, fiat_provider.value)
return provider_constructor()
fiat_providers: dict[str, FiatProvider] = {}
__all__ = [
"StripeWallet",
]
-135
View File
@@ -1,135 +0,0 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator, Coroutine
from typing import TYPE_CHECKING, NamedTuple
if TYPE_CHECKING:
pass
class FiatStatusResponse(NamedTuple):
error_message: str | None = None
balance: float = 0
class FiatInvoiceResponse(NamedTuple):
ok: bool
checking_id: str | None = None # payment_hash, rpc_id
payment_request: str | None = None
error_message: str | None = None
@property
def success(self) -> bool:
return self.ok is True
@property
def pending(self) -> bool:
return self.ok is None
@property
def failed(self) -> bool:
return self.ok is False
class FiatPaymentResponse(NamedTuple):
# when ok is None it means we don't know if this succeeded
ok: bool | None = None
checking_id: str | None = None # payment_hash, rcp_id
fee: float | None = None
error_message: str | None = None
@property
def success(self) -> bool:
return self.ok is True
@property
def pending(self) -> bool:
return self.ok is None
@property
def failed(self) -> bool:
return self.ok is False
class FiatPaymentStatus(NamedTuple):
paid: bool | None = None
fee: float | None = None # todo: what fee is this?
@property
def success(self) -> bool:
return self.paid is True
@property
def pending(self) -> bool:
return self.paid is not True
@property
def failed(self) -> bool:
return self.paid is False
def __str__(self) -> str:
if self.success:
return "success"
if self.failed:
return "failed"
return "pending"
class FiatPaymentSuccessStatus(FiatPaymentStatus):
paid = True
class FiatPaymentFailedStatus(FiatPaymentStatus):
paid = False
class FiatPaymentPendingStatus(FiatPaymentStatus):
paid = None
class FiatProvider(ABC):
@abstractmethod
async def cleanup(self):
pass
@abstractmethod
def status(
self, only_check_settings: bool | None = False
) -> Coroutine[None, None, FiatStatusResponse]:
pass
@abstractmethod
def create_invoice(
self,
amount: float,
payment_hash: str,
currency: str,
memo: str | None = None,
**kwargs,
) -> Coroutine[None, None, FiatInvoiceResponse]:
pass
@abstractmethod
def pay_invoice(
self,
payment_request: str,
) -> Coroutine[None, None, FiatPaymentResponse]:
pass
@abstractmethod
def get_invoice_status(
self, checking_id: str
) -> Coroutine[None, None, FiatPaymentStatus]:
pass
@abstractmethod
def get_payment_status(
self, checking_id: str
) -> Coroutine[None, None, FiatPaymentStatus]:
pass
async def paid_invoices_stream(
self,
) -> AsyncGenerator[str, None]:
yield ""
-174
View File
@@ -1,174 +0,0 @@
import asyncio
import json
from collections.abc import AsyncGenerator
from datetime import datetime, timedelta, timezone
from typing import Optional
from urllib.parse import urlencode
import httpx
from loguru import logger
from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings
from .base import (
FiatInvoiceResponse,
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentResponse,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
FiatProvider,
FiatStatusResponse,
)
class StripeWallet(FiatProvider):
"""https://docs.stripe.com/api"""
def __init__(self):
logger.debug("Initializing StripeWallet")
self._settings_fields = self._settings_connection_fields()
if not settings.stripe_api_endpoint:
raise ValueError("Cannot initialize StripeWallet: missing endpoint.")
if not settings.stripe_api_secret_key:
raise ValueError("Cannot initialize StripeWallet: missing API secret key.")
self.endpoint = normalize_endpoint(settings.stripe_api_endpoint)
self.headers = {
"Authorization": f"Bearer {settings.stripe_api_secret_key}",
"User-Agent": settings.user_agent,
}
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
logger.info("StripeWallet initialized.")
async def cleanup(self):
try:
await self.client.aclose()
except RuntimeError as e:
logger.warning(f"Error closing stripe wallet connection: {e}")
async def status(
self, only_check_settings: Optional[bool] = False
) -> FiatStatusResponse:
if only_check_settings:
if self._settings_fields != self._settings_connection_fields():
return FiatStatusResponse("Connection settings have changed.", 0)
return FiatStatusResponse(balance=0)
try:
r = await self.client.get(url="/v1/balance", timeout=15)
r.raise_for_status()
data = r.json()
available_balance = data.get("available", [{}])[0].get("amount", 0)
# pending_balance = data.get("pending", {}).get("amount", 0)
return FiatStatusResponse(balance=available_balance)
except json.JSONDecodeError:
return FiatStatusResponse("Server error: 'invalid json response'", 0)
except Exception as exc:
logger.warning(exc)
return FiatStatusResponse(f"Unable to connect to {self.endpoint}.", 0)
async def create_invoice(
self,
amount: float,
payment_hash: str,
currency: str,
memo: Optional[str] = None,
**kwargs,
) -> FiatInvoiceResponse:
amount_cents = int(amount * 100)
form_data = [
("mode", "payment"),
(
"success_url",
settings.stripe_payment_success_url or "https://lnbits.com",
),
("metadata[payment_hash]", payment_hash),
("line_items[0][price_data][currency]", currency.lower()),
("line_items[0][price_data][product_data][name]", memo or "LNbits Invoice"),
("line_items[0][price_data][unit_amount]", amount_cents),
("line_items[0][quantity]", "1"),
]
encoded_data = urlencode(form_data)
try:
headers = self.headers.copy()
headers["Content-Type"] = "application/x-www-form-urlencoded"
r = await self.client.post(
url="/v1/checkout/sessions", headers=headers, content=encoded_data
)
r.raise_for_status()
data = r.json()
session_id = data.get("id")
if not session_id:
return FiatInvoiceResponse(
ok=False, error_message="Server error: 'missing session id'"
)
payment_request = data.get("url")
if not payment_request:
return FiatInvoiceResponse(
ok=False, error_message="Server error: 'missing payment URL'"
)
return FiatInvoiceResponse(
ok=True, checking_id=session_id, payment_request=payment_request
)
except json.JSONDecodeError:
return FiatInvoiceResponse(
ok=False, error_message="Server error: 'invalid json response'"
)
except Exception as exc:
logger.warning(exc)
return FiatInvoiceResponse(
ok=False, error_message=f"Unable to connect to {self.endpoint}."
)
async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
raise NotImplementedError("Stripe does not support paying invoices directly.")
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
try:
r = await self.client.get(
url=f"/v1/checkout/sessions/{checking_id}",
)
r.raise_for_status()
data = r.json()
payment_status = data.get("payment_status")
if not payment_status:
return FiatPaymentPendingStatus()
if payment_status == "paid":
# todo: handle fee
return FiatPaymentSuccessStatus()
expires_at = data.get("expires_at")
_24_hours_ago = datetime.now(timezone.utc) - timedelta(hours=24)
if expires_at and expires_at < _24_hours_ago.timestamp():
# be defensive: add a 24 hour buffer
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
except Exception as exc:
logger.debug(f"Error getting invoice status: {exc}")
return FiatPaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> FiatPaymentStatus:
raise NotImplementedError("Stripe does not support outgoing payments.")
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
logger.warning(
"Stripe does not support paid invoices stream. Use webhooks instead."
)
mock_queue: asyncio.Queue[str] = asyncio.Queue(0)
while settings.lnbits_running:
value = await mock_queue.get()
yield value
def _settings_connection_fields(self) -> str:
return "-".join(
[str(settings.stripe_api_endpoint), str(settings.stripe_api_secret_key)]
)
+67 -82
View File
@@ -3,7 +3,7 @@ import json
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Optional
from typing import Any, Optional, Type
from urllib import request
from urllib.parse import urlparse
@@ -11,12 +11,12 @@ import jinja2
import jwt
import shortuuid
from fastapi.routing import APIRoute
from loguru import logger
from packaging import version
from pydantic.schema import field_schema
from lnbits.jinja2_templating import Jinja2Templates
from lnbits.nodes import get_node_class
from lnbits.requestvars import g
from lnbits.settings import settings
from lnbits.utils.crypto import AESCipher
@@ -41,7 +41,7 @@ def urlsafe_short_hash() -> str:
def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str:
base = f"http://{settings.host}:{settings.port}" if external else ""
base = g().base_url if external else ""
url_params = "?"
for key, value in params.items():
url_params += f"{key}={value}&"
@@ -64,52 +64,63 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
t.env.globals["static_url_for"] = static_url_for
window_settings = {
"AD_SPACE": settings.lnbits_ad_space.split(","),
"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 get_node_class() is not None,
"LNBITS_NODE_UI_AVAILABLE": get_node_class() is not None,
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
"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,
"USE_DEFAULT_REACTION": settings.lnbits_default_reaction,
"USE_DEFAULT_THEME": settings.lnbits_default_theme,
"USE_DEFAULT_BORDER": settings.lnbits_default_border,
"USE_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
"LNBITS_DENOMINATION": (
settings.lnbits_denomination
if settings.lnbits_denomination == "FakeWallet"
else "sats"
),
}
if settings.lnbits_ad_space_enabled:
t.env.globals["AD_SPACE"] = settings.lnbits_ad_space.split(",")
t.env.globals["AD_SPACE_TITLE"] = settings.lnbits_ad_space_title
t.env.globals["WINDOW_SETTINGS"] = window_settings
for key, value in window_settings.items():
t.env.globals[key] = value
t.env.globals["VOIDWALLET"] = settings.lnbits_backend_wallet_class == "VoidWallet"
t.env.globals["HIDE_API"] = settings.lnbits_hide_api
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
t.env.globals["LNBITS_DENOMINATION"] = (
settings.lnbits_denomination
if settings.lnbits_backend_wallet_class == "FakeWallet"
else "sats"
)
t.env.globals["SITE_TAGLINE"] = settings.lnbits_site_tagline
t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description
t.env.globals["LNBITS_SHOW_HOME_PAGE_ELEMENTS"] = (
settings.lnbits_show_home_page_elements
)
t.env.globals["LNBITS_CUSTOM_IMAGE"] = settings.lnbits_custom_image
t.env.globals["LNBITS_CUSTOM_BADGE"] = settings.lnbits_custom_badge
t.env.globals["LNBITS_CUSTOM_BADGE_COLOR"] = settings.lnbits_custom_badge_color
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.lnbits_theme_options
t.env.globals["LNBITS_QR_LOGO"] = settings.lnbits_qr_logo
t.env.globals["LNBITS_VERSION"] = settings.version
t.env.globals["LNBITS_NEW_ACCOUNTS_ALLOWED"] = settings.new_accounts_allowed
t.env.globals["LNBITS_AUTH_METHODS"] = settings.auth_allowed_methods
t.env.globals["LNBITS_ADMIN_UI"] = settings.lnbits_admin_ui
t.env.globals["LNBITS_EXTENSIONS_DEACTIVATE_ALL"] = (
settings.lnbits_extensions_deactivate_all
)
t.env.globals["LNBITS_AUDIT_ENABLED"] = settings.lnbits_audit_enabled
t.env.globals["LNBITS_SERVICE_FEE"] = settings.lnbits_service_fee
t.env.globals["LNBITS_SERVICE_FEE_MAX"] = settings.lnbits_service_fee_max
t.env.globals["LNBITS_SERVICE_FEE_WALLET"] = settings.lnbits_service_fee_wallet
t.env.globals["LNBITS_NODE_UI"] = (
settings.lnbits_node_ui and get_node_class() is not None
)
t.env.globals["LNBITS_NODE_UI_AVAILABLE"] = get_node_class() is not None
t.env.globals["EXTENSIONS"] = list(settings.lnbits_installed_extensions_ids)
if settings.lnbits_custom_logo:
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
if settings.lnbits_default_reaction:
t.env.globals["USE_DEFAULT_REACTION"] = settings.lnbits_default_reaction
if settings.lnbits_default_theme:
t.env.globals["USE_DEFAULT_THEME"] = settings.lnbits_default_theme
if settings.lnbits_default_border:
t.env.globals["USE_DEFAULT_BORDER"] = settings.lnbits_default_border
if settings.lnbits_default_gradient:
t.env.globals["USE_DEFAULT_GRADIENT"] = settings.lnbits_default_gradient
if settings.lnbits_default_bgimage:
t.env.globals["USE_DEFAULT_BGIMAGE"] = settings.lnbits_default_bgimage
if settings.bundle_assets:
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
@@ -123,6 +134,8 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
t.env.globals["WEBPUSH_PUBKEY"] = settings.lnbits_webpush_pubkey
return t
@@ -153,7 +166,7 @@ def get_current_extension_name() -> str:
return ext_name
def generate_filter_params_openapi(model: type[FilterModel], keep_optional=False):
def generate_filter_params_openapi(model: Type[FilterModel], keep_optional=False):
"""
Generate openapi documentation for Filters. This is intended to be used along
parse_filters (see example)
@@ -197,14 +210,6 @@ def is_valid_username(username: str) -> bool:
return re.fullmatch(username_regex, username) is not None
def is_valid_external_id(external_id: str) -> bool:
if len(external_id) > 256:
return False
if " " in external_id or "\n" in external_id:
return False
return True
def is_valid_pubkey(pubkey: str) -> bool:
if len(pubkey) != 64:
return False
@@ -289,21 +294,12 @@ def is_lnbits_version_ok(
def check_callback_url(url: str):
if not settings.lnbits_callback_url_rules:
# no rules, all urls are allowed
return
u = urlparse(url)
netloc = urlparse(url).netloc
for rule in settings.lnbits_callback_url_rules:
try:
if re.match(rule, f"{u.scheme}://{u.netloc}") is not None:
return
except re.error:
logger.debug(f"Invalid regex rule: '{rule}'. ")
continue
raise ValueError(
f"Callback not allowed. URL: {url}. Netloc: {u.netloc}. "
f"Please check your admin settings."
)
if re.match(rule, netloc) is None:
raise ValueError(
f"Callback not allowed. URL: {url}. Netloc: {netloc}. Rule: {rule}"
)
def download_url(url, save_path):
@@ -362,14 +358,3 @@ def safe_upload_file_path(filename: str, directory: str = "images") -> Path:
# Prevent filename with subdirectories
file_path = image_folder / filename.split("/")[-1]
return file_path.resolve()
def normalize_endpoint(endpoint: str, add_proto=True) -> str:
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
if add_proto:
if endpoint.startswith("ws://") or endpoint.startswith("wss://"):
return endpoint
endpoint = (
f"https://{endpoint}" if not endpoint.startswith("http") else endpoint
)
return endpoint
+2 -2
View File
@@ -58,9 +58,9 @@ class LnurlErrorResponseHandler(APIRoute):
__all__ = [
"LnurlErrorResponse",
"LnurlErrorResponseHandler",
"decode",
"encode",
"handle",
"LnurlErrorResponse",
"LnurlErrorResponseHandler",
]
+2 -2
View File
@@ -2,7 +2,7 @@ import asyncio
import json
from datetime import datetime, timezone
from http import HTTPStatus
from typing import Any, Optional, Union
from typing import Any, List, Optional, Union
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
@@ -61,7 +61,7 @@ class InstalledExtensionMiddleware:
await self.app(scope, receive, send)
def _response_by_accepted_type(
self, scope: Scope, headers: list[Any], msg: str, status_code: HTTPStatus
self, scope: Scope, headers: List[Any], msg: str, status_code: HTTPStatus
) -> Union[HTMLResponse, JSONResponse]:
"""
Build an HTTP response containing the `msg` as HTTP body and the `status_code`
+10
View File
@@ -0,0 +1,10 @@
import contextvars
import types
request_global = contextvars.ContextVar(
"request_global", default=types.SimpleNamespace()
)
def g() -> types.SimpleNamespace:
return request_global.get()
+1 -83
View File
@@ -381,7 +381,7 @@ class SecuritySettings(LNbitsSettings):
lnbits_allowed_ips: list[str] = Field(default=[])
lnbits_blocked_ips: list[str] = Field(default=[])
lnbits_callback_url_rules: list[str] = Field(
default=["https?://([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})(:\\d+)?"]
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
)
lnbits_wallet_limit_max_balance: int = Field(default=0, ge=0)
@@ -553,44 +553,7 @@ class BoltzFundingSource(LNbitsSettings):
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
boltz_client_macaroon: str | None = Field(default=None)
boltz_client_wallet: str | None = Field(default="lnbits")
boltz_client_password: str = Field(default="")
boltz_client_cert: str | None = Field(default=None)
boltz_mnemonic: str | None = Field(default=None)
class StrikeFundingSource(LNbitsSettings):
strike_api_endpoint: str | None = Field(
default="https://api.strike.me/v1", env="STRIKE_API_ENDPOINT"
)
strike_api_key: str | None = Field(default=None, env="STRIKE_API_KEY")
class FiatProviderLimits(BaseModel):
# empty list means all users are allowed to receive payments via Stripe
allowed_users: list[str] = Field(default=[])
service_max_fee_sats: int = Field(default=0)
service_fee_percent: float = Field(default=0)
service_fee_wallet_id: str | None = Field(default=None)
service_min_amount_sats: int = Field(default=0)
service_max_amount_sats: int = Field(default=0)
service_faucet_wallet_id: str | None = Field(default="")
class StripeFiatProvider(LNbitsSettings):
stripe_enabled: bool = Field(default=False)
stripe_api_endpoint: str = Field(default="https://api.stripe.com")
stripe_api_secret_key: str | None = Field(default=None)
stripe_payment_success_url: str = Field(default="https://lnbits.com")
stripe_payment_webhook_url: str = Field(
default="https://your-lnbits-domain-here.com/api/v1/callback/stripe"
)
# Use this secret to verify that events come from Stripe.
stripe_webhook_signing_secret: str | None = Field(default=None)
stripe_limits: FiatProviderLimits = Field(default_factory=FiatProviderLimits)
class LightningSettings(LNbitsSettings):
@@ -617,7 +580,6 @@ class FundingSourcesSettings(
LnTipsFundingSource,
NWCFundingSource,
BreezSdkFundingSource,
StrikeFundingSource,
):
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
# How long to wait for the payment to be confirmed before returning a pending status
@@ -625,40 +587,6 @@ class FundingSourcesSettings(
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
class FiatProvidersSettings(StripeFiatProvider):
def is_fiat_provider_enabled(self, provider: str | None) -> bool:
"""
Checks if a specific fiat provider is enabled.
"""
if not provider:
return False
if provider == "stripe":
return self.stripe_enabled
# Add checks for other fiat providers here as needed
return False
def get_fiat_providers_for_user(self, user_id: str) -> list[str]:
"""
Returns a list of fiat payment methods allowed for the user.
"""
allowed_providers = []
if self.stripe_enabled and (
not self.stripe_limits.allowed_users
or user_id in self.stripe_limits.allowed_users
):
allowed_providers.append("stripe")
# Add other fiat providers here as needed
return allowed_providers
def get_fiat_provider_limits(self, provider_name: str) -> FiatProviderLimits | None:
"""
Returns the limits for a specific fiat provider.
"""
return getattr(self, provider_name + "_limits", None)
class WebPushSettings(LNbitsSettings):
lnbits_webpush_pubkey: str | None = Field(default=None)
lnbits_webpush_privkey: str | None = Field(default=None)
@@ -731,8 +659,6 @@ class KeycloakAuthSettings(LNbitsSettings):
keycloak_discovery_url: str = Field(default="")
keycloak_client_id: str = Field(default="")
keycloak_client_secret: str = Field(default="")
keycloak_client_custom_org: str | None = Field(default=None)
keycloak_client_custom_icon: str | None = Field(default=None)
class AuditSettings(LNbitsSettings):
@@ -831,7 +757,6 @@ class EditableSettings(
SecuritySettings,
NotificationsSettings,
FundingSourcesSettings,
FiatProvidersSettings,
LightningSettings,
WebPushSettings,
NodeUISettings,
@@ -938,7 +863,6 @@ class SuperUserSettings(LNbitsSettings):
"VoidWallet",
"ZBDWallet",
"NWCWallet",
"StrikeWallet",
]
)
@@ -1026,12 +950,6 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
def is_installed_extension_id(self, ext_id: str) -> bool:
return ext_id in self.lnbits_installed_extensions_ids
def is_ready_to_install_extension_id(self, ext_id: str) -> bool:
return (
ext_id not in self.lnbits_installed_extensions_ids
and ext_id in self.lnbits_all_extensions_ids
)
class SuperSettings(EditableSettings):
super_user: str
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+10 -10
View File
File diff suppressed because one or more lines are too long
+572 -173
View File
@@ -1,218 +1,617 @@
body[data-theme=classic].neon-border .q-card,
body[data-theme=classic].neon-border .q-card.q-card--dark,
body[data-theme=classic].neon-border .q-date,
body[data-theme=classic].neon-border .q-date--dark {
border: 2px solid #673ab7;
box-shadow: none;
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
body[data-theme=bitcoin].neon-border .q-card,
body[data-theme=bitcoin].neon-border .q-card.q-card--dark,
body[data-theme=bitcoin].neon-border .q-date,
body[data-theme=bitcoin].neon-border .q-date--dark {
border: 2px solid #ea611d;
box-shadow: none;
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
body[data-theme=freedom].neon-border .q-card,
body[data-theme=freedom].neon-border .q-card.q-card--dark,
body[data-theme=freedom].neon-border .q-date,
body[data-theme=freedom].neon-border .q-date--dark {
border: 2px solid #e22156;
box-shadow: none;
[data-theme=classic] .q-drawer--dark,
body[data-theme=classic].body--dark,
[data-theme=classic] .q-menu--dark {
background: #1f2234 !important;
}
body[data-theme=cyber].neon-border .q-card,
body[data-theme=cyber].neon-border .q-card.q-card--dark,
body[data-theme=cyber].neon-border .q-date,
body[data-theme=cyber].neon-border .q-date--dark {
border: 2px solid #7cb342;
box-shadow: none;
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='classic'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
body[data-theme=mint].neon-border .q-card,
body[data-theme=mint].neon-border .q-card.q-card--dark,
body[data-theme=mint].neon-border .q-date,
body[data-theme=mint].neon-border .q-date--dark {
border: 2px solid #3ab77d;
box-shadow: none;
}
body[data-theme=autumn].neon-border .q-card,
body[data-theme=autumn].neon-border .q-card.q-card--dark,
body[data-theme=autumn].neon-border .q-date,
body[data-theme=autumn].neon-border .q-date--dark {
border: 2px solid #b7763a;
box-shadow: none;
}
body[data-theme=flamingo].neon-border .q-card,
body[data-theme=flamingo].neon-border .q-card.q-card--dark,
body[data-theme=flamingo].neon-border .q-date,
body[data-theme=flamingo].neon-border .q-date--dark {
border: 2px solid #ff00ff;
box-shadow: none;
}
body[data-theme=monochrome].neon-border .q-card,
body[data-theme=monochrome].neon-border .q-card.q-card--dark,
body[data-theme=monochrome].neon-border .q-date,
body[data-theme=monochrome].neon-border .q-date--dark {
border: 2px solid #494949;
box-shadow: none;
}
body[data-theme=salvador].neon-border .q-card,
body[data-theme=salvador].neon-border .q-card.q-card--dark,
body[data-theme=salvador].neon-border .q-date,
body[data-theme=salvador].neon-border .q-date--dark {
border: 2px solid #1976d2;
box-shadow: none;
}
body.hard-border .q-card,
body.hard-border .q-card.q-card--dark,
body.hard-border .q-date,
body.hard-border .q-date--dark {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(255, 255, 255, 0.2784313725);
border: none;
}
body.retro-border .q-card,
body.retro-border .q-card.q-card--dark,
body.retro-border .q-date,
body.retro-border .q-date--dark {
border: none;
box-shadow: 0 1px 5px rgba(255, 255, 255, 0.2), 0 2px 2px rgba(255, 255, 255, 0.14), 0 3px 1px -2px rgba(255, 255, 255, 0.12);
}
body.no-border .q-card,
body.no-border .q-card.q-card--dark,
body.no-border .q-date,
body.no-border .q-date--dark {
border: none;
box-shadow: none;
}
body[data-theme=classic] {
--q-primary: #673ab7;
--q-secondary: #9c27b0;
--q-dark-page: #1f2234;
}
body[data-theme=classic] [data-theme=classic] .q-card--dark,
body[data-theme=classic] [data-theme=classic] .q-stepper--dark {
[data-theme=classic] .q-card--dark,
[data-theme=classic] .q-stepper--dark {
background: #333646 !important;
}
body[data-theme=bitcoin] {
--q-primary: #ea611d;
--q-secondary: #e56f35;
--q-dark-page: #2d293b;
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
body[data-theme=bitcoin] [data-theme=bitcoin] .q-card--dark,
body[data-theme=bitcoin] [data-theme=bitcoin] .q-stepper--dark {
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
[data-theme=classic] .bg-primary {
background: #673ab7 !important;
}
[data-theme=classic] .text-primary {
color: #673ab7 !important;
}
[data-theme=classic] .bg-secondary {
background: #9c27b0 !important;
}
[data-theme=classic] .text-secondary {
color: #9c27b0 !important;
}
[data-theme=classic] .bg-dark {
background: #1f2234 !important;
}
[data-theme=classic] .text-dark {
color: #1f2234 !important;
}
[data-theme=classic] .bg-info {
background: #333646 !important;
}
[data-theme=classic] .text-info {
color: #333646 !important;
}
[data-theme=classic] .bg-marginal-bg {
background: #1f2234 !important;
}
[data-theme=classic] .text-marginal-bg {
color: #1f2234 !important;
}
[data-theme=classic] .bg-marginal-text {
background: #fff !important;
}
[data-theme=classic] .text-marginal-text {
color: #fff !important;
}
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
[data-theme=bitcoin] .q-drawer--dark,
body[data-theme=bitcoin].body--dark,
[data-theme=bitcoin] .q-menu--dark {
background: #2d293b !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='bitcoin'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
[data-theme=bitcoin] .q-card--dark,
[data-theme=bitcoin] .q-stepper--dark {
background: #333646 !important;
}
body[data-theme=freedom] {
--q-primary: #e22156;
--q-secondary: #b91a45;
--q-dark-page: #462f36;
}
body[data-theme=freedom] [data-theme=freedom] .q-card--dark,
body[data-theme=freedom] [data-theme=freedom] .q-stepper--dark {
background: #47393d !important;
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
body[data-theme=cyber] {
--q-primary: #7cb342;
--q-secondary: #558b2f;
--q-dark-page: #000;
}
body[data-theme=cyber] [data-theme=cyber] .q-card--dark,
body[data-theme=cyber] [data-theme=cyber] .q-stepper--dark {
background: #1f2915 !important;
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
body[data-theme=mint] {
--q-primary: #3ab77d;
--q-secondary: #27b065;
--q-dark-page: #1f342b;
[data-theme=bitcoin] .bg-primary {
background: #ea611d !important;
}
body[data-theme=mint] [data-theme=mint] .q-card--dark,
body[data-theme=mint] [data-theme=mint] .q-stepper--dark {
[data-theme=bitcoin] .text-primary {
color: #ea611d !important;
}
[data-theme=bitcoin] .bg-secondary {
background: #e56f35 !important;
}
[data-theme=bitcoin] .text-secondary {
color: #e56f35 !important;
}
[data-theme=bitcoin] .bg-dark {
background: #2d293b !important;
}
[data-theme=bitcoin] .text-dark {
color: #2d293b !important;
}
[data-theme=bitcoin] .bg-info {
background: #333646 !important;
}
[data-theme=bitcoin] .text-info {
color: #333646 !important;
}
[data-theme=bitcoin] .bg-marginal-bg {
background: #000000 !important;
}
[data-theme=bitcoin] .text-marginal-bg {
color: #000000 !important;
}
[data-theme=bitcoin] .bg-marginal-text {
background: #fff !important;
}
[data-theme=bitcoin] .text-marginal-text {
color: #fff !important;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
[data-theme=freedom] .q-drawer--dark,
body[data-theme=freedom].body--dark,
[data-theme=freedom] .q-menu--dark {
background: #0a0a0a !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='freedom'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
[data-theme=freedom] .q-card--dark,
[data-theme=freedom] .q-stepper--dark {
background: #1b1b1b !important;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
[data-theme=freedom] .bg-primary {
background: #e22156 !important;
}
[data-theme=freedom] .text-primary {
color: #e22156 !important;
}
[data-theme=freedom] .bg-secondary {
background: #b91a45 !important;
}
[data-theme=freedom] .text-secondary {
color: #b91a45 !important;
}
[data-theme=freedom] .bg-dark {
background: #0a0a0a !important;
}
[data-theme=freedom] .text-dark {
color: #0a0a0a !important;
}
[data-theme=freedom] .bg-info {
background: #1b1b1b !important;
}
[data-theme=freedom] .text-info {
color: #1b1b1b !important;
}
[data-theme=freedom] .bg-marginal-bg {
background: #2d293b !important;
}
[data-theme=freedom] .text-marginal-bg {
color: #2d293b !important;
}
[data-theme=freedom] .bg-marginal-text {
background: #fff !important;
}
[data-theme=freedom] .text-marginal-text {
color: #fff !important;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
[data-theme=cyber] .q-drawer--dark,
body[data-theme=cyber].body--dark,
[data-theme=cyber] .q-menu--dark {
background: #0a0a0a !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='cyber'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
[data-theme=cyber] .q-card--dark,
[data-theme=cyber] .q-stepper--dark {
background: #1b1b1b !important;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
[data-theme=cyber] .bg-primary {
background: #7cb342 !important;
}
[data-theme=cyber] .text-primary {
color: #7cb342 !important;
}
[data-theme=cyber] .bg-secondary {
background: #558b2f !important;
}
[data-theme=cyber] .text-secondary {
color: #558b2f !important;
}
[data-theme=cyber] .bg-dark {
background: #0a0a0a !important;
}
[data-theme=cyber] .text-dark {
color: #0a0a0a !important;
}
[data-theme=cyber] .bg-info {
background: #1b1b1b !important;
}
[data-theme=cyber] .text-info {
color: #1b1b1b !important;
}
[data-theme=cyber] .bg-marginal-bg {
background: #2d293b !important;
}
[data-theme=cyber] .text-marginal-bg {
color: #2d293b !important;
}
[data-theme=cyber] .bg-marginal-text {
background: #fff !important;
}
[data-theme=cyber] .text-marginal-text {
color: #fff !important;
}
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
[data-theme=mint] .q-drawer--dark,
body[data-theme=mint].body--dark,
[data-theme=mint] .q-menu--dark {
background: #1f342b !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='mint'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
[data-theme=mint] .q-card--dark,
[data-theme=mint] .q-stepper--dark {
background: #334642 !important;
}
body[data-theme=autumn] {
--q-primary: #b7763a;
--q-secondary: #b07927;
--q-dark-page: #34291f;
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
body[data-theme=autumn] [data-theme=autumn] .q-card--dark,
body[data-theme=autumn] [data-theme=autumn] .q-stepper--dark {
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
[data-theme=mint] .bg-primary {
background: #3ab77d !important;
}
[data-theme=mint] .text-primary {
color: #3ab77d !important;
}
[data-theme=mint] .bg-secondary {
background: #27b065 !important;
}
[data-theme=mint] .text-secondary {
color: #27b065 !important;
}
[data-theme=mint] .bg-dark {
background: #1f342b !important;
}
[data-theme=mint] .text-dark {
color: #1f342b !important;
}
[data-theme=mint] .bg-info {
background: #334642 !important;
}
[data-theme=mint] .text-info {
color: #334642 !important;
}
[data-theme=mint] .bg-marginal-bg {
background: #1f342b !important;
}
[data-theme=mint] .text-marginal-bg {
color: #1f342b !important;
}
[data-theme=mint] .bg-marginal-text {
background: #fff !important;
}
[data-theme=mint] .text-marginal-text {
color: #fff !important;
}
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
[data-theme=autumn] .q-drawer--dark,
body[data-theme=autumn].body--dark,
[data-theme=autumn] .q-menu--dark {
background: #34291f !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='autumn'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
[data-theme=autumn] .q-card--dark,
[data-theme=autumn] .q-stepper--dark {
background: #463f33 !important;
}
body[data-theme=flamingo] {
--q-primary: #ff00ff;
--q-secondary: #fda3fd;
--q-dark-page: #2f032f;
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
body[data-theme=flamingo] [data-theme=flamingo] .q-card--dark,
body[data-theme=flamingo] [data-theme=flamingo] .q-stepper--dark {
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
[data-theme=autumn] .bg-primary {
background: #b7763a !important;
}
[data-theme=autumn] .text-primary {
color: #b7763a !important;
}
[data-theme=autumn] .bg-secondary {
background: #b07927 !important;
}
[data-theme=autumn] .text-secondary {
color: #b07927 !important;
}
[data-theme=autumn] .bg-dark {
background: #34291f !important;
}
[data-theme=autumn] .text-dark {
color: #34291f !important;
}
[data-theme=autumn] .bg-info {
background: #463f33 !important;
}
[data-theme=autumn] .text-info {
color: #463f33 !important;
}
[data-theme=autumn] .bg-marginal-bg {
background: #342a1f !important;
}
[data-theme=autumn] .text-marginal-bg {
color: #342a1f !important;
}
[data-theme=autumn] .bg-marginal-text {
background: rgb(255, 255, 255) !important;
}
[data-theme=autumn] .text-marginal-text {
color: rgb(255, 255, 255) !important;
}
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
[data-theme=flamingo] .q-drawer--dark,
body[data-theme=flamingo].body--dark,
[data-theme=flamingo] .q-menu--dark {
background: #2f032f !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='flamingo'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
[data-theme=flamingo] .q-card--dark,
[data-theme=flamingo] .q-stepper--dark {
background: #bc23bc !important;
}
body[data-theme=monochrome] {
--q-primary: #494949;
--q-secondary: #6b6b6b;
--q-dark-page: #000;
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
body[data-theme=monochrome] [data-theme=monochrome] .q-card--dark,
body[data-theme=monochrome] [data-theme=monochrome] .q-stepper--dark {
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
[data-theme=flamingo] .bg-primary {
background: #ff00ff !important;
}
[data-theme=flamingo] .text-primary {
color: #ff00ff !important;
}
[data-theme=flamingo] .bg-secondary {
background: #fda3fd !important;
}
[data-theme=flamingo] .text-secondary {
color: #fda3fd !important;
}
[data-theme=flamingo] .bg-dark {
background: #2f032f !important;
}
[data-theme=flamingo] .text-dark {
color: #2f032f !important;
}
[data-theme=flamingo] .bg-info {
background: #bc23bc !important;
}
[data-theme=flamingo] .text-info {
color: #bc23bc !important;
}
[data-theme=flamingo] .bg-marginal-bg {
background: #311231 !important;
}
[data-theme=flamingo] .text-marginal-bg {
color: #311231 !important;
}
[data-theme=flamingo] .bg-marginal-text {
background: rgb(255, 255, 255) !important;
}
[data-theme=flamingo] .text-marginal-text {
color: rgb(255, 255, 255) !important;
}
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
[data-theme=monochrome] .q-drawer--dark,
body[data-theme=monochrome].body--dark,
[data-theme=monochrome] .q-menu--dark {
background: #000 !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='monochrome'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
[data-theme=monochrome] .q-card--dark,
[data-theme=monochrome] .q-stepper--dark {
background: rgb(39, 39, 39) !important;
}
body[data-theme=salvador] {
--q-primary: #1976d2;
--q-secondary: #26a69a;
--q-dark-page: #253647;
}
body[data-theme=salvador] [data-theme=salvador] .q-card--dark,
body[data-theme=salvador] [data-theme=salvador] .q-stepper--dark {
background: #343d47 !important;
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
body.gradient-bg {
background-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
background-attachment: fixed;
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
body.bg-image::before {
content: "";
position: fixed;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 100%;
filter: blur(8px);
background-image: var(--background);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
[data-theme=monochrome] .bg-primary {
background: #494949 !important;
}
body.bg-image .q-page-container {
backdrop-filter: none; /* Ensure the page content is not affected */
[data-theme=monochrome] .text-primary {
color: #494949 !important;
}
[data-theme=monochrome] .bg-secondary {
background: #6b6b6b !important;
}
[data-theme=monochrome] .text-secondary {
color: #6b6b6b !important;
}
[data-theme=monochrome] .bg-dark {
background: #000 !important;
}
[data-theme=monochrome] .text-dark {
color: #000 !important;
}
[data-theme=monochrome] .bg-info {
background: rgb(39, 39, 39) !important;
}
[data-theme=monochrome] .text-info {
color: rgb(39, 39, 39) !important;
}
[data-theme=monochrome] .bg-marginal-bg {
background: #000 !important;
}
[data-theme=monochrome] .text-marginal-bg {
color: #000 !important;
}
[data-theme=monochrome] .bg-marginal-text {
background: rgb(255, 255, 255) !important;
}
[data-theme=monochrome] .text-marginal-text {
color: rgb(255, 255, 255) !important;
}
body.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark),
body.body--dark .q-header,
body.body--dark .q-drawer {
--q-dark: rgba(29, 29, 29, 0.3);
background-color: var(--q-dark);
backdrop-filter: blur(6px);
[data-theme=freedom] .q-drawer--dark {
background: #0a0a0a !important;
}
[data-theme=freedom] .q-header {
background: #0a0a0a !important;
}
[data-theme=cyber] .q-drawer--dark {
background: #0a0a0a !important;
}
[data-theme=cyber] .q-header {
background: #0a0a0a !important;
}
[data-theme=salvador] .q-drawer--dark {
background: #242424 !important;
}
[data-theme=salvador] .q-header {
background: #0f47af !important;
}
[v-cloak] {
+1 -1
View File
@@ -338,7 +338,7 @@ window.localisation.br = {
node_balance: 'Saldo do Nó: {balance} sats',
lnbits_balance: 'Saldo do LNbits: {balance} sats',
funding_reserve_percent: 'Reserve Percentual: {percent} %',
node_management: 'Gerenciamento de Nós',
node_managment: 'Gerenciamento de Nós',
node_management_not_supported:
'Gerenciamento de nó não suportado pela fonte de financiamento ativa',
toggle_node_ui: 'Interface do Nó',
+1 -1
View File
@@ -320,7 +320,7 @@ window.localisation.cn = {
node_balance: '节点余额:{balance} sats',
lnbits_balance: 'LNbits 余额:{balance} sats',
funding_reserve_percent: '保留百分比: {percent} %',
node_management: '节点管理',
node_managment: '节点管理',
node_management_not_supported: '活动资金来源不支持节点管理',
toggle_node_ui: '节点用户界面',
toggle_public_node_ui: '公共节点用户界面',
+1 -1
View File
@@ -332,7 +332,7 @@ window.localisation.cs = {
node_balance: 'Stav uzlu: {balance} sats',
lnbits_balance: 'Zůstatek LNbits: {balance} sats',
funding_reserve_percent: 'Rezervovat procento: {percent} %',
node_management: 'Správa uzlů',
node_managment: 'Správa uzlů',
node_management_not_supported:
'Správa uzlů není podporována aktivním zdrojem financování',
toggle_node_ui: 'Uživatelské rozhraní uzlu',
+1 -1
View File
@@ -342,7 +342,7 @@ window.localisation.de = {
node_balance: 'Kontostand: {balance} Sats',
lnbits_balance: 'LNbits-Guthaben: {balance} Sats',
funding_reserve_percent: 'Reservieren Prozent: {percent} %',
node_management: 'Knotenverwaltung',
node_managment: 'Knotenverwaltung',
node_management_not_supported:
'Knotenverwaltung wird von der aktiven Finanzierungsquelle nicht unterstützt',
toggle_node_ui: 'Node-Benutzeroberfläche',
+7 -65
View File
@@ -22,7 +22,6 @@ window.localisation.en = {
active_channels: 'Active Channels',
connect_peer: 'Connect Peer',
connect: 'Connect',
reconnect: 'Reconnect',
open_channel: 'Open Channel',
open: 'Open',
close_channel: 'Close Channel',
@@ -50,18 +49,14 @@ window.localisation.en = {
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
access_wallet_on_mobile: 'Mobile Access',
wallet: 'Wallet: ',
wallet_name: 'Wallet name',
wallets: 'Wallets',
add_wallet: 'Add wallet',
add_new_wallet: 'Add a new wallet',
pin_wallet: 'Pin wallet',
add_wallet: 'Add a new wallet',
delete_wallet: 'Delete wallet',
delete_wallet_desc:
'This whole wallet will be deleted, the funds will be UNRECOVERABLE.',
rename_wallet: 'Rename wallet',
update_name: 'Update name',
fiat_tracking: 'Fiat tracking',
fiat_providers: 'Fiat providers',
currency: 'Currency',
update_currency: 'Update currency',
press_to_claim: 'Press to claim bitcoin',
@@ -130,7 +125,6 @@ window.localisation.en = {
no_extensions: "You don't have any extensions installed :(",
created: 'Created',
search_extensions: 'Search extensions',
search_wallets: 'Search wallets',
extension_sources: 'Extension Sources',
ext_sources_hint: 'Repositories from where the extensions can be downloaded',
ext_sources_label:
@@ -143,7 +137,6 @@ window.localisation.en = {
uninstall: 'Uninstall',
drop_db: 'Remove Data',
enable: 'Enable',
enabled: 'Enabled',
pay_to_enable: 'Pay To Enable',
enable_extension_details: 'Enable extension for current user',
disable: 'Disable',
@@ -174,33 +167,12 @@ window.localisation.en = {
payment_hash: 'Payment Hash',
fee: 'Fee',
amount: 'Amount',
amount_limits: 'Amount Limits',
amount_sats: 'Amount (sats)',
faucest_wallet: 'Faucet Wallet',
faucest_wallet_desc_1:
'Each time a payment is confirmed by the {provider} provider funds will be subtracted from this wallet.',
faucest_wallet_desc_2:
'This helps monitor all {provider} payments and their status.',
faucest_wallet_desc_3:
'This wallet must be topped up with the amount of sats that the admin is willing to offer in exchange for the fiat currency.',
faucest_wallet_desc_4:
'If this wallet is configured, but is empty, the {provider} payments will not be processed.',
faucest_wallet_desc_5:
'This wallet can eventually get to a negative balance if parallel fiat payments are made.',
faucest_wallet_id: 'Faucet Wallet ID (optional)',
faucest_wallet_id_hint:
'Wallet ID to use for the faucet. It will be used to send the funds to the user.',
tag: 'Tag',
unit: 'Unit',
description: 'Description',
expiry: 'Expiry',
webhook: 'Webhook',
webhook_url: 'Webhook URL',
webhook_url_hint:
'Webhook URL to send the payment details to. It will be called when the payment is completed.',
webhook_events_list: 'The following events must be supported by the webhook:',
webhook_stripe_description:
'One the stripe side you must configure a webhook with a URL that points to your LNbits server.',
payment_proof: 'Payment Proof',
update: 'Update',
update_available: 'Update {version} available!',
@@ -292,7 +264,6 @@ window.localisation.en = {
notification_source_label:
'Source URL (only use the official LNbits status source, and sources you can trust)',
more: 'more',
more_count: '{count} more',
less: 'less',
releases: 'Releases',
watchdog: 'Watchdog',
@@ -307,7 +278,7 @@ window.localisation.en = {
callback_url_rules: 'Callback URL Rules',
enter_callback_url_rule: 'Enter URL rule as regex and hit enter',
callback_url_rule_hint:
'Callback URLs (like LNURL one) will be validated against these rules. At leat one rule must match. No rule means all URLs are allowed.',
'Callback URLs (like LNURL one) will be validated against all of these rules. No rule means all URLs are allowed.',
wallet_limiter: 'Wallet Limiter',
wallet_config: 'Wallet Config',
wallet_charts: 'Wallet Charts',
@@ -332,9 +303,6 @@ window.localisation.en = {
login_with_user_id: 'Login with user ID',
or: 'or',
create_new_wallet: 'Create New Wallet',
delete_all_wallets: 'Delete All Wallets',
confirm_delete_all_wallets:
'Are you sure you want to delete ALL wallets for this user?',
login_to_account: 'Login to your account',
create_account: 'Create account',
account_settings: 'Account Settings',
@@ -343,7 +311,7 @@ window.localisation.en = {
signin_with_nostr: 'Continue with Nostr',
signin_with_google: 'Sign in with Google',
signin_with_github: 'Sign in with GitHub',
signin_with_custom_org: 'Sign in with {custom_org}',
signin_with_keycloak: 'Sign in with Keycloak',
username_or_email: 'Username or Email',
password: 'Password',
password_config: 'Password Config',
@@ -352,19 +320,14 @@ window.localisation.en = {
change_password: 'Change Password',
update_credentials: 'Update Credentials',
update_pubkey: 'Update Public Key',
nostr_pubkey_tooltip: "Enter this user's Nostr public key (hex value)",
set_password: 'Set Password',
set_password_tooltip: 'Set a password for this user',
invalid_password: 'Password must have at least 8 characters',
invalid_password_repeat: 'Passwords do not match',
reset_key_generated: 'A reset key has been generated.',
reset_key_copy: 'Click OK to copy the reset URL to your clipboard.',
login: 'Login',
register: 'Register',
username: 'Username',
pubkey: 'Public Key',
user_id: 'User ID',
id: 'ID',
email: 'Email',
first_name: 'First Name',
last_name: 'Last Name',
@@ -374,15 +337,12 @@ window.localisation.en = {
update_account: 'Update Account',
invalid_username: 'Invalid Username',
auth_provider: 'Auth Provider',
external_id: 'External ID',
my_account: 'My Account',
existing_account_question: 'Already have an account?',
background_image: 'Background Image',
back: 'Back',
logout: 'Logout',
look_and_feel: 'Look and Feel',
endpoint: 'Endpoint',
api: 'API',
api_token: 'API Token',
api_tokens: 'API Tokens',
access_control_list: 'Access Control List',
@@ -394,14 +354,11 @@ window.localisation.en = {
gradient_background: 'Gradient Background',
language: 'Language',
color_scheme: 'Color Scheme',
visible_wallet_count: 'Visible Wallet Count',
admin_settings: 'Admin Settings',
extension_cost: 'This release requires a payment of minimum {cost} sats.',
extension_paid_sats: 'You have already paid {paid_sats} sats.',
release_details_error: 'Cannot get the release details.',
pay_from_wallet: 'Pay from Wallet',
pay_with: 'Pay with {provider}',
select_payment_provider: 'Select payment provider',
wallet_required: 'Wallet *',
show_qr: 'Show QR',
retry_install: 'Retry Install',
@@ -414,9 +371,6 @@ window.localisation.en = {
'The {name} extension requires a payment of minimum {amount} sats to enable.',
hide_empty_wallets: 'Hide empty wallets',
recheck: 'Recheck',
check: 'Check',
check_connection: 'Check Connection',
check_webhook: 'Check Webhook',
contributors: 'Contributors',
license: 'License',
reset_key: 'Reset Key',
@@ -478,7 +432,7 @@ window.localisation.en = {
node_balance: 'Node Balance: {balance} sats',
lnbits_balance: 'LNbits Balance: {balance} sats',
funding_reserve_percent: 'Reserve Percent: {percent} %',
node_management: 'Node Management',
node_managment: 'Node Management',
node_management_not_supported:
'Node Management not supported by active funding source',
toggle_node_ui: 'Node UI',
@@ -490,7 +444,7 @@ window.localisation.en = {
fee_reserve_percent: 'Fee Reserve Percent',
fee_reserve_msats: 'Reserve fee in msats',
reserve_fee_in_percent: 'Reserve fee in percent',
payment_wait_time: 'Payment Wait Time (sec)',
payment_wait_time: 'Payment Wait Time',
payment_wait_time_desc:
'How long to wait when making a payment before marking it as pending. Set higher values for HODL invoices, Boltz, etc.',
server_management: 'Server Management',
@@ -516,16 +470,12 @@ window.localisation.en = {
auth_keycloak_ci_hint:
'Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Keycloak Client Secret',
auth_keycloak_custom_org_label: 'Keycloak Custom Organization',
auth_keycloak_custom_icon_label: 'Keycloak Custom Icon (URL)',
currency_settings: 'Currency Settings',
allowed_currencies: 'Allowed Currencies',
allowed_currencies_hint: 'Limit the number of available fiat currencies',
default_account_currency: 'Default Account Currency',
default_account_currency_hint: 'Default currency for accounting',
min_incoming_payment_amount: 'Min Incoming Payment Amount',
min_incoming_payment_amount_desc:
'Minimum amount allowed for generating an invoice',
max_incoming_payment_amount: 'Max Incoming Payment Amount',
max_incoming_payment_amount_desc:
'Maximum amount allowed for generating an invoice',
@@ -587,7 +537,6 @@ window.localisation.en = {
admin_users_label: 'User ID',
allowed_users: 'Allowed Users',
allowed_users_hint: 'Only these users can use LNbits',
allowed_users_hint_feature: 'Only these users can use {feature}',
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',
@@ -622,12 +571,5 @@ window.localisation.en = {
view_column: 'View wallets as rows',
filter_payments: 'Filter payments',
filter_date: 'Filter by date',
websocket_example: 'Websocket example',
secret_key: 'Secret Key',
signing_secret: 'Signing Secret',
signing_secret_hint:
'Signing secret for the webhook. Messages will be signed with this secret.',
callback_success_url: 'Callback Success URL',
callback_success_url_hint:
'The user will be redirected to this URL after the payment is successful'
websocket_example: 'Websocket example'
}
+1 -1
View File
@@ -343,7 +343,7 @@ window.localisation.es = {
node_balance: 'Balance de Nodo: {balance} sats',
lnbits_balance: 'Saldo de LNbits: {balance} sats',
funding_reserve_percent: 'Reserve Porcentaje: {percent} %',
node_management: 'Gestión de nodos',
node_managment: 'Gestión de nodos',
node_management_not_supported:
'La gestión de nodos no es compatible con la fuente de financiación activa',
toggle_node_ui: 'Interfaz de usuario de nodo',
+3 -5
View File
@@ -100,7 +100,7 @@ window.localisation.fi = {
payment_processing: 'Maksua käsitellään...',
not_enough_funds: 'Varat eivät riitä!',
search_by_tag_memo_amount: 'Etsi tunnisteella, muistiolla tai määrällä',
invoice_waiting: 'Lasku odottaa maksua',
invoice_waiting: 'Lasku osottaa maksamista',
payment_received: 'Maksu vastaanotettu',
payment_sent: 'Maksu lähetetty',
payment_failed: 'Maksu epäonnistui',
@@ -261,7 +261,7 @@ window.localisation.fi = {
notification_source: 'Tiedotteiden lähde',
notification_source_label:
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
more: 'näytä lisää',
more: 'laajenna',
less: 'supista',
releases: 'Julkaisut',
watchdog: 'Watchdog',
@@ -321,8 +321,6 @@ window.localisation.fi = {
set_password: 'Aseta salasana',
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
invalid_password_repeat: 'Salasanat eivät täsmää',
reset_key_generated: 'Salasanan vaihtoavain on luotu.',
reset_key_copy: 'Kopioi vaihto-URL leikepöydälle painamalla OK.',
login: 'Kirjaudu',
register: 'Rekisteröidy',
username: 'Käyttäjänimi',
@@ -434,7 +432,7 @@ window.localisation.fi = {
node_balance: 'Solmun saldo: {balance} sats',
lnbits_balance: 'LNbits-saldo: {balance} sat',
funding_reserve_percent: 'Omavaraisuusaste: {percent} %',
node_management: 'Solmun hallinta',
node_managment: 'Solmun hallinta',
node_management_not_supported:
'Solmun hallinta ei ole mahdollista valitun rahoituslähteen kanssa.',
toggle_node_ui: 'Solmun käyttöliittymä',
+1 -1
View File
@@ -346,7 +346,7 @@ window.localisation.fr = {
node_balance: 'Solde du nœud : {balance} sats',
lnbits_balance: 'Solde LNbits : {balance} sats',
funding_reserve_percent: 'Pourcentage de Réserve : {percent} %',
node_management: 'Gestion des nœuds',
node_managment: 'Gestion des nœuds',
node_management_not_supported:
"La gestion des nœuds n'est pas prise en charge par la source de financement active",
toggle_node_ui: 'Interface utilisateur de nœud',
+1 -1
View File
@@ -343,7 +343,7 @@ window.localisation.it = {
node_balance: 'Saldo Nodo: {balance} sats',
lnbits_balance: 'Saldo LNbits: {balance} sats',
funding_reserve_percent: 'Riserva Percentuale: {percent} %',
node_management: 'Gestione dei nodi',
node_managment: 'Gestione dei nodi',
node_management_not_supported:
'La gestione dei nodi non è supportata dalla fonte di finanziamento attiva.',
toggle_node_ui: 'Interfaccia utente del nodo',
+1 -1
View File
@@ -334,7 +334,7 @@ window.localisation.jp = {
node_balance: 'ノード残高: {balance} サッツ',
lnbits_balance: 'LNbits残高: {balance} sats',
funding_reserve_percent: '予約パーセント: {percent} %',
node_management: 'ノード管理',
node_managment: 'ノード管理',
node_management_not_supported:
'アクティブな資金源ではノード管理がサポートされていません',
toggle_node_ui: 'ノードUI',
+1 -1
View File
@@ -331,7 +331,7 @@ window.localisation.kr = {
node_balance: '노드 잔액: {balance} 사토시',
lnbits_balance: 'LNbits 잔액: {balance} sats',
funding_reserve_percent: '예약 비율: {percent} %',
node_management: '노드 관리',
node_managment: '노드 관리',
node_management_not_supported:
'활성화된 자금 출처에 의해 노드 관리는 지원되지 않습니다.',
toggle_node_ui: '노드 UI',
+1 -1
View File
@@ -341,7 +341,7 @@ window.localisation.nl = {
node_balance: 'Node Balans: {balance} sats',
lnbits_balance: 'LNbits Saldo: {balance} sats',
funding_reserve_percent: 'Reservepercentage: {percent} %',
node_management: 'Nodebeheer',
node_managment: 'Nodebeheer',
node_management_not_supported:
'Nodebeheer wordt niet ondersteund door de actieve financieringsbron',
toggle_node_ui: 'Node UI',
+1 -1
View File
@@ -337,7 +337,7 @@ window.localisation.pi = {
node_balance: 'Node Balance: {balance} doubloons',
lnbits_balance: "LNbits Balance: {balance} pieces o' eight",
funding_reserve_percent: 'Reserve Percent: {percent} %',
node_management: 'Node Management',
node_managment: 'Node Management',
node_management_not_supported:
'Node Management not be supported by active funding source',
toggle_node_ui: 'Node Main Deck',
+1 -1
View File
@@ -337,7 +337,7 @@ window.localisation.pl = {
node_balance: 'Saldo węzła: {balance} sats',
lnbits_balance: 'Saldo LNbits: {balance} sats',
funding_reserve_percent: 'Rezerwa procentowa: {percent} %',
node_management: 'Zarządzanie węzłami',
node_managment: 'Zarządzanie węzłami',
node_management_not_supported:
'Zarządzanie węzłami nie jest obsługiwane przez aktywne źródło finansowania.',
toggle_node_ui: 'Interfejs użytkownika węzła',
+1 -1
View File
@@ -340,7 +340,7 @@ window.localisation.pt = {
node_balance: 'Saldo do Nó: {balance} sats',
lnbits_balance: 'Saldo do LNbits: {balance} sats',
funding_reserve_percent: 'Reserve Percentagem: {percent} %',
node_management: 'Gerenciamento de Nós',
node_managment: 'Gerenciamento de Nós',
node_management_not_supported:
'Gerenciamento de nós não suportado pela fonte de financiamento ativa',
toggle_node_ui: 'Interface do Usuário de Nó',
+1 -1
View File
@@ -335,7 +335,7 @@ window.localisation.sk = {
node_balance: 'Stav uzla: {balance} sats',
lnbits_balance: 'Zostatok LNbits: {balance} sats',
funding_reserve_percent: 'Rezervovať percento: {percent} %',
node_management: 'Správa uzlov',
node_managment: 'Správa uzlov',
node_management_not_supported:
'Správa uzlov nie je podporovaná aktívnym zdrojom financovania',
toggle_node_ui: 'Používateľské rozhranie uzla',
+1 -1
View File
@@ -335,7 +335,7 @@ window.localisation.we = {
node_balance: 'Cydbwysedd Nôd: {balance} sats',
lnbits_balance: 'Cydbwysedd LNbits: {balance} sats',
funding_reserve_percent: 'Cadw Canran: {percent} %',
node_management: 'Rheoli Nodau',
node_managment: 'Rheoli Nodau',
node_management_not_supported:
'Nid yw Rheoli Nodau yn cael ei gefnogi gan ffynhonnell ariannu weithredol',
toggle_node_ui: 'Node UI',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

+33
View File
@@ -91,6 +91,35 @@ window.AccountPageLogic = {
window.i18n.global.locale = newValue
this.$q.localStorage.set('lnbits.lang', newValue)
},
toggleGradient() {
this.gradientSelection = !this.gradientChoice
this.gradientChoice = this.gradientSelection
this.$q.localStorage.set('lnbits.backgroundImage', 'none')
this.applyGradient()
if (!this.gradientChoice) {
window.location.reload()
}
},
reactionChoiceFunc() {
this.$q.localStorage.set('lnbits.reactions', this.reactionSelection)
this.reactionChoice = this.reactionSelection
},
bgimageChoiceFunc() {
this.$q.localStorage.set('lnbits.backgroundImage', this.bgimageSelection)
this.bgimageChoice = this.bgimageSelection
this.applyBackgroundImage()
},
themeChoiceFunc(newValue) {
this.changeTheme(newValue)
this.setColors()
if (this.gradientChoice) {
this.applyGradient()
}
if (this.bgimageChoice) {
this.applyBackgroundImage()
}
this.applyBorder()
},
async updateAccount() {
try {
const {data} = await LNbits.api.request(
@@ -397,6 +426,10 @@ window.AccountPageLogic = {
}
},
async created() {
this.borderSelection = this.borderChoice
this.reactionSelection = this.reactionChoice
this.bgimageSelection = this.bgimageChoice
this.themeSelection = this.themeChoice
try {
const {data} = await LNbits.api.getAuthenticatedUser()
this.user = data
+3 -35
View File
@@ -54,7 +54,6 @@ window.AdminPageLogic = {
chartReady: false,
formAddAdmin: '',
formAddUser: '',
formAddStripeUser: '',
hideInputToggle: true,
formAddExtensionsManifest: '',
nostrNotificationIdentifier: '',
@@ -188,23 +187,6 @@ window.AdminPageLogic = {
let allowed_users = this.formData.lnbits_allowed_users
this.formData.lnbits_allowed_users = allowed_users.filter(u => u !== user)
},
addStripeAllowedUser() {
const addUser = this.formAddStripeUser || ''
if (
addUser.length &&
!this.formData.stripe_limits.allowed_users.includes(addUser)
) {
this.formData.stripe_limits.allowed_users = [
...this.formData.stripe_limits.allowed_users,
addUser
]
this.formAddStripeUser = ''
}
},
removeStripeAllowedUser(user) {
this.formData.stripe_limits.allowed_users =
this.formData.stripe_limits.allowed_users.filter(u => u !== user)
},
addIncludePath() {
if (!this.formAddIncludePath) {
return
@@ -468,7 +450,7 @@ window.AdminPageLogic = {
.catch(LNbits.utils.notifyApiError)
},
formatDate(date) {
return moment.utc(date * 1000).fromNow()
return moment(date * 1000).fromNow()
},
sendTestEmail() {
LNbits.api
@@ -564,10 +546,10 @@ window.AdminPageLogic = {
Quasar.Notify.create({
type: 'positive',
message:
'Success! Restored settings to defaults. Restarting...',
'Success! Restored settings to defaults, restart required!',
icon: null
})
this.$q.localStorage.clear()
this.needsRestart = true
})
.catch(LNbits.utils.notifyApiError)
})
@@ -631,20 +613,6 @@ window.AdminPageLogic = {
.catch(LNbits.utils.notifyApiError)
})
},
checkFiatProvider(providerName) {
LNbits.api
.request('PUT', `/api/v1/fiat/check/${providerName}`)
.then(response => {
response
const data = response.data
Quasar.Notify.create({
type: data.success ? 'positive' : 'warning',
message: data.message,
icon: null
})
})
.catch(LNbits.utils.notifyApiError)
},
downloadBackup() {
window.open('/admin/api/v1/backup', '_blank')
},
+156 -64
View File
@@ -19,16 +19,14 @@ window.LNbits = {
amount,
memo,
unit = 'sat',
lnurlCallback = null,
fiatProvider = null
lnurlCallback = null
) {
return this.request('post', '/api/v1/payments', wallet.inkey, {
out: false,
amount: amount,
memo: memo,
lnurl_callback: lnurlCallback,
unit: unit,
fiat_provider: fiatProvider
unit: unit
})
},
payInvoice(wallet, bolt11) {
@@ -199,9 +197,7 @@ window.LNbits = {
email: data.email,
extensions: data.extensions,
wallets: data.wallets,
fiat_providers: data.fiat_providers || [],
super_user: data.super_user,
extra: data.extra ?? {}
super_user: data.super_user
}
const mapWallet = this.wallet
obj.wallets = obj.wallets
@@ -209,9 +205,6 @@ window.LNbits = {
return mapWallet(obj)
})
.sort((a, b) => {
if (a.extra.pinned !== b.extra.pinned) {
return a.extra.pinned ? -1 : 1
}
return a.name.localeCompare(b.name)
})
obj.walletOptions = obj.wallets.map(obj => {
@@ -220,10 +213,6 @@ window.LNbits = {
value: obj.id
}
})
obj.hiddenWalletsCount = Math.max(
0,
data.wallets.length - data.extra.visible_wallet_count
)
return obj
},
wallet(data) {
@@ -263,11 +252,13 @@ window.LNbits = {
fiat_currency: data.fiat_currency
}
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
obj.dateFrom = moment.utc(data.created_at).fromNow()
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
obj.expirydateFrom = moment.utc(obj.expiry).fromNow()
obj.date = Quasar.date.formatDate(new Date(obj.time), window.dateFormat)
obj.dateFrom = moment(obj.date).fromNow()
obj.expirydate = Quasar.date.formatDate(
new Date(obj.expiry),
window.dateFormat
)
obj.expirydateFrom = moment(obj.expirydate).fromNow()
obj.msat = obj.amount
obj.sat = obj.msat / 1000
obj.tag = obj.extra?.tag
@@ -437,6 +428,18 @@ window.LNbits = {
converter.setFlavor('github')
converter.setOption('simpleLineBreaks', true)
return converter.makeHtml(text)
},
hexToRgb(hex) {
return Quasar.colors.hexToRgb(hex)
},
hexDarken(hex, percent) {
return Quasar.colors.lighten(hex, percent)
},
hexAlpha(hex, alpha) {
return Quasar.colors.changeAlpha(hex, alpha)
},
getPaletteColor(color) {
return Quasar.colors.getPaletteColor(color)
}
}
}
@@ -453,6 +456,7 @@ if (!window.g) {
fiatTracking: false,
wallets: [],
payments: [],
allowedThemes: null,
langs: [],
walletEventListeners: [],
updatePayments: false,
@@ -469,13 +473,12 @@ window.windowMixin = {
mobileSimple: true,
walletFlip: true,
showAddWalletDialog: {show: false},
isUserAuthorized: false,
isSatsDenomination: WINDOW_SETTINGS['LNBITS_DENOMINATION'] == 'sats',
allowedThemes: WINDOW_SETTINGS['LNBITS_THEME_OPTIONS'],
walletEventListeners: [],
darkChoice: this.$q.localStorage.has('lnbits.darkMode')
? this.$q.localStorage.getItem('lnbits.darkMode')
: true,
borderSelection: null,
gradientSelection: null,
themeSelection: null,
reactionSelection: null,
bgimageSelection: null,
gradientSelection: false,
borderChoice: this.$q.localStorage.has('lnbits.border')
? this.$q.localStorage.getItem('lnbits.border')
: USE_DEFAULT_BORDER,
@@ -491,7 +494,9 @@ window.windowMixin = {
bgimageChoice: this.$q.localStorage.has('lnbits.backgroundImage')
? this.$q.localStorage.getItem('lnbits.backgroundImage')
: USE_DEFAULT_BGIMAGE,
...WINDOW_SETTINGS
isUserAuthorized: false,
walletEventListeners: [],
backgroundImage: ''
}
},
@@ -503,11 +508,6 @@ window.windowMixin = {
}
this.$q.localStorage.set('lnbits.walletFlip', this.walletFlip)
},
goToWallets() {
this.$router.push({
path: '/wallets'
})
},
submitAddWallet() {
if (
this.showAddWalletDialog.name &&
@@ -596,53 +596,139 @@ window.windowMixin = {
},
changeTheme(newValue) {
document.body.setAttribute('data-theme', newValue)
this.$q.localStorage.set('lnbits.theme', newValue)
this.themeChoice = newValue
if (this.themeSelection) {
this.themeChoice = newValue
this.$q.localStorage.set('lnbits.theme', newValue)
}
this.setColors()
},
applyGradient() {
darkBgColor = this.$q.localStorage.getItem('lnbits.darkBgColor')
primaryColor = this.$q.localStorage.getItem('lnbits.primaryColor')
if (this.gradientChoice) {
document.body.classList.add('gradient-bg')
this.$q.localStorage.set('lnbits.gradientBg', true)
// Ensure dark mode is enabled when gradient background is applied
if (!this.$q.dark.isActive) {
this.toggleDarkMode()
this.$q.dark.toggle()
this.$q.localStorage.set('lnbits.darkMode', true)
}
const gradientStyle = `linear-gradient(to bottom right, ${LNbits.utils.hexDarken(String(primaryColor), -70)}, #0a0a0a)`
document.body.style.setProperty(
'background-image',
gradientStyle,
'important'
)
const gradientStyleCards = `background-color: ${LNbits.utils.hexAlpha(String(darkBgColor), 0.4)} !important; backdrop-filter: blur(6px);`
const style = document.createElement('style')
style.innerHTML =
`body[data-theme="${this.themeChoice}"] .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark), body.body${this.$q.dark.isActive ? '--dark' : ''} .q-header, body.body${this.$q.dark.isActive ? '--dark' : ''} .q-drawer { ${gradientStyleCards} }` +
`body[data-theme="${this.themeChoice}"].body--dark{background: ${LNbits.utils.hexDarken(String(primaryColor), -88)} !important; }` +
`[data-theme="${this.themeChoice}"] .q-card--dark{background: ${String(darkBgColor)} !important;} }`
document.head.appendChild(style)
} else {
document.body.classList.remove('gradient-bg')
this.$q.localStorage.set('lnbits.gradientBg', false)
}
},
applyBackgroundImage() {
if (this.bgimageChoice == '') {
document.body.classList.remove('bg-image')
} else {
document.body.classList.add('bg-image')
document.body.style.setProperty(
'--background',
`url(${this.bgimageChoice})`
)
}
this.$q.localStorage.set('lnbits.backgroundImage', this.bgimageChoice)
},
applyBorder() {
// Remove any existing border classes
document.body.classList.forEach(cls => {
if (cls.endsWith('-border')) {
document.body.classList.remove(cls)
}
})
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
document.body.classList.add(this.borderChoice)
},
toggleDarkMode() {
this.$q.dark.toggle()
this.darkChoice = this.$q.dark.isActive
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
if (!this.$q.dark.isActive) {
this.gradientChoice = false
this.applyGradient()
this.bgimageSelection = 'null'
this.$q.localStorage.set('lnbits.gradientBg', false)
this.$q.localStorage.set('lnbits.backgroundImage', 'null')
window.location.hash = '#theme'
window.location.reload()
}
},
applyBackgroundImage() {
if (this.bgimageSelection) {
this.$q.localStorage.set(
'lnbits.backgroundImage',
this.bgimageSelection
)
this.bgimageChoice = this.bgimageSelection
}
if (
this.bgimageChoice &&
this.bgimageChoice !== 'null' &&
this.bgimageChoice !== 'none' &&
this.bgimageChoice !== ''
) {
this.gradientChoice = true
this.applyGradient()
const style = document.createElement('style')
style.innerHTML = `
body[data-theme="${this.themeChoice}"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url(${this.bgimageChoice});
background-size: cover;
filter: blur(8px);
z-index: -1;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
body[data-theme="${this.themeChoice}"] .q-page-container {
backdrop-filter: none; /* Ensure the page content is not affected */
}`
document.head.appendChild(style)
}
},
applyBorder() {
if (this.borderSelection) {
this.$q.localStorage.setItem('lnbits.border', this.borderSelection)
this.borderChoice = this.$q.localStorage.getItem('lnbits.border')
}
let borderStyleCSS
if (this.borderChoice == 'hard-border') {
borderStyleCSS = `box-shadow: 0 0 0 1px rgba(0,0,0,.12), 0 0 0 1px #ffffff47; border: none;`
}
if (this.borderChoice == 'neon-border') {
borderStyleCSS = `border: 2px solid ${this.$q.localStorage.getItem('lnbits.primaryColor')}; box-shadow: none;`
}
if (this.borderChoice == 'no-border') {
borderStyleCSS = `box-shadow: none; border: none;`
}
if (this.borderChoice == 'retro-border') {
borderStyleCSS = `border: none; border-color: rgba(255, 255, 255, 0.28); box-shadow: 0 1px 5px rgba(255, 255, 255, 0.2), 0 2px 2px rgba(255, 255, 255, 0.14), 0 3px 1px -2px rgba(255, 255, 255, 0.12);`
}
let style = document.createElement('style')
style.innerHTML = `
body[data-theme="${this.themeChoice}"] .q-card,
body[data-theme="${this.themeChoice}"] .q-card.q-card--dark,
body[data-theme="${this.themeChoice}"] .q-date,
body[data-theme="${this.themeChoice}"] .q-date--dark {
${borderStyleCSS}
}
`
document.head.appendChild(style)
},
setColors() {
this.$q.localStorage.set(
'lnbits.primaryColor',
LNbits.utils.getPaletteColor('primary')
)
this.$q.localStorage.set(
'lnbits.secondaryColor',
LNbits.utils.getPaletteColor('secondary')
)
this.$q.localStorage.set(
'lnbits.darkBgColor',
LNbits.utils.getPaletteColor('dark')
)
document.documentElement.style.setProperty(
'--q-primary',
LNbits.utils.getPaletteColor('primary')
)
document.documentElement.style.setProperty(
'--q-secondary',
LNbits.utils.getPaletteColor('secondary')
)
},
copyText(text, message, position) {
Quasar.copyToClipboard(text).then(() => {
Quasar.Notify.create({
@@ -706,7 +792,10 @@ window.windowMixin = {
const gradient = params.get('gradient')
const border = params.get('border')
if (theme && this.allowedThemes.includes(theme.trim().toLowerCase())) {
if (
theme &&
this.g.allowedThemes.includes(theme.trim().toLowerCase())
) {
const normalizedTheme = theme.trim().toLowerCase()
document.body.setAttribute('data-theme', normalizedTheme)
this.$q.localStorage.set('lnbits.theme', normalizedTheme)
@@ -736,6 +825,8 @@ window.windowMixin = {
window.history.replaceState(null, null, url.pathname)
}
this.setColors()
},
refreshRoute() {
const path = window.location.pathname
@@ -747,6 +838,7 @@ window.windowMixin = {
}
},
async created() {
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
this.$q.dark.set(
this.$q.localStorage.has('lnbits.darkMode')
? this.$q.localStorage.getItem('lnbits.darkMode')
+10 -5
View File
@@ -103,7 +103,14 @@ window.app.component('lnbits-extension-list', {
window.app.component('lnbits-manage', {
mixins: [window.windowMixin],
template: '#lnbits-manage',
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers', 'showAudit'],
props: [
'showAdmin',
'showNode',
'showExtensions',
'showUsers',
'showAudit',
'showPayments'
],
methods: {
isActive(path) {
return window.location.pathname === path
@@ -203,7 +210,7 @@ window.app.component('lnbits-qrcode', {
data() {
return {
custom: {
margin: 3,
margin: 1,
width: 350,
size: 350,
logo: LNBITS_QR_LOGO
@@ -559,9 +566,7 @@ window.app.component('username-password', {
username: this.userName,
password: this.password_1,
passwordRepeat: this.password_2,
reset_key: this.resetKey,
keycloakOrg: LNBITS_AUTH_KEYCLOAK_ORG || 'Keycloak',
keycloakIcon: LNBITS_AUTH_KEYCLOAK_ICON
reset_key: this.resetKey
}
},
methods: {
@@ -8,10 +8,6 @@ window.app.component('lnbits-funding-sources', {
fundingSource => fundingSource[0] === item
)
return fundingSource ? fundingSource[1] : item
},
showQRValue(value) {
this.qrValue = value
this.showQRDialog = true
}
},
computed: {
@@ -21,8 +17,7 @@ window.app.component('lnbits-funding-sources', {
const tmpObj = {}
if (obj !== null) {
for (let [k, v] of Object.entries(obj)) {
tmpObj[k] =
typeof v === 'string' ? {label: v, value: null} : v || {}
tmpObj[k] = {label: v, value: null}
}
}
tmp.push([key, tmpObj])
@@ -36,8 +31,6 @@ window.app.component('lnbits-funding-sources', {
data() {
return {
hideInput: true,
showQRDialog: false,
qrValue: '',
rawFundingSources: [
['VoidWallet', 'Void Wallet', null],
[
@@ -145,14 +138,7 @@ window.app.component('lnbits-funding-sources', {
boltz_client_endpoint: 'Endpoint',
boltz_client_macaroon: 'Admin Macaroon path or hex',
boltz_client_cert: 'Certificate path or hex',
boltz_client_wallet: 'Wallet Name',
boltz_client_password: 'Wallet Password (can be empty)',
boltz_mnemonic: {
label: 'Liquid mnemonic (copy into greenwallet)',
readonly: true,
copy: true,
qrcode: true
}
boltz_client_wallet: 'Wallet Name'
}
],
[
@@ -211,14 +197,6 @@ window.app.component('lnbits-funding-sources', {
breez_greenlight_device_cert: 'Greenlight Device Cert',
breez_greenlight_invite_code: 'Greenlight Invite Code'
}
],
[
'StrikeWallet',
'Strike (alpha)',
{
strike_api_endpoint: 'API Endpoint',
strike_api_key: 'API Key'
}
]
]
}
+2 -2
View File
@@ -101,8 +101,8 @@ window.app = Vue.createApp({
}
},
created() {
this.description = this.SITE_DESCRIPTION
this.allowedRegister = this.LNBITS_NEW_ACCOUNTS_ALLOWED
this.description = SITE_DESCRIPTION
this.allowedRegister = ALLOWED_REGISTER
this.authAction =
!this.allowedRegister ||
Quasar.LocalStorage.getItem('lnbits.disclaimerShown')
-9
View File
@@ -193,15 +193,6 @@ const routes = [
scripts: ['/static/js/account.js']
}
},
{
path: '/wallets',
name: 'Wallets',
component: DynamicComponent,
props: {
fetchUrl: '/wallets',
scripts: ['/static/js/wallets.js']
}
},
{
path: '/node',
name: 'Node',
+1 -1
View File
@@ -600,7 +600,7 @@ window.app.component('lnbits-date', {
return LNbits.utils.formatDate(this.ts)
},
dateFrom() {
return moment.utc(this.date).fromNow()
return moment(this.date).fromNow()
}
},
template: `

Some files were not shown because too many files have changed in this diff Show More