Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86a1313258 | ||
|
|
0fa8fca70e | ||
|
|
b33518f075 | ||
|
|
3dce95968e | ||
|
|
7c92788cd8 | ||
|
|
2e31d1fd91 | ||
|
|
6c353f366d | ||
|
|
902f3c7c6f | ||
|
|
7fbffa20d7 | ||
|
|
0185f1e465 | ||
|
|
42b3616b03 | ||
|
|
a446cd6c5b | ||
|
|
3c0c1ca930 | ||
|
|
1a4c706ed4 | ||
|
|
8969c0235d | ||
|
|
34ee5b273a | ||
|
|
c7c1af9465 | ||
|
|
f7ed194e02 | ||
|
|
fd4b6af1de | ||
|
|
342bab5714 | ||
|
|
3d9a882066 | ||
|
|
85b2f30a1a | ||
|
|
07eb0ecdde | ||
|
|
ea0e1e032e | ||
|
|
b23e8ea781 | ||
|
|
fa61016ed1 | ||
|
|
6f44b755e4 | ||
|
|
5997fd74fa | ||
|
|
fb5659de2a | ||
|
|
e13fd39e75 | ||
|
|
acd3b598b7 | ||
|
|
1a6671b4b3 | ||
|
|
7fc723c2dd | ||
|
|
468a786f78 | ||
|
|
f74a7cbe1f | ||
|
|
ce9b63c54e | ||
|
|
3dd1c6992b | ||
|
|
1c49430ece | ||
|
|
b14b3bbaad | ||
|
|
020f6b6b1b | ||
|
|
0314453100 | ||
|
|
37dca56ccc |
+17
-28
@@ -1,17 +1,5 @@
|
|||||||
#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/
|
#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/
|
||||||
|
|
||||||
######################################
|
|
||||||
####### Auth Configurations ##########
|
|
||||||
######################################
|
|
||||||
# Secret Key: will default to the hash of the super user.
|
|
||||||
# !!!!! It is strongly recommended that you set your own strong random value !!!!
|
|
||||||
AUTH_SECRET_KEY=""
|
|
||||||
AUTH_TOKEN_EXPIRE_MINUTES=525600
|
|
||||||
# Possible authorization methods: user-id-only, username-password, nostr-auth-nip98, google-auth, github-auth, keycloak-auth
|
|
||||||
AUTH_ALLOWED_METHODS="user-id-only, username-password"
|
|
||||||
# Set this flag if HTTP is used for OAuth
|
|
||||||
# OAUTHLIB_INSECURE_TRANSPORT="1"
|
|
||||||
|
|
||||||
######################################
|
######################################
|
||||||
########### Admin Settings ###########
|
########### Admin Settings ###########
|
||||||
######################################
|
######################################
|
||||||
@@ -26,7 +14,7 @@ LNBITS_ADMIN_UI=true
|
|||||||
|
|
||||||
# Change theme
|
# Change theme
|
||||||
LNBITS_SITE_TITLE="LNbits"
|
LNBITS_SITE_TITLE="LNbits"
|
||||||
LNBITS_SITE_TAGLINE="Open Source Lightning Payments Platform"
|
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
|
||||||
LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack."
|
LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack."
|
||||||
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
|
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
|
||||||
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
|
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
|
||||||
@@ -40,7 +28,7 @@ PORT=5000
|
|||||||
######################################
|
######################################
|
||||||
|
|
||||||
# which fundingsources are allowed in the admin ui
|
# 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
|
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||||
@@ -101,16 +89,10 @@ ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN
|
|||||||
|
|
||||||
# BoltzWallet
|
# BoltzWallet
|
||||||
BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
|
BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
|
||||||
# HEXSTRING instead of path also possible
|
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroon" # or HEXSTRING
|
||||||
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert" # or HEXSTRING
|
||||||
# HEXSTRING instead of path also possible
|
|
||||||
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
|
||||||
BOLTZ_CLIENT_WALLET="lnbits"
|
BOLTZ_CLIENT_WALLET="lnbits"
|
||||||
|
|
||||||
# StrikeWallet
|
|
||||||
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
|
||||||
STRIKE_API_KEY=YOUR_STRIKE_API_KEY
|
|
||||||
|
|
||||||
# ZBDWallet
|
# ZBDWallet
|
||||||
ZBD_API_ENDPOINT=https://api.zebedee.io/v0/
|
ZBD_API_ENDPOINT=https://api.zebedee.io/v0/
|
||||||
ZBD_API_KEY=ZBD_ACCESS_TOKEN
|
ZBD_API_KEY=ZBD_ACCESS_TOKEN
|
||||||
@@ -151,8 +133,17 @@ BREEZ_GREENLIGHT_SEED=SEED
|
|||||||
BREEZ_GREENLIGHT_INVITE_CODE=CODE
|
BREEZ_GREENLIGHT_INVITE_CODE=CODE
|
||||||
BREEZ_GREENLIGHT_DEVICE_KEY="/path/to/breezsdk/device.pem" # or BASE64/HEXSTRING
|
BREEZ_GREENLIGHT_DEVICE_KEY="/path/to/breezsdk/device.pem" # or BASE64/HEXSTRING
|
||||||
BREEZ_GREENLIGHT_DEVICE_CERT="/path/to/breezsdk/device.crt" # or BASE64/HEXSTRING
|
BREEZ_GREENLIGHT_DEVICE_CERT="/path/to/breezsdk/device.crt" # or BASE64/HEXSTRING
|
||||||
# BREEZ_USE_TRAMPOLINE=true
|
|
||||||
|
|
||||||
|
######################################
|
||||||
|
####### Auth Configurations ##########
|
||||||
|
######################################
|
||||||
|
# Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value.
|
||||||
|
AUTH_SECRET_KEY=""
|
||||||
|
AUTH_TOKEN_EXPIRE_MINUTES=525600
|
||||||
|
# Possible authorization methods: user-id-only, username-password, nostr-auth-nip98, google-auth, github-auth, keycloak-auth
|
||||||
|
AUTH_ALLOWED_METHODS="user-id-only, username-password"
|
||||||
|
# Set this flag if HTTP is used for OAuth
|
||||||
|
# OAUTHLIB_INSECURE_TRANSPORT="1"
|
||||||
|
|
||||||
# Google OAuth Config
|
# Google OAuth Config
|
||||||
# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
|
# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
|
||||||
@@ -169,16 +160,14 @@ GITHUB_CLIENT_SECRET=""
|
|||||||
KEYCLOAK_CLIENT_ID=""
|
KEYCLOAK_CLIENT_ID=""
|
||||||
KEYCLOAK_CLIENT_SECRET=""
|
KEYCLOAK_CLIENT_SECRET=""
|
||||||
KEYCLOAK_DISCOVERY_URL=""
|
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
|
# 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
|
# 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
|
# Server security, rate limiting ips, blocked ips, allowed ips
|
||||||
LNBITS_RATE_LIMIT_NO="200"
|
LNBITS_RATE_LIMIT_NO="200"
|
||||||
@@ -194,7 +183,7 @@ LNBITS_ADMIN_USERS=""
|
|||||||
# SUPER_USER=""
|
# SUPER_USER=""
|
||||||
|
|
||||||
# Extensions only admin can access
|
# Extensions only admin can access
|
||||||
LNBITS_ADMIN_EXTENSIONS="ngrok, nostrclient"
|
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||||
# Extensions enabled by default when a user is created
|
# Extensions enabled by default when a user is created
|
||||||
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
|
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ inputs:
|
|||||||
description: "Python Version"
|
description: "Python Version"
|
||||||
required: true
|
required: true
|
||||||
default: "3.10"
|
default: "3.10"
|
||||||
|
poetry-version:
|
||||||
|
description: "Poetry Version"
|
||||||
|
default: "1.7.0"
|
||||||
node-version:
|
node-version:
|
||||||
description: "Node Version"
|
description: "Node Version"
|
||||||
default: "20.x"
|
default: "20.x"
|
||||||
@@ -24,8 +27,10 @@ runs:
|
|||||||
# cache poetry install via pip
|
# cache poetry install via pip
|
||||||
cache: "pip"
|
cache: "pip"
|
||||||
|
|
||||||
- name: Set up Poetry
|
- name: Set up Poetry ${{ inputs.poetry-version }}
|
||||||
uses: abatilo/actions-poetry@v2
|
uses: abatilo/actions-poetry@v2
|
||||||
|
with:
|
||||||
|
poetry-version: ${{ inputs.poetry-version }}
|
||||||
|
|
||||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
LAUNCH_DIR="$PWD"
|
|
||||||
|
|
||||||
# Define persistent storage for extracted LNbits
|
|
||||||
PERSISTENT_DIR="$HOME/.local/share/lnbits"
|
|
||||||
|
|
||||||
# Remove existing LNbits directory before extraction
|
|
||||||
if [ -d "$PERSISTENT_DIR" ]; then
|
|
||||||
echo "Removing existing LNbits directory..."
|
|
||||||
rm -rf "$PERSISTENT_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure the persistent directory exists
|
|
||||||
mkdir -p "$PERSISTENT_DIR"
|
|
||||||
|
|
||||||
# Extract LNbits from the AppImage if not already extracted
|
|
||||||
echo "Extracting LNbits to disk for better performance..."
|
|
||||||
cp -r "$APPDIR/usr/lnbits"/* "$PERSISTENT_DIR/"
|
|
||||||
chmod +x "$PERSISTENT_DIR/dist/lnbits"
|
|
||||||
|
|
||||||
# Check if the directory exists, and create it if it doesn't
|
|
||||||
if [ ! -d "$LAUNCH_DIR/lnbits/database" ]; then
|
|
||||||
mkdir -p "$LAUNCH_DIR/lnbits/database"
|
|
||||||
echo "Created database directory at $LAUNCH_DIR/lnbits/database"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "$LAUNCH_DIR/lnbits/extensions" ]; then
|
|
||||||
mkdir -p "$LAUNCH_DIR/lnbits/extensions"
|
|
||||||
echo "Created extensions directory at $LAUNCH_DIR/lnbits/extensions"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "$PERSISTENT_DIR"
|
|
||||||
|
|
||||||
# Export the directory as an environment variable for the app
|
|
||||||
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
|
|
||||||
|
|
||||||
# Define the LNbits URL
|
|
||||||
URL="http://0.0.0.0:5000"
|
|
||||||
|
|
||||||
"./dist/lnbits" "$@" &
|
|
||||||
LNBITS_PID=$!
|
|
||||||
|
|
||||||
# Wait for LNbits to be ready before showing the popup
|
|
||||||
sleep 3
|
|
||||||
CLOSED=false
|
|
||||||
|
|
||||||
# Function to stop LNbits gracefully
|
|
||||||
kill_lnbits() {
|
|
||||||
LN_PIDS=$(lsof -t -i:5000 2>/dev/null) # Capture all PIDs
|
|
||||||
if [ -n "$LN_PIDS" ]; then
|
|
||||||
echo "Stopping LNbits (PIDs: $LN_PIDS)..."
|
|
||||||
kill -2 $LN_PIDS # Send SIGINT to all processes on port 5000
|
|
||||||
CLOSED=true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Show a GUI with a clickable link to open the browser
|
|
||||||
if command -v zenity >/dev/null 2>&1; then
|
|
||||||
while [ "$CLOSED" = false ]; do
|
|
||||||
zenity --info --title="LNbits" --width=400 --text="<b>LNbits is running.</b>\n\n<a href='$URL'>$URL</a>\n\nClick 'Close Server' to stop LNbits." --ok-label="Close Server"
|
|
||||||
kill_lnbits
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
elif command -v yad >/dev/null 2>&1; then
|
|
||||||
while [ "$CLOSED" = false ]; do
|
|
||||||
yad --title="LNbits" --width=400 --text="<b>LNbits is running.</b>\n\n<a href='$URL'>$URL</a>\n\nClick 'Close Server' to stop LNbits." --button="Close Server":0
|
|
||||||
kill_lnbits
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "No GUI tool found. LNbits is running at $URL"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure the script doesn't hang after closing
|
|
||||||
if ps -p $LNBITS_PID >/dev/null 2>&1; then
|
|
||||||
wait $LNBITS_PID 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Name=LNbits
|
|
||||||
Exec=lnbits
|
|
||||||
Icon=lnbits
|
|
||||||
Type=Application
|
|
||||||
Categories=X-Bitcoin;X-LightningNetwork;X-Finance;Network;
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@@ -88,6 +88,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10"]
|
python-version: ["3.10"]
|
||||||
|
poetry-version: ["1.5.1"]
|
||||||
uses: ./.github/workflows/jmeter.yml
|
uses: ./.github/workflows/jmeter.yml
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
poetry-version: ${{ matrix.poetry-version }}
|
||||||
|
|||||||
@@ -51,14 +51,3 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/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
|
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
default: "3.10"
|
default: "3.10"
|
||||||
type: string
|
type: string
|
||||||
|
poetry-version:
|
||||||
|
description: "Poetry Version"
|
||||||
|
required: true
|
||||||
|
default: "1.5.1"
|
||||||
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
jmeter:
|
jmeter:
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
nix:
|
nix:
|
||||||
if: false # temporarly disable nix support until the `poetry2nix` issue is resolved
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
name: Build LNbits AppImage DMG
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build-linux-package:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Step 1: Checkout the repository
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Step 2: Install Dependencies
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
curl -sSL https://install.python-poetry.org | python3 -
|
|
||||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libfuse2
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
# Step 3: Clone LNbits Repository
|
|
||||||
- name: Clone LNbits
|
|
||||||
run: |
|
|
||||||
mv .github/packaging packaging
|
|
||||||
mkdir -p packaging/linux/AppDir/usr
|
|
||||||
git clone https://github.com/lnbits/lnbits.git packaging/linux/AppDir/usr/lnbits
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
# Step 4: Make the AppImage Asset
|
|
||||||
- name: Make Asset
|
|
||||||
run: |
|
|
||||||
cd packaging/linux/AppDir/usr/lnbits
|
|
||||||
poetry install
|
|
||||||
poetry run pip install pyinstaller
|
|
||||||
|
|
||||||
# Build the LNbits binary
|
|
||||||
poetry run pyinstaller --onefile --name lnbits --hidden-import=embit --collect-all embit --collect-all lnbits --collect-all sqlalchemy --collect-all aiosqlite --hidden-import=passlib.handlers.bcrypt $(poetry run which lnbits)
|
|
||||||
cd ../../../../..
|
|
||||||
chmod +x packaging/linux/AppDir/AppRun
|
|
||||||
chmod +x packaging/linux/AppDir/lnbits.desktop
|
|
||||||
chmod +x packaging/linux/AppDir/usr/lnbits/dist/lnbits
|
|
||||||
|
|
||||||
find packaging/linux/AppDir/usr/lnbits -mindepth 1 -maxdepth 1 \
|
|
||||||
! -name 'dist' \
|
|
||||||
! -name 'lnbits' \
|
|
||||||
-exec rm -rf {} +
|
|
||||||
|
|
||||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
|
||||||
chmod +x appimagetool-x86_64.AppImage
|
|
||||||
TAG_NAME=${{ github.event.release.tag_name }}
|
|
||||||
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
|
|
||||||
./appimagetool-x86_64.AppImage --updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" packaging/linux/AppDir "$APPIMAGE_NAME"
|
|
||||||
chmod +x "$APPIMAGE_NAME"
|
|
||||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
# Step 5: Upload Linux Release Asset
|
|
||||||
- name: Upload Linux Release Asset
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
with:
|
|
||||||
upload_url: ${{ github.event.release.upload_url }}
|
|
||||||
asset_path: ${{ env.APPIMAGE_NAME }}
|
|
||||||
asset_name: ${{ env.APPIMAGE_NAME }}
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -63,8 +63,6 @@ jobs:
|
|||||||
LNBITS_ENDPOINT: http://localhost:5001
|
LNBITS_ENDPOINT: http://localhost:5001
|
||||||
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
|
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
|
||||||
ECLAIR_URL: http://127.0.0.1:8082
|
ECLAIR_URL: http://127.0.0.1:8082
|
||||||
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
|
|
||||||
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
|
|
||||||
ECLAIR_PASS: lnbits
|
ECLAIR_PASS: lnbits
|
||||||
PYTHONUNBUFFERED: 1
|
PYTHONUNBUFFERED: 1
|
||||||
DEBUG: true
|
DEBUG: true
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: release-rc
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "*-rc[0-9]+"
|
- "*-rc[0-9]"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
+1
-4
@@ -7,7 +7,6 @@ __pycache__
|
|||||||
.mypy_cache
|
.mypy_cache
|
||||||
.vscode
|
.vscode
|
||||||
*-lock.json
|
*-lock.json
|
||||||
.python-version
|
|
||||||
|
|
||||||
*.egg
|
*.egg
|
||||||
*.egg-info
|
*.egg-info
|
||||||
@@ -40,8 +39,6 @@ lnbits/static/bundle-components.js
|
|||||||
lnbits/static/bundle.css
|
lnbits/static/bundle.css
|
||||||
lnbits/static/bundle.min.js.old
|
lnbits/static/bundle.min.js.old
|
||||||
lnbits/static/bundle.min.css.old
|
lnbits/static/bundle.min.css.old
|
||||||
lnbits/static/bundle-components.min.js.old
|
|
||||||
lnbits/upgrades
|
|
||||||
docker
|
docker
|
||||||
|
|
||||||
# Nix
|
# Nix
|
||||||
@@ -54,7 +51,7 @@ lnbits-backup.zip
|
|||||||
|
|
||||||
# Ignore extensions (post installable extension PR)
|
# Ignore extensions (post installable extension PR)
|
||||||
/lnbits/extensions
|
/lnbits/extensions
|
||||||
/lnbits/upgrades/
|
/upgrades/
|
||||||
|
|
||||||
# builded python package
|
# builded python package
|
||||||
dist
|
dist
|
||||||
|
|||||||
+3
-6
@@ -11,7 +11,6 @@ WORKDIR /app
|
|||||||
|
|
||||||
# Only copy the files required to install the dependencies
|
# Only copy the files required to install the dependencies
|
||||||
COPY pyproject.toml poetry.lock ./
|
COPY pyproject.toml poetry.lock ./
|
||||||
RUN touch README.md
|
|
||||||
|
|
||||||
RUN mkdir data
|
RUN mkdir data
|
||||||
|
|
||||||
@@ -20,8 +19,7 @@ ENV POETRY_NO_INTERACTION=1 \
|
|||||||
POETRY_VIRTUALENVS_CREATE=1 \
|
POETRY_VIRTUALENVS_CREATE=1 \
|
||||||
POETRY_CACHE_DIR=/tmp/poetry_cache
|
POETRY_CACHE_DIR=/tmp/poetry_cache
|
||||||
|
|
||||||
ARG POETRY_INSTALL_ARGS="--only main"
|
RUN poetry install --only main
|
||||||
RUN poetry install --no-root ${POETRY_INSTALL_ARGS}
|
|
||||||
|
|
||||||
FROM python:3.12-slim-bookworm
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
@@ -48,12 +46,11 @@ WORKDIR /app
|
|||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=builder /app/.venv .venv
|
COPY --from=builder /app/.venv .venv
|
||||||
|
|
||||||
ARG POETRY_INSTALL_ARGS="--only main"
|
RUN poetry install --only main
|
||||||
RUN poetry install ${POETRY_INSTALL_ARGS}
|
|
||||||
|
|
||||||
ENV LNBITS_PORT="5000"
|
ENV LNBITS_PORT="5000"
|
||||||
ENV LNBITS_HOST="0.0.0.0"
|
ENV LNBITS_HOST="0.0.0.0"
|
||||||
|
|
||||||
EXPOSE 5000
|
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"]
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
@@ -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='*'
|
|
||||||
@@ -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. |
|
|
||||||
+18
-68
@@ -6,44 +6,30 @@ nav_order: 2
|
|||||||
|
|
||||||
# Basic installation
|
# Basic installation
|
||||||
|
|
||||||
|
The following sections explain how to install LNbits using varions package managers: `poetry`, `nix`, `Docker` and `Fly.io`.
|
||||||
|
|
||||||
Note that by default LNbits uses SQLite as its database, which is simple and effective but you can configure it to use PostgreSQL instead which is also described in a section below.
|
Note that by default LNbits uses SQLite as its database, which is simple and effective but you can configure it to use PostgreSQL instead which is also described in a section below.
|
||||||
|
|
||||||
## Option 1: AppImage (LInux)
|
## Option 1 (recommended): Poetry
|
||||||
|
|
||||||
### AppImage (Linux)
|
|
||||||
|
|
||||||
Go to [releases](https://github.com/lnbits/lnbits/releases) and pull latest AppImage, or:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
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 will create a folder for db and extension files in the folder the AppImage runs from.
|
|
||||||
|
|
||||||
## Option 2: Poetry (recommended for developers)
|
|
||||||
|
|
||||||
It is recommended to use the latest version of Poetry. Make sure you have Python version `3.12` installed.
|
It is recommended to use the latest version of Poetry. Make sure you have Python version `3.12` installed.
|
||||||
|
|
||||||
### Install Python 3.12
|
### Install Python 3.12
|
||||||
|
|
||||||
## Option 2 (recommended): Poetry
|
|
||||||
|
|
||||||
It is recommended to use the latest version of Poetry. Make sure you have Python version 3.9 or higher installed.
|
|
||||||
|
|
||||||
### Verify Python version
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3.12
|
||||||
|
sudo apt-get install python3.12-dev # ensure correct headers needed for secp256k1
|
||||||
python3 --version
|
python3 --version
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Poetry
|
### Install Poetry
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# If path 'export PATH="$HOME/.local/bin:$PATH"' fails, use the path echoed by the install
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
curl -sSL https://install.python-poetry.org | python3 - && export PATH="$HOME/.local/bin:$PATH"
|
# Once the above poetry install is completed, use the installation path printed to terminal and replace in the following command
|
||||||
|
export PATH="/home/user/.local/bin:$PATH"
|
||||||
```
|
```
|
||||||
|
|
||||||
### install LNbits
|
### install LNbits
|
||||||
@@ -78,24 +64,11 @@ poetry run lnbits-cli --help
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd lnbits
|
cd lnbits
|
||||||
# Stop LNbits with `ctrl + x` or with service manager
|
# Stop LNbits with `ctrl + x`
|
||||||
# sudo systemctl stop lnbits
|
git pull
|
||||||
|
# Keep your poetry install up to date, this can be done with `poetry self update`
|
||||||
# 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
|
|
||||||
poetry install --only main
|
poetry install --only main
|
||||||
poetry run lnbits
|
# Start LNbits with `poetry run lnbits`
|
||||||
|
|
||||||
# use LNbits admin UI Extensions page function "Update All" do get extensions onto proper level
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Option 2: Install script (on Debian/Ubuntu)
|
## Option 2: Install script (on Debian/Ubuntu)
|
||||||
@@ -108,7 +81,7 @@ chmod +x lnbits.sh &&
|
|||||||
|
|
||||||
Now visit `0.0.0.0:5000` to make a super-user account.
|
Now visit `0.0.0.0:5000` to make a super-user account.
|
||||||
|
|
||||||
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `poetry run lnbits` (see previous option).
|
`export PATH="/home/$USER/.local/bin:$PATH"` then `./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `poetry run lnbits` (see previous option).
|
||||||
|
|
||||||
## Option 3: Nix
|
## Option 3: Nix
|
||||||
|
|
||||||
@@ -150,7 +123,7 @@ SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/ln
|
|||||||
|
|
||||||
## Option 4: Docker
|
## Option 4: Docker
|
||||||
|
|
||||||
Use latest version from Docker Hub.
|
use latest version from docker hub
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker pull lnbits/lnbits
|
docker pull lnbits/lnbits
|
||||||
@@ -159,15 +132,7 @@ mkdir data
|
|||||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
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.
|
build the image yourself
|
||||||
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.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/lnbits/lnbits.git
|
git clone https://github.com/lnbits/lnbits.git
|
||||||
@@ -178,12 +143,6 @@ mkdir data
|
|||||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
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
|
## 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.
|
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.
|
||||||
@@ -267,7 +226,7 @@ Problems installing? These commands have helped us install LNbits.
|
|||||||
sudo apt install pkg-config libffi-dev libpq-dev
|
sudo apt install pkg-config libffi-dev libpq-dev
|
||||||
|
|
||||||
# build essentials for debian/ubuntu
|
# build essentials for debian/ubuntu
|
||||||
sudo apt install python3.10-dev gcc build-essential
|
sudo apt install python3.9-dev gcc build-essential
|
||||||
|
|
||||||
# if the secp256k1 build fails:
|
# if the secp256k1 build fails:
|
||||||
# if you used poetry
|
# if you used poetry
|
||||||
@@ -591,12 +550,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.
|
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
|
|
||||||
|
|||||||
@@ -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:
|
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
|
### CoreLightning
|
||||||
|
|
||||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
|
- `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
|
- `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
|
### LNbits
|
||||||
|
|
||||||
|
|||||||
Generated
+3
-3
@@ -41,11 +41,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1735563628,
|
"lastModified": 1723938990,
|
||||||
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
|
"narHash": "sha256-9tUadhnZQbWIiYVXH8ncfGXGvkNq3Hag4RCBEMUk7MI=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
|
"rev": "c42fcfbdfeae23e68fc520f9182dde9f38ad1890",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -34,16 +34,6 @@
|
|||||||
protobuf = prev.protobuf.override { preferWheel = true; };
|
protobuf = prev.protobuf.override { preferWheel = true; };
|
||||||
ruff = prev.ruff.override { preferWheel = true; };
|
ruff = prev.ruff.override { preferWheel = true; };
|
||||||
wallycore = prev.wallycore.override { preferWheel = true; };
|
wallycore = prev.wallycore.override { preferWheel = true; };
|
||||||
tlv8 = prev.tlv8.overrideAttrs (old: {
|
|
||||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
|
|
||||||
prev.setuptools
|
|
||||||
];
|
|
||||||
});
|
|
||||||
pynostr = prev.pynostr.overrideAttrs (old: {
|
|
||||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
|
|
||||||
prev.setuptools-scm
|
|
||||||
];
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ if [ ! -d lnbits/data ]; then
|
|||||||
# Update package list and install prerequisites non-interactively
|
# Update package list and install prerequisites non-interactively
|
||||||
sudo apt update -y
|
sudo apt update -y
|
||||||
sudo apt install -y software-properties-common
|
sudo apt install -y software-properties-common
|
||||||
|
|
||||||
# Add the deadsnakes PPA repository non-interactively
|
# Add the deadsnakes PPA repository non-interactively
|
||||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||||
|
|
||||||
# Install Python 3.10 and distutils non-interactively
|
# Install Python 3.9 and distutils non-interactively
|
||||||
sudo apt install -y python3.10 python3.10-distutils
|
sudo apt install -y python3.9 python3.9-distutils
|
||||||
|
|
||||||
# Install Poetry
|
# 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
|
# Add Poetry to PATH for the current session
|
||||||
export PATH="/home/$USER/.local/bin:$PATH"
|
export PATH="/home/$USER/.local/bin:$PATH"
|
||||||
@@ -42,6 +42,9 @@ elif [ ! -d lnbits/wallets ]; then
|
|||||||
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
|
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Set path for running after install
|
||||||
|
export PATH="/home/$USER/.local/bin:$PATH"
|
||||||
|
|
||||||
# Install the dependencies using Poetry
|
# Install the dependencies using Poetry
|
||||||
poetry env use python3.9
|
poetry env use python3.9
|
||||||
poetry install --only main
|
poetry install --only main
|
||||||
@@ -51,4 +54,4 @@ export LNBITS_ADMIN_UI=true
|
|||||||
export HOST=0.0.0.0
|
export HOST=0.0.0.0
|
||||||
|
|
||||||
# Run LNbits
|
# Run LNbits
|
||||||
poetry run lnbits
|
poetry run lnbits
|
||||||
+8
-5
@@ -9,13 +9,16 @@ from .decorators import (
|
|||||||
from .exceptions import InvoiceError, PaymentError
|
from .exceptions import InvoiceError, PaymentError
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"InvoiceError",
|
# decorators
|
||||||
"PaymentError",
|
"require_admin_key",
|
||||||
|
"require_invoice_key",
|
||||||
"check_admin",
|
"check_admin",
|
||||||
"check_super_user",
|
"check_super_user",
|
||||||
"check_user_exists",
|
"check_user_exists",
|
||||||
"create_invoice",
|
# services
|
||||||
"pay_invoice",
|
"pay_invoice",
|
||||||
"require_admin_key",
|
"create_invoice",
|
||||||
"require_invoice_key",
|
# exceptions
|
||||||
|
"PaymentError",
|
||||||
|
"InvoiceError",
|
||||||
]
|
]
|
||||||
|
|||||||
+19
-39
@@ -24,17 +24,14 @@ from lnbits.core.crud import (
|
|||||||
)
|
)
|
||||||
from lnbits.core.crud.extensions import create_installed_extension
|
from lnbits.core.crud.extensions import create_installed_extension
|
||||||
from lnbits.core.helpers import migrate_extension_database
|
from lnbits.core.helpers import migrate_extension_database
|
||||||
from lnbits.core.models.notifications import NotificationType
|
|
||||||
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
||||||
from lnbits.core.services.notifications import enqueue_notification
|
from lnbits.core.tasks import ( # watchdog_task
|
||||||
from lnbits.core.tasks import (
|
|
||||||
audit_queue,
|
audit_queue,
|
||||||
collect_exchange_rates_data,
|
collect_exchange_rates_data,
|
||||||
|
killswitch_task,
|
||||||
purge_audit_data,
|
purge_audit_data,
|
||||||
run_by_the_minute_tasks,
|
|
||||||
wait_for_audit_data,
|
wait_for_audit_data,
|
||||||
wait_for_paid_invoices,
|
wait_for_paid_invoices,
|
||||||
wait_notification_messages,
|
|
||||||
)
|
)
|
||||||
from lnbits.exceptions import register_exception_handlers
|
from lnbits.exceptions import register_exception_handlers
|
||||||
from lnbits.helpers import version_parse
|
from lnbits.helpers import version_parse
|
||||||
@@ -65,6 +62,7 @@ from .middleware import (
|
|||||||
add_ip_block_middleware,
|
add_ip_block_middleware,
|
||||||
add_ratelimit_middleware,
|
add_ratelimit_middleware,
|
||||||
)
|
)
|
||||||
|
from .requestvars import g
|
||||||
from .tasks import (
|
from .tasks import (
|
||||||
check_pending_payments,
|
check_pending_payments,
|
||||||
internal_invoice_listener,
|
internal_invoice_listener,
|
||||||
@@ -80,7 +78,6 @@ async def startup(app: FastAPI):
|
|||||||
|
|
||||||
# setup admin settings
|
# setup admin settings
|
||||||
await check_admin_settings()
|
await check_admin_settings()
|
||||||
core_app_extra.register_new_ratelimiter()
|
|
||||||
await check_webpush_settings()
|
await check_webpush_settings()
|
||||||
|
|
||||||
# check extensions after restart
|
# check extensions after restart
|
||||||
@@ -105,24 +102,9 @@ async def startup(app: FastAPI):
|
|||||||
# initialize tasks
|
# initialize tasks
|
||||||
register_async_tasks()
|
register_async_tasks()
|
||||||
|
|
||||||
enqueue_notification(
|
|
||||||
NotificationType.server_start_stop,
|
|
||||||
{
|
|
||||||
"message": "LNbits server started.",
|
|
||||||
"up_time": settings.lnbits_server_up_time,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
logger.warning("LNbits shutting down...")
|
logger.warning("LNbits shutting down...")
|
||||||
enqueue_notification(
|
|
||||||
NotificationType.server_start_stop,
|
|
||||||
{
|
|
||||||
"message": "LNbits server shutting down...",
|
|
||||||
"up_time": settings.lnbits_server_up_time,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
settings.lnbits_running = False
|
settings.lnbits_running = False
|
||||||
|
|
||||||
# shutdown event
|
# shutdown event
|
||||||
@@ -162,13 +144,11 @@ def create_app() -> FastAPI:
|
|||||||
core_app_extra.register_new_ratelimiter = register_new_ratelimiter(app)
|
core_app_extra.register_new_ratelimiter = register_new_ratelimiter(app)
|
||||||
|
|
||||||
# register static files
|
# register static files
|
||||||
app.mount("/static", StaticFiles(directory=Path("lnbits", "static")), name="static")
|
static_path = Path("lnbits", "static")
|
||||||
Path(settings.lnbits_data_folder, "images").mkdir(parents=True, exist_ok=True)
|
static = StaticFiles(directory=static_path)
|
||||||
app.mount(
|
app.mount("/static", static, name="static")
|
||||||
"/library",
|
|
||||||
StaticFiles(directory=Path(settings.lnbits_data_folder, "images")),
|
g().base_url = f"http://{settings.host}:{settings.port}"
|
||||||
name="library",
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
||||||
@@ -285,14 +265,14 @@ async def build_all_installed_extensions_list(
|
|||||||
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
|
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
|
||||||
"""
|
"""
|
||||||
installed_extensions = await get_installed_extensions()
|
installed_extensions = await get_installed_extensions()
|
||||||
settings.lnbits_installed_extensions_ids = {e.id for e in installed_extensions}
|
settings.lnbits_all_extensions_ids = {e.id for e in installed_extensions}
|
||||||
|
|
||||||
for ext_dir in Path(settings.lnbits_extensions_path, "extensions").iterdir():
|
for ext_dir in Path(settings.lnbits_extensions_path, "extensions").iterdir():
|
||||||
try:
|
try:
|
||||||
if not ext_dir.is_dir():
|
if not ext_dir.is_dir():
|
||||||
continue
|
continue
|
||||||
ext_id = ext_dir.name
|
ext_id = ext_dir.name
|
||||||
if ext_id in settings.lnbits_installed_extensions_ids:
|
if ext_id in settings.lnbits_all_extensions_ids:
|
||||||
continue
|
continue
|
||||||
ext_info = InstallableExtension.from_ext_dir(ext_id)
|
ext_info = InstallableExtension.from_ext_dir(ext_id)
|
||||||
if not ext_info:
|
if not ext_info:
|
||||||
@@ -307,7 +287,7 @@ async def build_all_installed_extensions_list(
|
|||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
|
|
||||||
for ext_id in settings.lnbits_extensions_default_install:
|
for ext_id in settings.lnbits_extensions_default_install:
|
||||||
if ext_id in settings.lnbits_installed_extensions_ids:
|
if ext_id in settings.lnbits_all_extensions_ids:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
|
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
|
||||||
@@ -368,11 +348,6 @@ async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
|
|||||||
|
|
||||||
|
|
||||||
def register_custom_extensions_path():
|
def register_custom_extensions_path():
|
||||||
upgrades_dir = settings.lnbits_extensions_upgrade_path
|
|
||||||
shutil.rmtree(upgrades_dir, True)
|
|
||||||
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
|
|
||||||
sys.path.append(str(upgrades_dir))
|
|
||||||
|
|
||||||
if settings.has_default_extension_path:
|
if settings.has_default_extension_path:
|
||||||
return
|
return
|
||||||
default_ext_path = os.path.join("lnbits", "extensions")
|
default_ext_path = os.path.join("lnbits", "extensions")
|
||||||
@@ -391,6 +366,11 @@ def register_custom_extensions_path():
|
|||||||
Path(extensions_dir).mkdir(parents=True, exist_ok=True)
|
Path(extensions_dir).mkdir(parents=True, exist_ok=True)
|
||||||
sys.path.append(str(extensions_dir))
|
sys.path.append(str(extensions_dir))
|
||||||
|
|
||||||
|
upgrades_dir = Path(settings.lnbits_extensions_path, "upgrades")
|
||||||
|
shutil.rmtree(upgrades_dir, True)
|
||||||
|
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
sys.path.append(str(upgrades_dir))
|
||||||
|
|
||||||
|
|
||||||
def register_new_ext_routes(app: FastAPI) -> Callable:
|
def register_new_ext_routes(app: FastAPI) -> Callable:
|
||||||
# Returns a function that registers new routes for an extension.
|
# Returns a function that registers new routes for an extension.
|
||||||
@@ -464,8 +444,6 @@ async def check_and_register_extensions(app: FastAPI):
|
|||||||
def register_async_tasks():
|
def register_async_tasks():
|
||||||
|
|
||||||
create_permanent_task(wait_for_audit_data)
|
create_permanent_task(wait_for_audit_data)
|
||||||
create_permanent_task(wait_notification_messages)
|
|
||||||
|
|
||||||
create_permanent_task(check_pending_payments)
|
create_permanent_task(check_pending_payments)
|
||||||
create_permanent_task(invoice_listener)
|
create_permanent_task(invoice_listener)
|
||||||
create_permanent_task(internal_invoice_listener)
|
create_permanent_task(internal_invoice_listener)
|
||||||
@@ -476,7 +454,9 @@ def register_async_tasks():
|
|||||||
register_invoice_listener(invoice_queue, "core")
|
register_invoice_listener(invoice_queue, "core")
|
||||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||||
|
|
||||||
create_permanent_task(run_by_the_minute_tasks)
|
# TODO: implement watchdog properly
|
||||||
|
# create_permanent_task(watchdog_task)
|
||||||
|
create_permanent_task(killswitch_task)
|
||||||
create_permanent_task(purge_audit_data)
|
create_permanent_task(purge_audit_data)
|
||||||
create_permanent_task(collect_exchange_rates_data)
|
create_permanent_task(collect_exchange_rates_data)
|
||||||
|
|
||||||
|
|||||||
+15
-111
@@ -3,10 +3,8 @@ import importlib
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from getpass import getpass
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import httpx
|
import httpx
|
||||||
@@ -29,21 +27,19 @@ from lnbits.core.crud import (
|
|||||||
update_payment,
|
update_payment,
|
||||||
)
|
)
|
||||||
from lnbits.core.helpers import is_valid_url, migrate_databases
|
from lnbits.core.helpers import is_valid_url, migrate_databases
|
||||||
from lnbits.core.models import Account, Payment, PaymentState
|
from lnbits.core.models import Payment, PaymentState
|
||||||
from lnbits.core.models.extensions import (
|
from lnbits.core.models.extensions import (
|
||||||
CreateExtension,
|
CreateExtension,
|
||||||
ExtensionRelease,
|
ExtensionRelease,
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
)
|
)
|
||||||
from lnbits.core.services import check_admin_settings, create_user_account_no_ckeck
|
from lnbits.core.services import check_admin_settings
|
||||||
from lnbits.core.views.extension_api import (
|
from lnbits.core.views.extension_api import (
|
||||||
api_install_extension,
|
api_install_extension,
|
||||||
api_uninstall_extension,
|
api_uninstall_extension,
|
||||||
)
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import AESCipher
|
|
||||||
from lnbits.wallets.base import Wallet
|
from lnbits.wallets.base import Wallet
|
||||||
from lnbits.wallets.macaroon import load_macaroon
|
|
||||||
|
|
||||||
|
|
||||||
def coro(f):
|
def coro(f):
|
||||||
@@ -68,13 +64,6 @@ def db():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@lnbits_cli.group()
|
|
||||||
def users():
|
|
||||||
"""
|
|
||||||
Users related commands
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@lnbits_cli.group()
|
@lnbits_cli.group()
|
||||||
def extensions():
|
def extensions():
|
||||||
"""
|
"""
|
||||||
@@ -82,20 +71,6 @@ def extensions():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@lnbits_cli.group()
|
|
||||||
def encrypt():
|
|
||||||
"""
|
|
||||||
Encryption commands
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@lnbits_cli.group()
|
|
||||||
def decrypt():
|
|
||||||
"""
|
|
||||||
Decryption commands
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def get_super_user() -> Optional[str]:
|
def get_super_user() -> Optional[str]:
|
||||||
"""Get the superuser"""
|
"""Get the superuser"""
|
||||||
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
|
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
|
||||||
@@ -134,7 +109,7 @@ async def delete_settings():
|
|||||||
"""Deletes the settings"""
|
"""Deletes the settings"""
|
||||||
|
|
||||||
async with core_db.connect() as conn:
|
async with core_db.connect() as conn:
|
||||||
await conn.execute("DELETE from system_settings")
|
await conn.execute("DELETE from settings")
|
||||||
|
|
||||||
|
|
||||||
@db.command("migrate")
|
@db.command("migrate")
|
||||||
@@ -205,6 +180,17 @@ async def database_revert_payment(checking_id: str):
|
|||||||
click.echo(f"Payment '{checking_id}' marked as pending.")
|
click.echo(f"Payment '{checking_id}' marked as pending.")
|
||||||
|
|
||||||
|
|
||||||
|
@db.command("cleanup-accounts")
|
||||||
|
@click.argument("days", type=int, required=False)
|
||||||
|
@coro
|
||||||
|
async def database_cleanup_accounts(days: Optional[int] = None):
|
||||||
|
"""Delete all accounts that have no wallets"""
|
||||||
|
async with core_db.connect() as conn:
|
||||||
|
delta = days or settings.cleanup_wallets_days
|
||||||
|
delta = delta * 24 * 60 * 60
|
||||||
|
await delete_accounts_no_wallets(delta, conn)
|
||||||
|
|
||||||
|
|
||||||
@db.command("check-payments")
|
@db.command("check-payments")
|
||||||
@click.option("-d", "--days", help="Maximum age of payments in days.")
|
@click.option("-d", "--days", help="Maximum age of payments in days.")
|
||||||
@click.option("-l", "--limit", help="Maximum number of payments to be checked.")
|
@click.option("-l", "--limit", help="Maximum number of payments to be checked.")
|
||||||
@@ -285,32 +271,6 @@ async def check_invalid_payments(
|
|||||||
click.echo(" ".join([w, str(data[0]), str(data[1] / 1000).ljust(10)]))
|
click.echo(" ".join([w, str(data[0]), str(data[1] / 1000).ljust(10)]))
|
||||||
|
|
||||||
|
|
||||||
@users.command("new")
|
|
||||||
@click.option("-u", "--username", required=True, help="Username.")
|
|
||||||
@click.option("-p", "--password", required=True, help="Password.")
|
|
||||||
@coro
|
|
||||||
async def create_user(username: str, password: str):
|
|
||||||
"""Create a new user bypassing the system 'new_accounts_allowed' rules"""
|
|
||||||
account = Account(
|
|
||||||
id=uuid4().hex,
|
|
||||||
username=username,
|
|
||||||
)
|
|
||||||
account.hash_password(password)
|
|
||||||
user = await create_user_account_no_ckeck(account)
|
|
||||||
click.echo(f"User '{user.username}' created. Id: '{user.id}'")
|
|
||||||
|
|
||||||
|
|
||||||
@users.command("cleanup-accounts")
|
|
||||||
@click.argument("days", type=int, required=False)
|
|
||||||
@coro
|
|
||||||
async def database_cleanup_accounts(days: Optional[int] = None):
|
|
||||||
"""Delete all accounts that have no wallets"""
|
|
||||||
async with core_db.connect() as conn:
|
|
||||||
delta = days or settings.cleanup_wallets_days
|
|
||||||
delta = delta * 24 * 60 * 60
|
|
||||||
await delete_accounts_no_wallets(delta, conn)
|
|
||||||
|
|
||||||
|
|
||||||
@extensions.command("list")
|
@extensions.command("list")
|
||||||
@coro
|
@coro
|
||||||
async def extensions_list():
|
async def extensions_list():
|
||||||
@@ -379,7 +339,7 @@ async def extensions_update(
|
|||||||
if not await _can_run_operation(url):
|
if not await _can_run_operation(url):
|
||||||
return
|
return
|
||||||
|
|
||||||
upgrades_dir = settings.lnbits_extensions_upgrade_path
|
upgrades_dir = Path(settings.lnbits_extensions_path, "upgrades")
|
||||||
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
|
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
|
||||||
sys.path.append(str(upgrades_dir))
|
sys.path.append(str(upgrades_dir))
|
||||||
|
|
||||||
@@ -496,62 +456,6 @@ async def extensions_uninstall(
|
|||||||
return False, str(ex)
|
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():
|
def main():
|
||||||
"""main function"""
|
"""main function"""
|
||||||
lnbits_cli()
|
lnbits_cli()
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ from .views.admin_api import admin_router
|
|||||||
from .views.api import api_router
|
from .views.api import api_router
|
||||||
from .views.audit_api import audit_router
|
from .views.audit_api import audit_router
|
||||||
from .views.auth_api import auth_router
|
from .views.auth_api import auth_router
|
||||||
from .views.callback_api import callback_router
|
|
||||||
from .views.extension_api import extension_router
|
from .views.extension_api import extension_router
|
||||||
from .views.fiat_api import fiat_router
|
|
||||||
|
|
||||||
# this compat is needed for usermanager extension
|
# this compat is needed for usermanager extension
|
||||||
from .views.generic import generic_router
|
from .views.generic import generic_router
|
||||||
@@ -36,12 +34,10 @@ def init_core_routers(app: FastAPI):
|
|||||||
app.include_router(wallet_router)
|
app.include_router(wallet_router)
|
||||||
app.include_router(api_router)
|
app.include_router(api_router)
|
||||||
app.include_router(websocket_router)
|
app.include_router(websocket_router)
|
||||||
app.include_router(callback_router)
|
|
||||||
app.include_router(tinyurl_router)
|
app.include_router(tinyurl_router)
|
||||||
app.include_router(webpush_router)
|
app.include_router(webpush_router)
|
||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
app.include_router(audit_router)
|
app.include_router(audit_router)
|
||||||
app.include_router(fiat_router)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["core_app", "core_app_extra", "db"]
|
__all__ = ["core_app", "core_app_extra", "db"]
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ from .settings import (
|
|||||||
delete_admin_settings,
|
delete_admin_settings,
|
||||||
get_admin_settings,
|
get_admin_settings,
|
||||||
get_super_settings,
|
get_super_settings,
|
||||||
reset_core_settings,
|
|
||||||
update_admin_settings,
|
update_admin_settings,
|
||||||
update_super_user,
|
update_super_user,
|
||||||
)
|
)
|
||||||
@@ -59,7 +58,6 @@ from .users import (
|
|||||||
get_account_by_username_or_email,
|
get_account_by_username_or_email,
|
||||||
get_accounts,
|
get_accounts,
|
||||||
get_user,
|
get_user,
|
||||||
get_user_access_control_lists,
|
|
||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
update_account,
|
update_account,
|
||||||
)
|
)
|
||||||
@@ -85,78 +83,85 @@ from .webpush import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DateTrunc",
|
# audit
|
||||||
"check_internal",
|
|
||||||
"create_account",
|
|
||||||
"create_admin_settings",
|
|
||||||
"create_audit_entry",
|
"create_audit_entry",
|
||||||
"create_installed_extension",
|
# db_versions
|
||||||
"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",
|
|
||||||
"get_db_version",
|
"get_db_version",
|
||||||
"get_db_versions",
|
"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_extension",
|
||||||
"get_installed_extensions",
|
"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_latest_payments_by_extension",
|
||||||
"get_payment",
|
"get_payment",
|
||||||
"get_payments",
|
"get_payments",
|
||||||
"get_payments_history",
|
"get_payments_history",
|
||||||
"get_payments_paginated",
|
"get_payments_paginated",
|
||||||
"get_standalone_payment",
|
"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_wallet_payment",
|
||||||
"get_wallets",
|
|
||||||
"get_webpush_subscription",
|
|
||||||
"get_webpush_subscriptions_for_user",
|
|
||||||
"is_internal_status_success",
|
"is_internal_status_success",
|
||||||
"mark_webhook_sent",
|
"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",
|
||||||
"update_payment_checking_id",
|
"update_payment_checking_id",
|
||||||
"update_payment_extra",
|
"update_payment_extra",
|
||||||
|
# settings
|
||||||
|
"create_admin_settings",
|
||||||
|
"delete_admin_settings",
|
||||||
|
"get_admin_settings",
|
||||||
|
"get_super_settings",
|
||||||
|
"update_admin_settings",
|
||||||
"update_super_user",
|
"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",
|
||||||
|
"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",
|
"update_wallet",
|
||||||
|
# webpush
|
||||||
|
"create_webpush_subscription",
|
||||||
|
"delete_webpush_subscription",
|
||||||
|
"delete_webpush_subscriptions",
|
||||||
|
"get_webpush_subscription",
|
||||||
|
"get_webpush_subscriptions_for_user",
|
||||||
]
|
]
|
||||||
|
|||||||
+22
-179
@@ -1,23 +1,25 @@
|
|||||||
from time import time
|
from time import time
|
||||||
from typing import Any, Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
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.db import db
|
||||||
from lnbits.core.models import PaymentState
|
from lnbits.core.models import PaymentState
|
||||||
from lnbits.db import Connection, DateTrunc, Filters, Page
|
from lnbits.db import DB_TYPE, SQLITE, Connection, Filters, Page
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
CreatePayment,
|
CreatePayment,
|
||||||
Payment,
|
Payment,
|
||||||
PaymentCountField,
|
|
||||||
PaymentCountStat,
|
|
||||||
PaymentDailyStats,
|
|
||||||
PaymentFilters,
|
PaymentFilters,
|
||||||
PaymentHistoryPoint,
|
PaymentHistoryPoint,
|
||||||
PaymentsStatusCount,
|
|
||||||
PaymentWalletStats,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DateTrunc = Literal["hour", "day", "month"]
|
||||||
|
sqlite_formats = {
|
||||||
|
"hour": "%Y-%m-%d %H:00:00",
|
||||||
|
"day": "%Y-%m-%d 00:00:00",
|
||||||
|
"month": "%Y-%m-01 00:00:00",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def update_payment_extra():
|
def update_payment_extra():
|
||||||
pass
|
pass
|
||||||
@@ -85,7 +87,7 @@ async def get_latest_payments_by_extension(
|
|||||||
WHERE status = '{PaymentState.SUCCESS}'
|
WHERE status = '{PaymentState.SUCCESS}'
|
||||||
AND extra LIKE :ext_name
|
AND extra LIKE :ext_name
|
||||||
AND extra LIKE :ext_id
|
AND extra LIKE :ext_id
|
||||||
ORDER BY time DESC LIMIT {int(limit)}
|
ORDER BY time DESC LIMIT {limit}
|
||||||
""",
|
""",
|
||||||
{"ext_name": f"%{ext_name}%", "ext_id": f"%{ext_id}%"},
|
{"ext_name": f"%{ext_name}%", "ext_id": f"%{ext_id}%"},
|
||||||
Payment,
|
Payment,
|
||||||
@@ -95,10 +97,8 @@ async def get_latest_payments_by_extension(
|
|||||||
async def get_payments_paginated(
|
async def get_payments_paginated(
|
||||||
*,
|
*,
|
||||||
wallet_id: Optional[str] = None,
|
wallet_id: Optional[str] = None,
|
||||||
user_id: Optional[str] = None,
|
|
||||||
complete: bool = False,
|
complete: bool = False,
|
||||||
pending: bool = False,
|
pending: bool = False,
|
||||||
failed: bool = False,
|
|
||||||
outgoing: bool = False,
|
outgoing: bool = False,
|
||||||
incoming: bool = False,
|
incoming: bool = False,
|
||||||
since: Optional[int] = None,
|
since: Optional[int] = None,
|
||||||
@@ -107,11 +107,11 @@ async def get_payments_paginated(
|
|||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Page[Payment]:
|
) -> Page[Payment]:
|
||||||
"""
|
"""
|
||||||
Filters payments to be returned by:
|
Filters payments to be returned by complete | pending | outgoing | incoming.
|
||||||
- complete | pending | failed | outgoing | incoming.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
values: dict[str, Any] = {
|
values: dict = {
|
||||||
|
"wallet_id": wallet_id,
|
||||||
"time": since,
|
"time": since,
|
||||||
}
|
}
|
||||||
clause: list[str] = []
|
clause: list[str] = []
|
||||||
@@ -120,11 +120,7 @@ async def get_payments_paginated(
|
|||||||
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
||||||
|
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
values["wallet_id"] = wallet_id
|
|
||||||
clause.append("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:
|
if complete and pending:
|
||||||
clause.append(
|
clause.append(
|
||||||
@@ -141,8 +137,6 @@ async def get_payments_paginated(
|
|||||||
)
|
)
|
||||||
elif pending:
|
elif pending:
|
||||||
clause.append(f"status = '{PaymentState.PENDING}'")
|
clause.append(f"status = '{PaymentState.PENDING}'")
|
||||||
elif failed:
|
|
||||||
clause.append(f"status = '{PaymentState.FAILED}'")
|
|
||||||
|
|
||||||
if outgoing and incoming:
|
if outgoing and incoming:
|
||||||
pass
|
pass
|
||||||
@@ -206,21 +200,6 @@ async def get_payments(
|
|||||||
return page.data
|
return page.data
|
||||||
|
|
||||||
|
|
||||||
async def get_payments_status_count() -> PaymentsStatusCount:
|
|
||||||
empty_page: Filters = Filters(limit=0)
|
|
||||||
in_payments = await get_payments_paginated(incoming=True, filters=empty_page)
|
|
||||||
out_payments = await get_payments_paginated(outgoing=True, filters=empty_page)
|
|
||||||
pending_payments = await get_payments_paginated(pending=True, filters=empty_page)
|
|
||||||
failed_payments = await get_payments_paginated(failed=True, filters=empty_page)
|
|
||||||
|
|
||||||
return PaymentsStatusCount(
|
|
||||||
incoming=in_payments.total,
|
|
||||||
outgoing=out_payments.total,
|
|
||||||
pending=pending_payments.total,
|
|
||||||
failed=failed_payments.total,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_expired_invoices(
|
async def delete_expired_invoices(
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -255,7 +234,6 @@ async def create_payment(
|
|||||||
# note: this can be removed if the db uniqueness constraints are set appropriately
|
# note: this can be removed if the db uniqueness constraints are set appropriately
|
||||||
previous_payment = await get_standalone_payment(checking_id, conn=conn)
|
previous_payment = await get_standalone_payment(checking_id, conn=conn)
|
||||||
assert previous_payment is None, "Payment already exists"
|
assert previous_payment is None, "Payment already exists"
|
||||||
extra = data.extra or {}
|
|
||||||
|
|
||||||
payment = Payment(
|
payment = Payment(
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
@@ -269,8 +247,7 @@ async def create_payment(
|
|||||||
expiry=data.expiry,
|
expiry=data.expiry,
|
||||||
webhook=data.webhook,
|
webhook=data.webhook,
|
||||||
fee=data.fee,
|
fee=data.fee,
|
||||||
tag=extra.get("tag", None),
|
extra=data.extra or {},
|
||||||
extra=extra,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await (conn or db).insert("apipayments", payment)
|
await (conn or db).insert("apipayments", payment)
|
||||||
@@ -307,7 +284,12 @@ async def get_payments_history(
|
|||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|
||||||
date_trunc = db.datetime_grouping(group)
|
if DB_TYPE == SQLITE and group in sqlite_formats:
|
||||||
|
date_trunc = f"strftime('{sqlite_formats[group]}', time, 'unixepoch')"
|
||||||
|
elif group in ("day", "hour", "month"):
|
||||||
|
date_trunc = f"date_trunc('{group}', time)"
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid group value: {group}")
|
||||||
|
|
||||||
values = {
|
values = {
|
||||||
"wallet_id": wallet_id,
|
"wallet_id": wallet_id,
|
||||||
@@ -359,134 +341,6 @@ async def get_payments_history(
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
data = await (conn or db).fetchall(
|
|
||||||
query=f"""
|
|
||||||
SELECT {field} as field, count(*) as total
|
|
||||||
FROM apipayments
|
|
||||||
{clause}
|
|
||||||
GROUP BY {field}
|
|
||||||
ORDER BY {field}
|
|
||||||
""",
|
|
||||||
values=filters.values(),
|
|
||||||
model=PaymentCountStat,
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def get_daily_stats(
|
|
||||||
filters: Optional[Filters[PaymentFilters]] = None,
|
|
||||||
user_id: Optional[str] = None,
|
|
||||||
conn: Optional[Connection] = None,
|
|
||||||
) -> 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)
|
|
||||||
|
|
||||||
date_trunc = db.datetime_grouping("day")
|
|
||||||
query = """
|
|
||||||
SELECT {date_trunc} date,
|
|
||||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance,
|
|
||||||
ABS(SUM(apipayments.fee)) as fee,
|
|
||||||
COUNT(*) as payments_count
|
|
||||||
FROM wallets
|
|
||||||
LEFT JOIN apipayments ON apipayments.wallet_id = wallets.id
|
|
||||||
{clause}
|
|
||||||
AND (wallets.deleted = false OR wallets.deleted is NULL)
|
|
||||||
GROUP BY date
|
|
||||||
ORDER BY date ASC
|
|
||||||
"""
|
|
||||||
|
|
||||||
data_in = await (conn or db).fetchall(
|
|
||||||
query=query.format(date_trunc=date_trunc, clause=in_clause),
|
|
||||||
values=filters.values(),
|
|
||||||
model=PaymentDailyStats,
|
|
||||||
)
|
|
||||||
data_out = await (conn or db).fetchall(
|
|
||||||
query=query.format(date_trunc=date_trunc, clause=out_clause),
|
|
||||||
values=filters.values(),
|
|
||||||
model=PaymentDailyStats,
|
|
||||||
)
|
|
||||||
|
|
||||||
return data_in, data_out
|
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_stats(
|
|
||||||
filters: Optional[Filters[PaymentFilters]] = None,
|
|
||||||
user_id: Optional[str] = None,
|
|
||||||
conn: Optional[Connection] = None,
|
|
||||||
) -> list[PaymentWalletStats]:
|
|
||||||
|
|
||||||
if not filters:
|
|
||||||
filters = Filters()
|
|
||||||
|
|
||||||
where_stmts = [
|
|
||||||
"(wallets.deleted = false OR wallets.deleted is NULL)",
|
|
||||||
"""
|
|
||||||
(
|
|
||||||
(apipayments.status = 'success' AND apipayments.amount > 0)
|
|
||||||
OR (
|
|
||||||
apipayments.status IN ('success', 'pending')
|
|
||||||
AND apipayments.amount < 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
]
|
|
||||||
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(
|
|
||||||
query=f"""
|
|
||||||
SELECT apipayments.wallet_id,
|
|
||||||
MAX(wallets.name) AS wallet_name,
|
|
||||||
MAX(wallets.user) AS user_id,
|
|
||||||
COUNT(*) as payments_count,
|
|
||||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
|
||||||
FROM wallets
|
|
||||||
LEFT JOIN apipayments ON apipayments.wallet_id = wallets.id
|
|
||||||
{clauses}
|
|
||||||
GROUP BY apipayments.wallet_id
|
|
||||||
ORDER BY payments_count
|
|
||||||
""",
|
|
||||||
values=filters.values(),
|
|
||||||
model=PaymentWalletStats,
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_wallet_payment(
|
async def delete_wallet_payment(
|
||||||
checking_id: str, wallet_id: str, conn: Optional[Connection] = None
|
checking_id: str, wallet_id: str, conn: Optional[Connection] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -532,7 +386,7 @@ async def is_internal_status_success(
|
|||||||
return payment.status == PaymentState.SUCCESS.value
|
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(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE apipayments SET webhook_status = :status
|
UPDATE apipayments SET webhook_status = :status
|
||||||
@@ -540,14 +394,3 @@ async def mark_webhook_sent(payment_hash: str, status: str) -> None:
|
|||||||
""",
|
""",
|
||||||
{"status": status, "hash": payment_hash},
|
{"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)}) """
|
|
||||||
|
|||||||
@@ -61,23 +61,7 @@ async def update_super_user(super_user: str) -> SuperSettings:
|
|||||||
|
|
||||||
|
|
||||||
async def delete_admin_settings(tag: Optional[str] = "core") -> None:
|
async def delete_admin_settings(tag: Optional[str] = "core") -> None:
|
||||||
await db.execute(
|
await db.execute("DELETE FROM settings WHERE tag = :tag", {"tag": tag})
|
||||||
"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'
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSettings:
|
async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSettings:
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from uuid import uuid4
|
|||||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||||
from lnbits.core.crud.wallets import get_wallets
|
from lnbits.core.crud.wallets import get_wallets
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import UserAcls
|
|
||||||
from lnbits.db import Connection, Filters, Page
|
from lnbits.db import Connection, Filters, Page
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
@@ -68,7 +67,6 @@ async def get_accounts(
|
|||||||
accounts.username,
|
accounts.username,
|
||||||
accounts.email,
|
accounts.email,
|
||||||
accounts.pubkey,
|
accounts.pubkey,
|
||||||
accounts.external_id,
|
|
||||||
SUM(COALESCE((
|
SUM(COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)) as balance_msat,
|
), 0)) as balance_msat,
|
||||||
@@ -95,8 +93,6 @@ async def get_accounts(
|
|||||||
async def get_account(
|
async def get_account(
|
||||||
user_id: str, conn: Optional[Connection] = None
|
user_id: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[Account]:
|
) -> Optional[Account]:
|
||||||
if len(user_id) == 0:
|
|
||||||
return None
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE id = :id",
|
"SELECT * FROM accounts WHERE id = :id",
|
||||||
{"id": user_id},
|
{"id": user_id},
|
||||||
@@ -126,11 +122,9 @@ async def delete_accounts_no_wallets(
|
|||||||
async def get_account_by_username(
|
async def get_account_by_username(
|
||||||
username: str, conn: Optional[Connection] = None
|
username: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[Account]:
|
) -> Optional[Account]:
|
||||||
if len(username) == 0:
|
|
||||||
return None
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(username) = :username",
|
"SELECT * FROM accounts WHERE username = :username",
|
||||||
{"username": username.lower()},
|
{"username": username},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -139,8 +133,8 @@ async def get_account_by_pubkey(
|
|||||||
pubkey: str, conn: Optional[Connection] = None
|
pubkey: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[Account]:
|
) -> Optional[Account]:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
"SELECT * FROM accounts WHERE pubkey = :pubkey",
|
||||||
{"pubkey": pubkey.lower()},
|
{"pubkey": pubkey},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -148,11 +142,9 @@ async def get_account_by_pubkey(
|
|||||||
async def get_account_by_email(
|
async def get_account_by_email(
|
||||||
email: str, conn: Optional[Connection] = None
|
email: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[Account]:
|
) -> Optional[Account]:
|
||||||
if len(email) == 0:
|
|
||||||
return None
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(email) = :email",
|
"SELECT * FROM accounts WHERE email = :email",
|
||||||
{"email": email.lower()},
|
{"email": email},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -161,11 +153,8 @@ async def get_account_by_username_or_email(
|
|||||||
username_or_email: str, conn: Optional[Connection] = None
|
username_or_email: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[Account]:
|
) -> Optional[Account]:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"SELECT * FROM accounts WHERE email = :value or username = :value",
|
||||||
SELECT * FROM accounts
|
{"value": username_or_email},
|
||||||
WHERE LOWER(email) = :value or LOWER(username) = :value
|
|
||||||
""",
|
|
||||||
{"value": username_or_email.lower()},
|
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -187,7 +176,6 @@ async def get_user_from_account(
|
|||||||
email=account.email,
|
email=account.email,
|
||||||
username=account.username,
|
username=account.username,
|
||||||
pubkey=account.pubkey,
|
pubkey=account.pubkey,
|
||||||
external_id=account.external_id,
|
|
||||||
extra=account.extra,
|
extra=account.extra,
|
||||||
created_at=account.created_at,
|
created_at=account.created_at,
|
||||||
updated_at=account.updated_at,
|
updated_at=account.updated_at,
|
||||||
@@ -195,23 +183,5 @@ async def get_user_from_account(
|
|||||||
wallets=wallets,
|
wallets=wallets,
|
||||||
admin=account.is_admin,
|
admin=account.is_admin,
|
||||||
super_user=account.is_super_user,
|
super_user=account.is_super_user,
|
||||||
fiat_providers=account.fiat_providers,
|
|
||||||
has_password=account.password_hash is not None,
|
has_password=account.password_hash is not None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def update_user_access_control_list(user_acls: UserAcls):
|
|
||||||
user_acls.updated_at = datetime.now(timezone.utc)
|
|
||||||
await db.update("accounts", user_acls)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_user_access_control_lists(
|
|
||||||
user_id: str, conn: Optional[Connection] = None
|
|
||||||
) -> UserAcls:
|
|
||||||
user_acls = await (conn or db).fetchone(
|
|
||||||
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
|
||||||
{"id": user_id},
|
|
||||||
UserAcls,
|
|
||||||
)
|
|
||||||
|
|
||||||
return user_acls or UserAcls(id=user_id)
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ from typing import Optional
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.wallets import WalletsFilters
|
from lnbits.db import Connection
|
||||||
from lnbits.db import Connection, Filters, Page
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from ..models import Wallet
|
from ..models import Wallet
|
||||||
@@ -24,7 +23,7 @@ async def create_wallet(
|
|||||||
user=user_id,
|
user=user_id,
|
||||||
adminkey=uuid4().hex,
|
adminkey=uuid4().hex,
|
||||||
inkey=uuid4().hex,
|
inkey=uuid4().hex,
|
||||||
currency=settings.lnbits_default_accounting_currency or "USD",
|
currency="USD",
|
||||||
)
|
)
|
||||||
await (conn or db).insert("wallets", wallet)
|
await (conn or db).insert("wallets", wallet)
|
||||||
return wallet
|
return wallet
|
||||||
@@ -136,49 +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()
|
|
||||||
return row.get("count", 0)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_wallet_for_key(
|
async def get_wallet_for_key(
|
||||||
key: str,
|
key: str,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
@@ -197,6 +153,6 @@ async def get_wallet_for_key(
|
|||||||
|
|
||||||
|
|
||||||
async def get_total_balance(conn: Optional[Connection] = None):
|
async def get_total_balance(conn: Optional[Connection] = None):
|
||||||
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
result = await (conn or db).execute("SELECT SUM(balance) FROM balances")
|
||||||
row = result.mappings().first()
|
row = result.mappings().first()
|
||||||
return row.get("balance", 0) or 0
|
return row.get("balance", 0)
|
||||||
|
|||||||
+21
-60
@@ -602,46 +602,34 @@ async def m026_update_payment_table(db: Connection):
|
|||||||
async def m027_update_apipayments_data(db: Connection):
|
async def m027_update_apipayments_data(db: Connection):
|
||||||
result = None
|
result = None
|
||||||
try:
|
try:
|
||||||
result = await db.execute("SELECT * FROM apipayments LIMIT 100")
|
result = await db.execute("SELECT * FROM apipayments")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Could not select, trying again after cache cleared.")
|
logger.warning("Could not select, trying again after cache cleared.")
|
||||||
logger.debug(exc)
|
logger.debug(exc)
|
||||||
await db.execute("COMMIT")
|
await db.execute("COMMIT")
|
||||||
|
|
||||||
offset = 0
|
result = await db.execute("SELECT * FROM apipayments")
|
||||||
limit = 1000
|
|
||||||
payments: list[dict[Any, Any]] = []
|
|
||||||
logger.info("Updating payments")
|
|
||||||
while len(payments) > 0 or offset == 0:
|
|
||||||
logger.info(f"Updating {offset} to {offset+limit}")
|
|
||||||
|
|
||||||
result = await db.execute(
|
payments = result.mappings().all()
|
||||||
f"SELECT * FROM apipayments ORDER BY time LIMIT {limit} OFFSET {offset}"
|
for payment in payments:
|
||||||
|
tag = None
|
||||||
|
created_at = payment.get("time")
|
||||||
|
if payment.get("extra"):
|
||||||
|
extra = json.loads(payment.get("extra"))
|
||||||
|
tag = extra.get("tag")
|
||||||
|
tsph = db.timestamp_placeholder("created_at")
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE apipayments
|
||||||
|
SET tag = :tag, created_at = {tsph}, updated_at = {tsph}
|
||||||
|
WHERE checking_id = :checking_id
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"tag": tag,
|
||||||
|
"created_at": created_at,
|
||||||
|
"checking_id": payment.get("checking_id"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
payments = result.mappings().all()
|
|
||||||
logger.info(f"Payments count: {len(payments)}")
|
|
||||||
|
|
||||||
for payment in payments:
|
|
||||||
tag = None
|
|
||||||
created_at = payment.get("time")
|
|
||||||
if payment.get("extra"):
|
|
||||||
extra = json.loads(str(payment.get("extra")))
|
|
||||||
tag = extra.get("tag")
|
|
||||||
tsph = db.timestamp_placeholder("created_at")
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
UPDATE apipayments
|
|
||||||
SET tag = :tag, created_at = {tsph}, updated_at = {tsph}
|
|
||||||
WHERE checking_id = :checking_id
|
|
||||||
""",
|
|
||||||
{
|
|
||||||
"tag": tag,
|
|
||||||
"created_at": created_at,
|
|
||||||
"checking_id": payment.get("checking_id"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
offset += limit
|
|
||||||
logger.info("Payments updated")
|
|
||||||
|
|
||||||
|
|
||||||
async def m028_update_settings(db: Connection):
|
async def m028_update_settings(db: Connection):
|
||||||
@@ -696,30 +684,3 @@ async def m029_create_audit_table(db: Connection):
|
|||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m030_add_user_api_tokens_column(db: Connection):
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
ALTER TABLE accounts ADD COLUMN access_control_list TEXT
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|||||||
@@ -14,15 +14,10 @@ from .payments import (
|
|||||||
DecodePayment,
|
DecodePayment,
|
||||||
PayInvoice,
|
PayInvoice,
|
||||||
Payment,
|
Payment,
|
||||||
PaymentCountField,
|
|
||||||
PaymentCountStat,
|
|
||||||
PaymentDailyStats,
|
|
||||||
PaymentExtra,
|
PaymentExtra,
|
||||||
PaymentFilters,
|
PaymentFilters,
|
||||||
PaymentHistoryPoint,
|
PaymentHistoryPoint,
|
||||||
PaymentsStatusCount,
|
|
||||||
PaymentState,
|
PaymentState,
|
||||||
PaymentWalletStats,
|
|
||||||
)
|
)
|
||||||
from .tinyurl import TinyURL
|
from .tinyurl import TinyURL
|
||||||
from .users import (
|
from .users import (
|
||||||
@@ -41,61 +36,62 @@ from .users import (
|
|||||||
UpdateUserPassword,
|
UpdateUserPassword,
|
||||||
UpdateUserPubkey,
|
UpdateUserPubkey,
|
||||||
User,
|
User,
|
||||||
UserAcls,
|
|
||||||
UserExtra,
|
UserExtra,
|
||||||
)
|
)
|
||||||
from .wallets import BaseWallet, CreateWallet, KeyType, Wallet, WalletTypeInfo
|
from .wallets import BaseWallet, CreateWallet, KeyType, Wallet, WalletTypeInfo
|
||||||
from .webpush import CreateWebPushSubscription, WebPushSubscription
|
from .webpush import CreateWebPushSubscription, WebPushSubscription
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AccessTokenPayload",
|
# audit
|
||||||
"Account",
|
|
||||||
"AccountFilters",
|
|
||||||
"AccountOverview",
|
|
||||||
"AuditEntry",
|
"AuditEntry",
|
||||||
"AuditFilters",
|
"AuditFilters",
|
||||||
|
# lnurl
|
||||||
|
"CreateLnurl",
|
||||||
|
"CreateLnurlAuth",
|
||||||
|
"PayLnurlWData",
|
||||||
|
# misc
|
||||||
"BalanceDelta",
|
"BalanceDelta",
|
||||||
"BaseWallet",
|
|
||||||
"Callback",
|
"Callback",
|
||||||
"ConversionData",
|
"ConversionData",
|
||||||
"CoreAppExtra",
|
"CoreAppExtra",
|
||||||
"CreateInvoice",
|
|
||||||
"CreateLnurl",
|
|
||||||
"CreateLnurlAuth",
|
|
||||||
"CreatePayment",
|
|
||||||
"CreateUser",
|
|
||||||
"CreateWallet",
|
|
||||||
"CreateWebPushSubscription",
|
|
||||||
"DbVersion",
|
"DbVersion",
|
||||||
|
"SimpleStatus",
|
||||||
|
# payments
|
||||||
|
"CreateInvoice",
|
||||||
|
"CreatePayment",
|
||||||
"DecodePayment",
|
"DecodePayment",
|
||||||
"KeyType",
|
|
||||||
"LoginUsernamePassword",
|
|
||||||
"LoginUsr",
|
|
||||||
"PayInvoice",
|
"PayInvoice",
|
||||||
"PayLnurlWData",
|
|
||||||
"Payment",
|
"Payment",
|
||||||
"PaymentCountField",
|
|
||||||
"PaymentCountStat",
|
|
||||||
"PaymentDailyStats",
|
|
||||||
"PaymentExtra",
|
"PaymentExtra",
|
||||||
"PaymentFilters",
|
"PaymentFilters",
|
||||||
"PaymentHistoryPoint",
|
"PaymentHistoryPoint",
|
||||||
"PaymentState",
|
"PaymentState",
|
||||||
"PaymentWalletStats",
|
# tinyurl
|
||||||
"PaymentsStatusCount",
|
|
||||||
"RegisterUser",
|
|
||||||
"ResetUserPassword",
|
|
||||||
"SimpleStatus",
|
|
||||||
"TinyURL",
|
"TinyURL",
|
||||||
|
# users
|
||||||
|
"AccessTokenPayload",
|
||||||
|
"Account",
|
||||||
|
"AccountFilters",
|
||||||
|
"AccountOverview",
|
||||||
|
"CreateUser",
|
||||||
|
"RegisterUser",
|
||||||
|
"LoginUsernamePassword",
|
||||||
|
"LoginUsr",
|
||||||
|
"ResetUserPassword",
|
||||||
"UpdateBalance",
|
"UpdateBalance",
|
||||||
"UpdateSuperuserPassword",
|
"UpdateSuperuserPassword",
|
||||||
"UpdateUser",
|
"UpdateUser",
|
||||||
"UpdateUserPassword",
|
"UpdateUserPassword",
|
||||||
"UpdateUserPubkey",
|
"UpdateUserPubkey",
|
||||||
"User",
|
"User",
|
||||||
"UserAcls",
|
|
||||||
"UserExtra",
|
"UserExtra",
|
||||||
|
# wallets
|
||||||
|
"BaseWallet",
|
||||||
|
"CreateWallet",
|
||||||
|
"KeyType",
|
||||||
"Wallet",
|
"Wallet",
|
||||||
"WalletTypeInfo",
|
"WalletTypeInfo",
|
||||||
|
# webpush
|
||||||
|
"CreateWebPushSubscription",
|
||||||
"WebPushSubscription",
|
"WebPushSubscription",
|
||||||
]
|
]
|
||||||
|
|||||||
+16
-15
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -9,16 +10,16 @@ from lnbits.settings import settings
|
|||||||
|
|
||||||
|
|
||||||
class AuditEntry(BaseModel):
|
class AuditEntry(BaseModel):
|
||||||
component: str | None = None
|
component: Optional[str] = None
|
||||||
ip_address: str | None = None
|
ip_address: Optional[str] = None
|
||||||
user_id: str | None = None
|
user_id: Optional[str] = None
|
||||||
path: str | None = None
|
path: Optional[str] = None
|
||||||
request_type: str | None = None
|
request_type: Optional[str] = None
|
||||||
request_method: str | None = None
|
request_method: Optional[str] = None
|
||||||
request_details: str | None = None
|
request_details: Optional[str] = None
|
||||||
response_code: str | None = None
|
response_code: Optional[str] = None
|
||||||
duration: float
|
duration: float
|
||||||
delete_at: datetime | None = None
|
delete_at: Optional[datetime] = None
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, **data):
|
||||||
@@ -41,12 +42,12 @@ class AuditFilters(FilterModel):
|
|||||||
"duration",
|
"duration",
|
||||||
]
|
]
|
||||||
|
|
||||||
ip_address: str | None = None
|
ip_address: Optional[str] = None
|
||||||
user_id: str | None = None
|
user_id: Optional[str] = None
|
||||||
path: str | None = None
|
path: Optional[str] = None
|
||||||
request_method: str | None = None
|
request_method: Optional[str] = None
|
||||||
response_code: str | None = None
|
response_code: Optional[str] = None
|
||||||
component: str | None = None
|
component: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class AuditCountStat(BaseModel):
|
class AuditCountStat(BaseModel):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -29,17 +29,17 @@ class ExplicitRelease(BaseModel):
|
|||||||
archive: str
|
archive: str
|
||||||
hash: str
|
hash: str
|
||||||
dependencies: list[str] = []
|
dependencies: list[str] = []
|
||||||
repo: str | None
|
repo: Optional[str]
|
||||||
icon: str | None
|
icon: Optional[str]
|
||||||
short_description: str | None
|
short_description: Optional[str]
|
||||||
min_lnbits_version: str | None
|
min_lnbits_version: Optional[str]
|
||||||
max_lnbits_version: str | None
|
max_lnbits_version: Optional[str]
|
||||||
html_url: str | None # todo: release_url
|
html_url: Optional[str] # todo: release_url
|
||||||
warning: str | None
|
warning: Optional[str]
|
||||||
info_notification: str | None
|
info_notification: Optional[str]
|
||||||
critical_notification: str | None
|
critical_notification: Optional[str]
|
||||||
details_link: str | None
|
details_link: Optional[str]
|
||||||
pay_link: str | None
|
pay_link: Optional[str]
|
||||||
|
|
||||||
def is_version_compatible(self):
|
def is_version_compatible(self):
|
||||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
@@ -77,9 +77,9 @@ class ExtensionConfig(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
short_description: str
|
short_description: str
|
||||||
tile: str = ""
|
tile: str = ""
|
||||||
warning: str | None = ""
|
warning: Optional[str] = ""
|
||||||
min_lnbits_version: str | None
|
min_lnbits_version: Optional[str]
|
||||||
max_lnbits_version: str | None
|
max_lnbits_version: Optional[str]
|
||||||
|
|
||||||
def is_version_compatible(self) -> bool:
|
def is_version_compatible(self) -> bool:
|
||||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
@@ -87,7 +87,7 @@ class ExtensionConfig(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def fetch_github_release_config(
|
async def fetch_github_release_config(
|
||||||
cls, org: str, repo: str, tag_name: str
|
cls, org: str, repo: str, tag_name: str
|
||||||
) -> ExtensionConfig | None:
|
) -> Optional[ExtensionConfig]:
|
||||||
config_url = (
|
config_url = (
|
||||||
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
|
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
|
||||||
)
|
)
|
||||||
@@ -97,28 +97,28 @@ class ExtensionConfig(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ReleasePaymentInfo(BaseModel):
|
class ReleasePaymentInfo(BaseModel):
|
||||||
amount: int | None = None
|
amount: Optional[int] = None
|
||||||
pay_link: str | None = None
|
pay_link: Optional[str] = None
|
||||||
payment_hash: str | None = None
|
payment_hash: Optional[str] = None
|
||||||
payment_request: str | None = None
|
payment_request: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class PayToEnableInfo(BaseModel):
|
class PayToEnableInfo(BaseModel):
|
||||||
amount: int = 0
|
amount: int
|
||||||
required: bool = False
|
required: bool = False
|
||||||
wallet: str | None = None
|
wallet: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class UserExtensionInfo(BaseModel):
|
class UserExtensionInfo(BaseModel):
|
||||||
paid_to_enable: bool | None = False
|
paid_to_enable: Optional[bool] = False
|
||||||
payment_hash_to_enable: str | None = None
|
payment_hash_to_enable: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class UserExtension(BaseModel):
|
class UserExtension(BaseModel):
|
||||||
user: str
|
user: str
|
||||||
extension: str
|
extension: str
|
||||||
active: bool
|
active: bool
|
||||||
extra: UserExtensionInfo | None = None
|
extra: Optional[UserExtensionInfo] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_paid(self) -> bool:
|
def is_paid(self) -> bool:
|
||||||
@@ -140,14 +140,16 @@ class UserExtension(BaseModel):
|
|||||||
class Extension(BaseModel):
|
class Extension(BaseModel):
|
||||||
code: str
|
code: str
|
||||||
is_valid: bool
|
is_valid: bool
|
||||||
name: str | None = None
|
name: Optional[str] = None
|
||||||
short_description: str | None = None
|
short_description: Optional[str] = None
|
||||||
tile: str | None = None
|
tile: Optional[str] = None
|
||||||
upgrade_hash: str | None = ""
|
upgrade_hash: Optional[str] = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module_name(self) -> str:
|
def module_name(self) -> str:
|
||||||
if self.is_upgrade_extension:
|
if self.is_upgrade_extension:
|
||||||
|
if settings.has_default_extension_path:
|
||||||
|
return f"lnbits.upgrades.{self.code}-{self.upgrade_hash}"
|
||||||
return f"{self.code}-{self.upgrade_hash}"
|
return f"{self.code}-{self.upgrade_hash}"
|
||||||
|
|
||||||
if settings.has_default_extension_path:
|
if settings.has_default_extension_path:
|
||||||
@@ -176,21 +178,21 @@ class ExtensionRelease(BaseModel):
|
|||||||
archive: str
|
archive: str
|
||||||
source_repo: str
|
source_repo: str
|
||||||
is_github_release: bool = False
|
is_github_release: bool = False
|
||||||
hash: str | None = None
|
hash: Optional[str] = None
|
||||||
min_lnbits_version: str | None = None
|
min_lnbits_version: Optional[str] = None
|
||||||
max_lnbits_version: str | None = None
|
max_lnbits_version: Optional[str] = None
|
||||||
is_version_compatible: bool | None = True
|
is_version_compatible: Optional[bool] = True
|
||||||
html_url: str | None = None
|
html_url: Optional[str] = None
|
||||||
description: str | None = None
|
description: Optional[str] = None
|
||||||
warning: str | None = None
|
warning: Optional[str] = None
|
||||||
repo: str | None = None
|
repo: Optional[str] = None
|
||||||
icon: str | None = None
|
icon: Optional[str] = None
|
||||||
details_link: str | None = None
|
details_link: Optional[str] = None
|
||||||
|
|
||||||
pay_link: str | None = None
|
pay_link: Optional[str] = None
|
||||||
cost_sats: int | None = None
|
cost_sats: Optional[int] = None
|
||||||
paid_sats: int | None = 0
|
paid_sats: Optional[int] = 0
|
||||||
payment_hash: str | None = None
|
payment_hash: Optional[str] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def archive_url(self) -> str:
|
def archive_url(self) -> str:
|
||||||
@@ -208,8 +210,8 @@ class ExtensionRelease(BaseModel):
|
|||||||
self.cost_sats = payment_info.amount if payment_info else None
|
self.cost_sats = payment_info.amount if payment_info else None
|
||||||
|
|
||||||
async def fetch_release_payment_info(
|
async def fetch_release_payment_info(
|
||||||
self, amount: int | None = None
|
self, amount: Optional[int] = None
|
||||||
) -> ReleasePaymentInfo | None:
|
) -> Optional[ReleasePaymentInfo]:
|
||||||
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
|
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
|
||||||
assert url, "Missing URL for payment info."
|
assert url, "Missing URL for payment info."
|
||||||
try:
|
try:
|
||||||
@@ -281,7 +283,7 @@ class ExtensionRelease(BaseModel):
|
|||||||
return [GitHubRepoRelease.parse_obj(r) for r in releases]
|
return [GitHubRepoRelease.parse_obj(r) for r in releases]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def fetch_release_details(cls, details_link: str) -> dict | None:
|
async def fetch_release_details(cls, details_link: str) -> Optional[dict]:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
@@ -300,12 +302,12 @@ class ExtensionRelease(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ExtensionMeta(BaseModel):
|
class ExtensionMeta(BaseModel):
|
||||||
installed_release: ExtensionRelease | None = None
|
installed_release: Optional[ExtensionRelease] = None
|
||||||
latest_release: ExtensionRelease | None = None
|
latest_release: Optional[ExtensionRelease] = None
|
||||||
pay_to_enable: PayToEnableInfo | None = None
|
pay_to_enable: Optional[PayToEnableInfo] = None
|
||||||
payments: list[ReleasePaymentInfo] = []
|
payments: list[ReleasePaymentInfo] = []
|
||||||
dependencies: list[str] = []
|
dependencies: list[str] = []
|
||||||
archive: str | None = None
|
archive: Optional[str] = None
|
||||||
featured: bool = False
|
featured: bool = False
|
||||||
|
|
||||||
|
|
||||||
@@ -313,11 +315,11 @@ class InstallableExtension(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
version: str
|
version: str
|
||||||
active: bool | None = False
|
active: Optional[bool] = False
|
||||||
short_description: str | None = None
|
short_description: Optional[str] = None
|
||||||
icon: str | None = None
|
icon: Optional[str] = None
|
||||||
stars: int = 0
|
stars: int = 0
|
||||||
meta: ExtensionMeta | None = None
|
meta: Optional[ExtensionMeta] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hash(self) -> str:
|
def hash(self) -> str:
|
||||||
@@ -341,11 +343,15 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def ext_upgrade_dir(self) -> Path:
|
def ext_upgrade_dir(self) -> Path:
|
||||||
return Path(settings.lnbits_extensions_upgrade_path, f"{self.id}-{self.hash}")
|
return Path(
|
||||||
|
settings.lnbits_extensions_path, "upgrades", f"{self.id}-{self.hash}"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module_name(self) -> str:
|
def module_name(self) -> str:
|
||||||
if self.ext_upgrade_dir.is_dir():
|
if self.ext_upgrade_dir.is_dir():
|
||||||
|
if settings.has_default_extension_path:
|
||||||
|
return f"lnbits.upgrades.{self.id}-{self.hash}"
|
||||||
return f"{self.id}-{self.hash}"
|
return f"{self.id}-{self.hash}"
|
||||||
|
|
||||||
if settings.has_default_extension_path:
|
if settings.has_default_extension_path:
|
||||||
@@ -405,7 +411,9 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
def extract_archive(self):
|
def extract_archive(self):
|
||||||
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
|
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
|
||||||
Path(settings.lnbits_extensions_upgrade_path).mkdir(parents=True, exist_ok=True)
|
Path(settings.lnbits_extensions_path, "upgrades").mkdir(
|
||||||
|
parents=True, exist_ok=True
|
||||||
|
)
|
||||||
|
|
||||||
tmp_dir = Path(settings.lnbits_data_folder, "unzip-temp", self.hash)
|
tmp_dir = Path(settings.lnbits_data_folder, "unzip-temp", self.hash)
|
||||||
shutil.rmtree(tmp_dir, True)
|
shutil.rmtree(tmp_dir, True)
|
||||||
@@ -452,7 +460,7 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
shutil.rmtree(self.ext_upgrade_dir, True)
|
shutil.rmtree(self.ext_upgrade_dir, True)
|
||||||
|
|
||||||
def check_latest_version(self, release: ExtensionRelease | None):
|
def check_latest_version(self, release: Optional[ExtensionRelease]):
|
||||||
if not release:
|
if not release:
|
||||||
return
|
return
|
||||||
if not self.meta or not self.meta.latest_release:
|
if not self.meta or not self.meta.latest_release:
|
||||||
@@ -465,7 +473,9 @@ class InstallableExtension(BaseModel):
|
|||||||
):
|
):
|
||||||
self.meta.latest_release = release
|
self.meta.latest_release = release
|
||||||
|
|
||||||
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
|
def find_existing_payment(
|
||||||
|
self, pay_link: Optional[str]
|
||||||
|
) -> Optional[ReleasePaymentInfo]:
|
||||||
if not pay_link or not self.meta or not self.meta.payments:
|
if not pay_link or not self.meta or not self.meta.payments:
|
||||||
return None
|
return None
|
||||||
return next(
|
return next(
|
||||||
@@ -505,7 +515,7 @@ class InstallableExtension(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def from_github_release(
|
async def from_github_release(
|
||||||
cls, github_release: GitHubRelease
|
cls, github_release: GitHubRelease
|
||||||
) -> InstallableExtension | None:
|
) -> Optional[InstallableExtension]:
|
||||||
try:
|
try:
|
||||||
repo, latest_release, config = await cls.fetch_github_repo_info(
|
repo, latest_release, config = await cls.fetch_github_repo_info(
|
||||||
github_release.organisation, github_release.repository
|
github_release.organisation, github_release.repository
|
||||||
@@ -544,7 +554,7 @@ class InstallableExtension(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_ext_dir(cls, ext_id: str) -> InstallableExtension | None:
|
def from_ext_dir(cls, ext_id: str) -> Optional[InstallableExtension]:
|
||||||
try:
|
try:
|
||||||
conf_path = Path(
|
conf_path = Path(
|
||||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||||
@@ -584,6 +594,7 @@ class InstallableExtension(BaseModel):
|
|||||||
cls,
|
cls,
|
||||||
) -> list[InstallableExtension]:
|
) -> list[InstallableExtension]:
|
||||||
extension_list: list[InstallableExtension] = []
|
extension_list: list[InstallableExtension] = []
|
||||||
|
extension_id_list: list[str] = []
|
||||||
|
|
||||||
for url in settings.lnbits_extensions_manifests:
|
for url in settings.lnbits_extensions_manifests:
|
||||||
try:
|
try:
|
||||||
@@ -604,6 +615,7 @@ class InstallableExtension(BaseModel):
|
|||||||
meta.featured = ext.id in manifest.featured
|
meta.featured = ext.id in manifest.featured
|
||||||
ext.meta = meta
|
ext.meta = meta
|
||||||
extension_list += [ext]
|
extension_list += [ext]
|
||||||
|
extension_id_list += [ext.id]
|
||||||
|
|
||||||
for e in manifest.extensions:
|
for e in manifest.extensions:
|
||||||
release = ExtensionRelease.from_explicit_release(url, e)
|
release = ExtensionRelease.from_explicit_release(url, e)
|
||||||
@@ -619,10 +631,10 @@ class InstallableExtension(BaseModel):
|
|||||||
meta.featured = ext.id in manifest.featured
|
meta.featured = ext.id in manifest.featured
|
||||||
ext.meta = meta
|
ext.meta = meta
|
||||||
extension_list += [ext]
|
extension_list += [ext]
|
||||||
|
extension_id_list += [e.id]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Manifest {url} failed with '{e!s}'")
|
logger.warning(f"Manifest {url} failed with '{e!s}'")
|
||||||
|
|
||||||
settings.lnbits_all_extensions_ids = {e.id for e in extension_list}
|
|
||||||
return extension_list
|
return extension_list
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -655,7 +667,7 @@ class InstallableExtension(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def get_extension_release(
|
async def get_extension_release(
|
||||||
cls, ext_id: str, source_repo: str, archive: str, version: str
|
cls, ext_id: str, source_repo: str, archive: str, version: str
|
||||||
) -> ExtensionRelease | None:
|
) -> Optional[ExtensionRelease]:
|
||||||
all_releases: list[ExtensionRelease] = (
|
all_releases: list[ExtensionRelease] = (
|
||||||
await InstallableExtension.get_extension_releases(ext_id)
|
await InstallableExtension.get_extension_releases(ext_id)
|
||||||
)
|
)
|
||||||
@@ -706,8 +718,8 @@ class CreateExtension(BaseModel):
|
|||||||
archive: str
|
archive: str
|
||||||
source_repo: str
|
source_repo: str
|
||||||
version: str
|
version: str
|
||||||
cost_sats: int | None = 0
|
cost_sats: Optional[int] = 0
|
||||||
payment_hash: str | None = None
|
payment_hash: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ExtensionDetailsRequest(BaseModel):
|
class ExtensionDetailsRequest(BaseModel):
|
||||||
@@ -716,7 +728,7 @@ class ExtensionDetailsRequest(BaseModel):
|
|||||||
version: str
|
version: str
|
||||||
|
|
||||||
|
|
||||||
async def github_api_get(url: str, error_msg: str | None) -> Any:
|
async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
if settings.lnbits_ext_github_token:
|
if settings.lnbits_ext_github_token:
|
||||||
headers["Authorization"] = f"Bearer {settings.lnbits_ext_github_token}"
|
headers["Authorization"] = f"Bearer {settings.lnbits_ext_github_token}"
|
||||||
@@ -728,7 +740,7 @@ async def github_api_get(url: str, error_msg: str | None) -> Any:
|
|||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
def icon_to_github_url(source_repo: str, path: str | None) -> str:
|
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
|
||||||
if not path:
|
if not path:
|
||||||
return ""
|
return ""
|
||||||
_, _, *rest = path.split("/")
|
_, _, *rest = path.split("/")
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ class Callback(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class BalanceDelta(BaseModel):
|
class BalanceDelta(BaseModel):
|
||||||
lnbits_balance_sats: int
|
lnbits_balance_msats: int
|
||||||
node_balance_sats: int
|
node_balance_msats: int
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def delta_sats(self) -> int:
|
def delta_msats(self):
|
||||||
return int(self.lnbits_balance_sats - self.node_balance_sats)
|
return self.node_balance_msats - self.lnbits_balance_msats
|
||||||
|
|
||||||
|
|
||||||
class SimpleStatus(BaseModel):
|
class SimpleStatus(BaseModel):
|
||||||
@@ -38,16 +38,6 @@ class SimpleStatus(BaseModel):
|
|||||||
message: str
|
message: str
|
||||||
|
|
||||||
|
|
||||||
class SimpleItem(BaseModel):
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
class DbVersion(BaseModel):
|
class DbVersion(BaseModel):
|
||||||
db: str
|
db: str
|
||||||
version: int
|
version: int
|
||||||
|
|
||||||
|
|
||||||
class Image(BaseModel):
|
|
||||||
filename: str
|
|
||||||
directory: str = "library"
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationType(Enum):
|
|
||||||
server_status = "server_status"
|
|
||||||
settings_update = "settings_update"
|
|
||||||
balance_update = "balance_update"
|
|
||||||
watchdog_check = "watchdog_check"
|
|
||||||
balance_delta = "balance_delta"
|
|
||||||
server_start_stop = "server_start_stop"
|
|
||||||
incoming_payment = "incoming_payment"
|
|
||||||
outgoing_payment = "outgoing_payment"
|
|
||||||
text_message = "text_message"
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationMessage(BaseModel):
|
|
||||||
message_type: NotificationType
|
|
||||||
values: dict
|
|
||||||
|
|
||||||
|
|
||||||
NOTIFICATION_TEMPLATES = {
|
|
||||||
"text_message": "{message}",
|
|
||||||
"server_status": """*SERVER STATUS*
|
|
||||||
*Up time*: `{up_time}`.
|
|
||||||
*Accounts*: `{accounts_count}`.
|
|
||||||
*Wallets*: `{wallets_count}`.
|
|
||||||
*In/Out payments*: `{in_payments_count}`/`{out_payments_count}`.
|
|
||||||
*Pending payments*: `{pending_payments_count}`.
|
|
||||||
*Failed payments*: `{failed_payments_count}`.
|
|
||||||
*LNbits balance*: `{lnbits_balance_sats}` sats.
|
|
||||||
*Node balance*: `{node_balance_sats}` sats.""",
|
|
||||||
"server_start_stop": """*SERVER*
|
|
||||||
{message}
|
|
||||||
*Time*: `{up_time}` seconds.
|
|
||||||
""",
|
|
||||||
"settings_update": """*SETTINGS UPDATED*
|
|
||||||
User: `{username}`.
|
|
||||||
""",
|
|
||||||
"balance_update": """*WALLET CREDIT/DEBIT*
|
|
||||||
Wallet `{wallet_name}` balance updated with `{amount}` sats.
|
|
||||||
*Current balance*: `{balance}` sats.
|
|
||||||
*Wallet ID*: `{wallet_id}`
|
|
||||||
""",
|
|
||||||
"watchdog_check": """*WATCHDOG BALANCE CHECK*
|
|
||||||
*Delta*: `{delta_sats}` sats.
|
|
||||||
*LNbits balance*: `{lnbits_balance_sats}` sats.
|
|
||||||
*Node balance*: `{node_balance_sats}` sats.
|
|
||||||
*Switching to Void Wallet*: `{switch_to_void_wallet}`.
|
|
||||||
""",
|
|
||||||
"balance_delta": """*BALANCE DELTA CHANGED*
|
|
||||||
*New delta*: `{delta_sats}` sats.
|
|
||||||
*Old delta*: `{old_delta_sats}` sats.
|
|
||||||
*LNbits balance*: `{lnbits_balance_sats}` sats.
|
|
||||||
*Node balance*: `{node_balance_sats}` sats.
|
|
||||||
""",
|
|
||||||
"outgoing_payment": """*PAYMENT SENT*
|
|
||||||
*Amount*: {fiat_value_fmt}`{amount_sats}`*sats*.
|
|
||||||
*Wallet*: `{wallet_name}` ({wallet_id}).""",
|
|
||||||
"incoming_payment": """*PAYMENT RECEIVED*
|
|
||||||
*Amount*: {fiat_value_fmt}`{amount_sats}`*sats*.
|
|
||||||
*Wallet*: `{wallet_name}` ({wallet_id}).
|
|
||||||
""",
|
|
||||||
}
|
|
||||||
+41
-138
@@ -2,19 +2,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Literal
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
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.utils.exchange_rates import allowed_currencies
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
from lnbits.wallets.base import (
|
from lnbits.wallets.base import (
|
||||||
@@ -35,16 +28,16 @@ class PaymentState(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class PaymentExtra(BaseModel):
|
class PaymentExtra(BaseModel):
|
||||||
comment: str | None = None
|
comment: Optional[str] = None
|
||||||
success_action: str | None = None
|
success_action: Optional[str] = None
|
||||||
lnurl_response: str | None = None
|
lnurl_response: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class PayInvoice(BaseModel):
|
class PayInvoice(BaseModel):
|
||||||
payment_request: str
|
payment_request: str
|
||||||
description: str | None = None
|
description: Optional[str] = None
|
||||||
max_sat: int | None = None
|
max_sat: Optional[int] = None
|
||||||
extra: dict | None = {}
|
extra: Optional[dict] = {}
|
||||||
|
|
||||||
|
|
||||||
class CreatePayment(BaseModel):
|
class CreatePayment(BaseModel):
|
||||||
@@ -53,10 +46,10 @@ class CreatePayment(BaseModel):
|
|||||||
bolt11: str
|
bolt11: str
|
||||||
amount_msat: int
|
amount_msat: int
|
||||||
memo: str
|
memo: str
|
||||||
extra: dict | None = {}
|
extra: Optional[dict] = {}
|
||||||
preimage: str | None = None
|
preimage: Optional[str] = None
|
||||||
expiry: datetime | None = None
|
expiry: Optional[datetime] = None
|
||||||
webhook: str | None = None
|
webhook: Optional[str] = None
|
||||||
fee: int = 0
|
fee: int = 0
|
||||||
|
|
||||||
|
|
||||||
@@ -67,16 +60,14 @@ class Payment(BaseModel):
|
|||||||
amount: int
|
amount: int
|
||||||
fee: int
|
fee: int
|
||||||
bolt11: str
|
bolt11: str
|
||||||
# payment_request: str | None
|
|
||||||
fiat_provider: str | None = None
|
|
||||||
status: str = PaymentState.PENDING
|
status: str = PaymentState.PENDING
|
||||||
memo: str | None = None
|
memo: Optional[str] = None
|
||||||
expiry: datetime | None = None
|
expiry: Optional[datetime] = None
|
||||||
webhook: str | None = None
|
webhook: Optional[str] = None
|
||||||
webhook_status: str | None = None
|
webhook_status: Optional[int] = None
|
||||||
preimage: str | None = None
|
preimage: Optional[str] = None
|
||||||
tag: str | None = None
|
tag: Optional[str] = None
|
||||||
extension: str | None = None
|
extension: Optional[str] = None
|
||||||
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
@@ -116,23 +107,14 @@ class Payment(BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_internal(self) -> bool:
|
def is_internal(self) -> bool:
|
||||||
return self.checking_id.startswith("internal_") or self.checking_id.startswith(
|
return self.checking_id.startswith("internal_")
|
||||||
"fiat_"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def check_status(
|
async def check_status(self) -> PaymentStatus:
|
||||||
self, skip_internal_payment_notifications: bool | None = False
|
|
||||||
) -> PaymentStatus:
|
|
||||||
if self.is_internal:
|
if self.is_internal:
|
||||||
if self.success:
|
if self.success:
|
||||||
return PaymentSuccessStatus()
|
return PaymentSuccessStatus()
|
||||||
if self.failed:
|
if self.failed:
|
||||||
return PaymentFailedStatus()
|
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()
|
return PaymentPendingStatus()
|
||||||
funding_source = get_funding_source()
|
funding_source = get_funding_source()
|
||||||
if self.is_out:
|
if self.is_out:
|
||||||
@@ -141,95 +123,24 @@ class Payment(BaseModel):
|
|||||||
status = await funding_source.get_invoice_status(self.checking_id)
|
status = await funding_source.get_invoice_status(self.checking_id)
|
||||||
return status
|
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):
|
class PaymentFilters(FilterModel):
|
||||||
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
|
__search_fields__ = ["memo", "amount"]
|
||||||
|
|
||||||
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
status: str
|
||||||
|
checking_id: str
|
||||||
status: str | None
|
|
||||||
tag: str | None
|
|
||||||
checking_id: str | None
|
|
||||||
amount: int
|
amount: int
|
||||||
fee: int
|
fee: int
|
||||||
memo: str | None
|
memo: Optional[str]
|
||||||
time: datetime
|
time: datetime
|
||||||
preimage: str | None
|
bolt11: str
|
||||||
payment_hash: str | None
|
preimage: str
|
||||||
wallet_id: str | None
|
payment_hash: str
|
||||||
|
expiry: Optional[datetime]
|
||||||
|
extra: dict = {}
|
||||||
class PaymentDataPoint(BaseModel):
|
wallet_id: str
|
||||||
date: datetime
|
webhook: Optional[str]
|
||||||
count: int
|
webhook_status: Optional[int]
|
||||||
max_amount: int
|
|
||||||
min_amount: int
|
|
||||||
average_amount: int
|
|
||||||
total_amount: int
|
|
||||||
max_fee: int
|
|
||||||
min_fee: int
|
|
||||||
average_fee: int
|
|
||||||
total_fee: int
|
|
||||||
|
|
||||||
|
|
||||||
PaymentCountField = Literal["status", "tag", "extension", "wallet_id"]
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentCountStat(BaseModel):
|
|
||||||
field: str = ""
|
|
||||||
total: float = 0
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentWalletStats(BaseModel):
|
|
||||||
wallet_id: str = ""
|
|
||||||
wallet_name: str = ""
|
|
||||||
user_id: str = ""
|
|
||||||
payments_count: int
|
|
||||||
balance: float = 0
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentDailyStats(BaseModel):
|
|
||||||
date: datetime
|
|
||||||
balance: float = 0
|
|
||||||
balance_in: float | None = 0
|
|
||||||
balance_out: float | None = 0
|
|
||||||
payments_count: int = 0
|
|
||||||
count_in: int | None = 0
|
|
||||||
count_out: int | None = 0
|
|
||||||
fee: float = 0
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentHistoryPoint(BaseModel):
|
class PaymentHistoryPoint(BaseModel):
|
||||||
@@ -241,7 +152,7 @@ class PaymentHistoryPoint(BaseModel):
|
|||||||
|
|
||||||
class DecodePayment(BaseModel):
|
class DecodePayment(BaseModel):
|
||||||
data: str
|
data: str
|
||||||
filter_fields: list[str] | None = []
|
filter_fields: Optional[list[str]] = []
|
||||||
|
|
||||||
|
|
||||||
class CreateInvoice(BaseModel):
|
class CreateInvoice(BaseModel):
|
||||||
@@ -249,15 +160,14 @@ class CreateInvoice(BaseModel):
|
|||||||
internal: bool = False
|
internal: bool = False
|
||||||
out: bool = True
|
out: bool = True
|
||||||
amount: float = Query(None, ge=0)
|
amount: float = Query(None, ge=0)
|
||||||
memo: str | None = Query(None, max_length=640)
|
memo: Optional[str] = None
|
||||||
description_hash: str | None = None
|
description_hash: Optional[str] = None
|
||||||
unhashed_description: str | None = None
|
unhashed_description: Optional[str] = None
|
||||||
expiry: int | None = None
|
expiry: Optional[int] = None
|
||||||
extra: dict | None = None
|
extra: Optional[dict] = None
|
||||||
webhook: str | None = None
|
webhook: Optional[str] = None
|
||||||
bolt11: str | None = None
|
bolt11: Optional[str] = None
|
||||||
lnurl_callback: str | None = None
|
lnurl_callback: Optional[str] = None
|
||||||
fiat_provider: str | None = None
|
|
||||||
|
|
||||||
@validator("unit")
|
@validator("unit")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -265,10 +175,3 @@ class CreateInvoice(BaseModel):
|
|||||||
if v != "sat" and v not in allowed_currencies():
|
if v != "sat" and v not in allowed_currencies():
|
||||||
raise ValueError("The provided unit is not supported")
|
raise ValueError("The provided unit is not supported")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class PaymentsStatusCount(BaseModel):
|
|
||||||
incoming: int = 0
|
|
||||||
outgoing: int = 0
|
|
||||||
failed: int = 0
|
|
||||||
pending: int = 0
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Keycloak SSO Login Helper"""
|
"""Keycloak SSO Login Helper
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|||||||
+41
-156
@@ -1,122 +1,50 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.core.models.misc import SimpleItem
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import is_valid_email_address, is_valid_pubkey, is_valid_username
|
||||||
is_valid_email_address,
|
|
||||||
is_valid_external_id,
|
|
||||||
is_valid_pubkey,
|
|
||||||
is_valid_username,
|
|
||||||
)
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .wallets import Wallet
|
from .wallets import Wallet
|
||||||
|
|
||||||
|
|
||||||
class UserExtra(BaseModel):
|
class UserExtra(BaseModel):
|
||||||
email_verified: bool | None = False
|
email_verified: Optional[bool] = False
|
||||||
first_name: str | None = None
|
first_name: Optional[str] = None
|
||||||
last_name: str | None = None
|
last_name: Optional[str] = None
|
||||||
display_name: str | None = None
|
display_name: Optional[str] = None
|
||||||
picture: str | None = None
|
picture: Optional[str] = None
|
||||||
# Auth provider, possible values:
|
# Auth provider, possible values:
|
||||||
# - "env": the user was created automatically by the system
|
# - "env": the user was created automatically by the system
|
||||||
# - "lnbits": the user was created via register form (username/pass or user_id only)
|
# - "lnbits": the user was created via register form (username/pass or user_id only)
|
||||||
# - "google | github | ...": the user was created using an SSO provider
|
# - "google | github | ...": the user was created using an SSO provider
|
||||||
provider: str | None = "lnbits" # auth provider
|
provider: Optional[str] = "lnbits" # auth provider
|
||||||
|
|
||||||
# how many wallets are shown in the user interface
|
|
||||||
visible_wallet_count: int | None = 10
|
|
||||||
|
|
||||||
|
|
||||||
class EndpointAccess(BaseModel):
|
|
||||||
path: str
|
|
||||||
name: str
|
|
||||||
read: bool = False
|
|
||||||
write: bool = False
|
|
||||||
|
|
||||||
def supports_method(self, method: str) -> bool:
|
|
||||||
# all http methods
|
|
||||||
if method in ["GET", "OPTIONS", "HEAD"]:
|
|
||||||
return self.read
|
|
||||||
if method in ["POST", "PUT", "PATCH", "DELETE"]:
|
|
||||||
return self.write
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class AccessControlList(BaseModel):
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
endpoints: list[EndpointAccess] = []
|
|
||||||
token_id_list: list[SimpleItem] = []
|
|
||||||
|
|
||||||
def get_endpoint(self, path: str) -> EndpointAccess | None:
|
|
||||||
for e in self.endpoints:
|
|
||||||
if e.path == path:
|
|
||||||
return e
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_token_by_id(self, token_id: str) -> SimpleItem | None:
|
|
||||||
for t in self.token_id_list:
|
|
||||||
if t.id == token_id:
|
|
||||||
return t
|
|
||||||
return None
|
|
||||||
|
|
||||||
def delete_token_by_id(self, token_id: str):
|
|
||||||
self.token_id_list = [t for t in self.token_id_list if t.id != token_id]
|
|
||||||
|
|
||||||
|
|
||||||
class UserAcls(BaseModel):
|
|
||||||
id: str
|
|
||||||
access_control_list: list[AccessControlList] = []
|
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
||||||
|
|
||||||
def get_acl_by_id(self, acl_id: str) -> AccessControlList | None:
|
|
||||||
for acl in self.access_control_list:
|
|
||||||
if acl.id == acl_id:
|
|
||||||
return acl
|
|
||||||
return None
|
|
||||||
|
|
||||||
def delete_acl_by_id(self, acl_id: str):
|
|
||||||
self.access_control_list = [
|
|
||||||
acl for acl in self.access_control_list if acl.id != acl_id
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_acl_by_token_id(self, token_id: str) -> AccessControlList | None:
|
|
||||||
for acl in self.access_control_list:
|
|
||||||
if acl.get_token_by_id(token_id):
|
|
||||||
return acl
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class Account(BaseModel):
|
class Account(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
external_id: str | None = None # for external account linking
|
username: Optional[str] = None
|
||||||
username: str | None = None
|
password_hash: Optional[str] = None
|
||||||
password_hash: str | None = None
|
pubkey: Optional[str] = None
|
||||||
pubkey: str | None = None
|
email: Optional[str] = None
|
||||||
email: str | None = None
|
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
is_super_user: bool = Field(default=False, no_database=True)
|
is_super_user: bool = Field(default=False, no_database=True)
|
||||||
is_admin: 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):
|
def __init__(self, **data):
|
||||||
super().__init__(**data)
|
super().__init__(**data)
|
||||||
self.is_super_user = settings.is_super_user(self.id)
|
self.is_super_user = settings.is_super_user(self.id)
|
||||||
self.is_admin = settings.is_admin_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:
|
def hash_password(self, password: str) -> str:
|
||||||
"""sets and returns the hashed password"""
|
"""sets and returns the hashed password"""
|
||||||
@@ -138,32 +66,20 @@ class Account(BaseModel):
|
|||||||
raise ValueError("Invalid email.")
|
raise ValueError("Invalid email.")
|
||||||
if self.pubkey and not is_valid_pubkey(self.pubkey):
|
if self.pubkey and not is_valid_pubkey(self.pubkey):
|
||||||
raise ValueError("Invalid 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)
|
user_uuid4 = UUID(hex=self.id, version=4)
|
||||||
if user_uuid4.hex != self.id:
|
if user_uuid4.hex != self.id:
|
||||||
raise ValueError("User ID is not valid UUID4 hex string.")
|
raise ValueError("User ID is not valid UUID4 hex string.")
|
||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
transaction_count: int | None = 0
|
transaction_count: Optional[int] = 0
|
||||||
wallet_count: int | None = 0
|
wallet_count: Optional[int] = 0
|
||||||
balance_msat: int | None = 0
|
balance_msat: Optional[int] = 0
|
||||||
last_payment: datetime | None = None
|
last_payment: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
class AccountFilters(FilterModel):
|
class AccountFilters(FilterModel):
|
||||||
__search_fields__ = [
|
__search_fields__ = ["user", "email", "username", "pubkey", "wallet_id"]
|
||||||
"user",
|
|
||||||
"email",
|
|
||||||
"username",
|
|
||||||
"pubkey",
|
|
||||||
"external_id",
|
|
||||||
"wallet_id",
|
|
||||||
]
|
|
||||||
__sort_fields__ = [
|
__sort_fields__ = [
|
||||||
"balance_msat",
|
"balance_msat",
|
||||||
"email",
|
"email",
|
||||||
@@ -173,27 +89,24 @@ class AccountFilters(FilterModel):
|
|||||||
"last_payment",
|
"last_payment",
|
||||||
]
|
]
|
||||||
|
|
||||||
email: str | None = None
|
email: Optional[str] = None
|
||||||
user: str | None = None
|
user: Optional[str] = None
|
||||||
username: str | None = None
|
username: Optional[str] = None
|
||||||
pubkey: str | None = None
|
pubkey: Optional[str] = None
|
||||||
external_id: str | None = None
|
wallet_id: Optional[str] = None
|
||||||
wallet_id: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
email: str | None = None
|
email: Optional[str] = None
|
||||||
username: str | None = None
|
username: Optional[str] = None
|
||||||
pubkey: str | None = None
|
pubkey: Optional[str] = None
|
||||||
external_id: str | None = None # for external account linking
|
|
||||||
extensions: list[str] = []
|
extensions: list[str] = []
|
||||||
wallets: list[Wallet] = []
|
wallets: list[Wallet] = []
|
||||||
admin: bool = False
|
admin: bool = False
|
||||||
super_user: bool = False
|
super_user: bool = False
|
||||||
fiat_providers: list[str] = []
|
|
||||||
has_password: bool = False
|
has_password: bool = False
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
|
||||||
@@ -201,7 +114,7 @@ class User(BaseModel):
|
|||||||
def wallet_ids(self) -> list[str]:
|
def wallet_ids(self) -> list[str]:
|
||||||
return [wallet.id for wallet in self.wallets]
|
return [wallet.id for wallet in self.wallets]
|
||||||
|
|
||||||
def get_wallet(self, wallet_id: str) -> Wallet | None:
|
def get_wallet(self, wallet_id: str) -> Optional[Wallet]:
|
||||||
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
||||||
return w[0] if w else None
|
return w[0] if w else None
|
||||||
|
|
||||||
@@ -217,33 +130,33 @@ class User(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class RegisterUser(BaseModel):
|
class RegisterUser(BaseModel):
|
||||||
email: str | None = Query(default=None)
|
email: Optional[str] = Query(default=None)
|
||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
|
||||||
|
|
||||||
class CreateUser(BaseModel):
|
class CreateUser(BaseModel):
|
||||||
id: str | None = Query(default=None)
|
id: Optional[str] = Query(default=None)
|
||||||
email: str | None = Query(default=None)
|
email: Optional[str] = Query(default=None)
|
||||||
username: str | None = Query(default=None, min_length=2, max_length=20)
|
username: Optional[str] = Query(default=None, min_length=2, max_length=20)
|
||||||
password: str | None = Query(default=None, min_length=8, max_length=50)
|
password: Optional[str] = Query(default=None, min_length=8, max_length=50)
|
||||||
password_repeat: str | None = Query(default=None, min_length=8, max_length=50)
|
password_repeat: Optional[str] = Query(default=None, min_length=8, max_length=50)
|
||||||
pubkey: str = Query(default=None, max_length=64)
|
pubkey: str = Query(default=None, max_length=64)
|
||||||
external_id: str = Query(default=None, max_length=256)
|
extensions: Optional[list[str]] = None
|
||||||
extensions: list[str] | None = None
|
extra: Optional[UserExtra] = None
|
||||||
extra: UserExtra | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateUser(BaseModel):
|
class UpdateUser(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
username: str | None = Query(default=..., min_length=2, max_length=20)
|
email: Optional[str] = Query(default=None)
|
||||||
extra: UserExtra | None = None
|
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
|
||||||
|
extra: Optional[UserExtra] = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserPassword(BaseModel):
|
class UpdateUserPassword(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
password_old: str | None = None
|
password_old: Optional[str] = None
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
@@ -277,39 +190,11 @@ class LoginUsernamePassword(BaseModel):
|
|||||||
|
|
||||||
class AccessTokenPayload(BaseModel):
|
class AccessTokenPayload(BaseModel):
|
||||||
sub: str
|
sub: str
|
||||||
usr: str | None = None
|
usr: Optional[str] = None
|
||||||
email: str | None = None
|
email: Optional[str] = None
|
||||||
auth_time: int | None = 0
|
auth_time: Optional[int] = 0
|
||||||
api_token_id: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateBalance(BaseModel):
|
class UpdateBalance(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
amount: int
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
class ApiTokenRequest(BaseModel):
|
|
||||||
acl_id: str
|
|
||||||
token_name: str
|
|
||||||
password: str
|
|
||||||
expiration_time_minutes: int
|
|
||||||
|
|
||||||
|
|
||||||
class ApiTokenResponse(BaseModel):
|
|
||||||
id: str
|
|
||||||
api_token: str
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateAccessControlList(AccessControlList):
|
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteAccessControlList(BaseModel):
|
|
||||||
id: str
|
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteTokenRequest(BaseModel):
|
|
||||||
id: str
|
|
||||||
acl_id: str
|
|
||||||
password: str
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import hmac
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from ecdsa import SECP256k1, SigningKey
|
from ecdsa import SECP256k1, SigningKey
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
|
||||||
from lnbits.helpers import url_for
|
from lnbits.helpers import url_for
|
||||||
from lnbits.lnurl import encode as lnurl_encode
|
from lnbits.lnurl import encode as lnurl_encode
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -23,12 +23,6 @@ class BaseWallet(BaseModel):
|
|||||||
balance_msat: int
|
balance_msat: int
|
||||||
|
|
||||||
|
|
||||||
class WalletExtra(BaseModel):
|
|
||||||
icon: str = "flash_on"
|
|
||||||
color: str = "primary"
|
|
||||||
pinned: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class Wallet(BaseModel):
|
class Wallet(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
user: str
|
user: str
|
||||||
@@ -38,9 +32,8 @@ class Wallet(BaseModel):
|
|||||||
deleted: bool = False
|
deleted: bool = False
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
currency: str | None = None
|
currency: Optional[str] = None
|
||||||
balance_msat: int = Field(default=0, no_database=True)
|
balance_msat: int = Field(default=0, no_database=True)
|
||||||
extra: WalletExtra = WalletExtra()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def balance(self) -> int:
|
def balance(self) -> int:
|
||||||
@@ -68,7 +61,7 @@ class Wallet(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CreateWallet(BaseModel):
|
class CreateWallet(BaseModel):
|
||||||
name: str | None = None
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class KeyType(Enum):
|
class KeyType(Enum):
|
||||||
@@ -85,13 +78,3 @@ class KeyType(Enum):
|
|||||||
class WalletTypeInfo:
|
class WalletTypeInfo:
|
||||||
key_type: KeyType
|
key_type: KeyType
|
||||||
wallet: Wallet
|
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
|
|
||||||
|
|||||||
@@ -3,20 +3,16 @@ from .funding_source import (
|
|||||||
switch_to_voidwallet,
|
switch_to_voidwallet,
|
||||||
)
|
)
|
||||||
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
||||||
from .notifications import enqueue_notification, send_payment_notification
|
|
||||||
from .payments import (
|
from .payments import (
|
||||||
calculate_fiat_amounts,
|
calculate_fiat_amounts,
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
check_wallet_limits,
|
check_wallet_limits,
|
||||||
create_fiat_invoice,
|
|
||||||
create_invoice,
|
create_invoice,
|
||||||
create_wallet_invoice,
|
|
||||||
fee_reserve,
|
fee_reserve,
|
||||||
fee_reserve_total,
|
fee_reserve_total,
|
||||||
get_payments_daily_stats,
|
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
|
send_payment_notification,
|
||||||
service_fee,
|
service_fee,
|
||||||
update_pending_payment,
|
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
update_wallet_balance,
|
update_wallet_balance,
|
||||||
)
|
)
|
||||||
@@ -28,39 +24,42 @@ from .users import (
|
|||||||
check_admin_settings,
|
check_admin_settings,
|
||||||
create_user_account,
|
create_user_account,
|
||||||
create_user_account_no_ckeck,
|
create_user_account_no_ckeck,
|
||||||
|
init_admin_settings,
|
||||||
update_user_account,
|
update_user_account,
|
||||||
update_user_extensions,
|
update_user_extensions,
|
||||||
)
|
)
|
||||||
from .websockets import websocket_manager, websocket_updater
|
from .websockets import websocket_manager, websocket_updater
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
# funding source
|
||||||
|
"get_balance_delta",
|
||||||
|
"switch_to_voidwallet",
|
||||||
|
# lnurl
|
||||||
|
"redeem_lnurl_withdraw",
|
||||||
|
"perform_lnurlauth",
|
||||||
|
# payments
|
||||||
"calculate_fiat_amounts",
|
"calculate_fiat_amounts",
|
||||||
"check_admin_settings",
|
|
||||||
"check_transaction_status",
|
"check_transaction_status",
|
||||||
"check_wallet_limits",
|
"check_wallet_limits",
|
||||||
"check_webpush_settings",
|
|
||||||
"create_fiat_invoice",
|
|
||||||
"create_invoice",
|
"create_invoice",
|
||||||
"create_user_account",
|
|
||||||
"create_user_account_no_ckeck",
|
|
||||||
"create_wallet_invoice",
|
|
||||||
"enqueue_notification",
|
|
||||||
"fee_reserve",
|
"fee_reserve",
|
||||||
"fee_reserve_total",
|
"fee_reserve_total",
|
||||||
"get_balance_delta",
|
|
||||||
"get_payments_daily_stats",
|
|
||||||
"pay_invoice",
|
"pay_invoice",
|
||||||
"perform_lnurlauth",
|
|
||||||
"redeem_lnurl_withdraw",
|
|
||||||
"send_payment_notification",
|
"send_payment_notification",
|
||||||
"service_fee",
|
"service_fee",
|
||||||
"switch_to_voidwallet",
|
|
||||||
"update_cached_settings",
|
|
||||||
"update_pending_payment",
|
|
||||||
"update_pending_payments",
|
"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",
|
||||||
|
"init_admin_settings",
|
||||||
"update_user_account",
|
"update_user_account",
|
||||||
"update_user_extensions",
|
"update_user_extensions",
|
||||||
"update_wallet_balance",
|
# websockets
|
||||||
"websocket_manager",
|
"websocket_manager",
|
||||||
"websocket_updater",
|
"websocket_updater",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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}.",
|
|
||||||
)
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.models.notifications import NotificationType
|
|
||||||
from lnbits.core.services.notifications import enqueue_notification
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets import get_funding_source, set_funding_source
|
from lnbits.wallets import get_funding_source, set_funding_source
|
||||||
|
|
||||||
@@ -22,62 +18,6 @@ async def get_balance_delta() -> BalanceDelta:
|
|||||||
status = await funding_source.status()
|
status = await funding_source.status()
|
||||||
lnbits_balance = await get_total_balance()
|
lnbits_balance = await get_total_balance()
|
||||||
return BalanceDelta(
|
return BalanceDelta(
|
||||||
lnbits_balance_sats=int(lnbits_balance) // 1000,
|
lnbits_balance_msats=lnbits_balance,
|
||||||
node_balance_sats=status.balance_msat // 1000,
|
node_balance_msats=status.balance_msat,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def check_server_balance_against_node():
|
|
||||||
"""
|
|
||||||
Watchdog will check lnbits balance and nodebalance
|
|
||||||
and will switch to VoidWallet if the watchdog delta is reached.
|
|
||||||
"""
|
|
||||||
if (
|
|
||||||
not settings.lnbits_watchdog_switch_to_voidwallet
|
|
||||||
and not settings.lnbits_notification_watchdog
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
funding_source = get_funding_source()
|
|
||||||
if funding_source.__class__.__name__ == "VoidWallet":
|
|
||||||
return
|
|
||||||
|
|
||||||
status = await get_balance_delta()
|
|
||||||
if status.delta_sats < settings.lnbits_watchdog_delta:
|
|
||||||
return
|
|
||||||
|
|
||||||
use_voidwallet = settings.lnbits_watchdog_switch_to_voidwallet
|
|
||||||
logger.warning(
|
|
||||||
f"Balance delta reached: {status.delta_sats} sats."
|
|
||||||
f" Switch to void wallet: {use_voidwallet}."
|
|
||||||
)
|
|
||||||
enqueue_notification(
|
|
||||||
NotificationType.watchdog_check,
|
|
||||||
{
|
|
||||||
"delta_sats": status.delta_sats,
|
|
||||||
"lnbits_balance_sats": status.lnbits_balance_sats,
|
|
||||||
"node_balance_sats": status.node_balance_sats,
|
|
||||||
"switch_to_void_wallet": use_voidwallet,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if use_voidwallet:
|
|
||||||
logger.error(f"Switching to VoidWallet. Delta: {status.delta_sats} sats.")
|
|
||||||
await switch_to_voidwallet()
|
|
||||||
|
|
||||||
|
|
||||||
async def check_balance_delta_changed():
|
|
||||||
status = await get_balance_delta()
|
|
||||||
if settings.latest_balance_delta_sats is None:
|
|
||||||
settings.latest_balance_delta_sats = status.delta_sats
|
|
||||||
return
|
|
||||||
if status.delta_sats != settings.latest_balance_delta_sats:
|
|
||||||
enqueue_notification(
|
|
||||||
NotificationType.balance_delta,
|
|
||||||
{
|
|
||||||
"delta_sats": status.delta_sats,
|
|
||||||
"old_delta_sats": settings.latest_balance_delta_sats,
|
|
||||||
"lnbits_balance_sats": status.lnbits_balance_sats,
|
|
||||||
"node_balance_sats": status.node_balance_sats,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
settings.latest_balance_delta_sats = status.delta_sats
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from lnbits.decorators import (
|
|||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import check_callback_url, url_for
|
from lnbits.helpers import url_for
|
||||||
from lnbits.lnurl import LnurlErrorResponse
|
from lnbits.lnurl import LnurlErrorResponse
|
||||||
from lnbits.lnurl import decode as decode_lnurl
|
from lnbits.lnurl import decode as decode_lnurl
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -37,7 +37,6 @@ async def redeem_lnurl_withdraw(
|
|||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
lnurl = decode_lnurl(lnurl_request)
|
lnurl = decode_lnurl(lnurl_request)
|
||||||
check_callback_url(str(lnurl))
|
|
||||||
r = await client.get(str(lnurl))
|
r = await client.get(str(lnurl))
|
||||||
res = r.json()
|
res = r.json()
|
||||||
|
|
||||||
@@ -73,7 +72,6 @@ async def redeem_lnurl_withdraw(
|
|||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
try:
|
try:
|
||||||
check_callback_url(res["callback"])
|
|
||||||
await client.get(res["callback"], params=params)
|
await client.get(res["callback"], params=params)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -137,7 +135,6 @@ async def perform_lnurlauth(
|
|||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
assert key.verifying_key, "LNURLauth verifying_key does not exist"
|
assert key.verifying_key, "LNURLauth verifying_key does not exist"
|
||||||
check_callback_url(callback)
|
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
callback,
|
callback,
|
||||||
params={
|
params={
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from loguru import logger
|
|
||||||
from pynostr.encrypted_dm import EncryptedDirectMessage
|
|
||||||
from websocket import WebSocket, create_connection
|
|
||||||
|
|
||||||
from lnbits.core.helpers import is_valid_url
|
|
||||||
from lnbits.utils.nostr import (
|
|
||||||
validate_identifier,
|
|
||||||
validate_pub_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_nostr_dm(
|
|
||||||
from_private_key_hex: str,
|
|
||||||
to_pubkey_hex: str,
|
|
||||||
message: str,
|
|
||||||
relays: list[str],
|
|
||||||
) -> dict:
|
|
||||||
dm = EncryptedDirectMessage()
|
|
||||||
dm.encrypt(
|
|
||||||
private_key_hex=from_private_key_hex,
|
|
||||||
recipient_pubkey=to_pubkey_hex,
|
|
||||||
cleartext_content=message,
|
|
||||||
)
|
|
||||||
|
|
||||||
dm_event = dm.to_event()
|
|
||||||
dm_event.sign(private_key_hex=from_private_key_hex)
|
|
||||||
notification = dm_event.to_message()
|
|
||||||
|
|
||||||
ws_connections: list[WebSocket] = []
|
|
||||||
for relay in relays:
|
|
||||||
try:
|
|
||||||
ws = create_connection(relay, timeout=2)
|
|
||||||
ws.send(notification)
|
|
||||||
ws_connections.append(ws)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error sending notification to relay {relay}: {e}")
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
for ws in ws_connections:
|
|
||||||
try:
|
|
||||||
ws.close()
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Failed to close websocket connection: {e}")
|
|
||||||
|
|
||||||
return dm_event.to_dict()
|
|
||||||
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
if not is_valid_url(f"https://{domain}"):
|
|
||||||
raise ValueError("Invalid NIP5 domain")
|
|
||||||
|
|
||||||
validate_identifier(identifier)
|
|
||||||
|
|
||||||
url = f"https://{domain}/.well-known/nostr.json?name={identifier}"
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
resp = await client.get(url)
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
|
||||||
if "names" not in data or identifier not in data["names"]:
|
|
||||||
raise ValueError("NIP5 not name found")
|
|
||||||
pubkey = data["names"][identifier]
|
|
||||||
validate_pub_key(pubkey)
|
|
||||||
|
|
||||||
relays = data["relays"].get(pubkey, []) if "relays" in data else []
|
|
||||||
|
|
||||||
return pubkey, relays
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import smtplib
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
from http import HTTPStatus
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from loguru import logger
|
|
||||||
from py_vapid import Vapid
|
|
||||||
from pywebpush import WebPushException, webpush
|
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
|
||||||
delete_webpush_subscriptions,
|
|
||||||
get_webpush_subscriptions_for_user,
|
|
||||||
mark_webhook_sent,
|
|
||||||
)
|
|
||||||
from lnbits.core.models import Payment, Wallet
|
|
||||||
from lnbits.core.models.notifications import (
|
|
||||||
NOTIFICATION_TEMPLATES,
|
|
||||||
NotificationMessage,
|
|
||||||
NotificationType,
|
|
||||||
)
|
|
||||||
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
|
||||||
from lnbits.core.services.websockets import websocket_manager
|
|
||||||
from lnbits.helpers import check_callback_url, is_valid_email_address
|
|
||||||
from lnbits.settings import settings
|
|
||||||
from lnbits.utils.nostr import normalize_private_key
|
|
||||||
|
|
||||||
notifications_queue: asyncio.Queue = asyncio.Queue()
|
|
||||||
|
|
||||||
|
|
||||||
def enqueue_notification(message_type: NotificationType, values: dict) -> None:
|
|
||||||
if not is_message_type_enabled(message_type):
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
notifications_queue.put_nowait(
|
|
||||||
NotificationMessage(message_type=message_type, values=values)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error enqueuing notification: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
async def process_next_notification():
|
|
||||||
notification_message: NotificationMessage = await notifications_queue.get()
|
|
||||||
message_type, text = _notification_message_to_text(notification_message)
|
|
||||||
await send_notification(text, message_type)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_notification(
|
|
||||||
message: str,
|
|
||||||
message_type: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
try:
|
|
||||||
if settings.lnbits_telegram_notifications_enabled:
|
|
||||||
await send_telegram_notification(message)
|
|
||||||
logger.debug(f"Sent telegram notification: {message_type}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error sending telegram notification {message_type}: {e}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
if settings.lnbits_nostr_notifications_enabled:
|
|
||||||
await send_nostr_notification(message)
|
|
||||||
logger.debug(f"Sent nostr notification: {message_type}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error sending nostr notification {message_type}: {e}")
|
|
||||||
try:
|
|
||||||
if settings.lnbits_email_notifications_enabled:
|
|
||||||
await send_email_notification(message)
|
|
||||||
logger.debug(f"Sent email notification: {message_type}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error sending email notification {message_type}: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
async def send_nostr_notification(message: str) -> dict:
|
|
||||||
for i in settings.lnbits_nostr_notifications_identifiers:
|
|
||||||
try:
|
|
||||||
identifier = await fetch_nip5_details(i)
|
|
||||||
user_pubkey = identifier[0]
|
|
||||||
relays = identifier[1]
|
|
||||||
server_private_key = normalize_private_key(
|
|
||||||
settings.lnbits_nostr_notifications_private_key
|
|
||||||
)
|
|
||||||
await send_nostr_dm(
|
|
||||||
server_private_key,
|
|
||||||
user_pubkey,
|
|
||||||
message,
|
|
||||||
relays,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error notifying identifier {i}: {e}")
|
|
||||||
|
|
||||||
return {"status": "ok"}
|
|
||||||
|
|
||||||
|
|
||||||
async def send_telegram_notification(message: str) -> dict:
|
|
||||||
return await send_telegram_message(
|
|
||||||
settings.lnbits_telegram_notifications_access_token,
|
|
||||||
settings.lnbits_telegram_notifications_chat_id,
|
|
||||||
message,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
|
|
||||||
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
|
||||||
payload = {"chat_id": chat_id, "text": message, "parse_mode": "markdown"}
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(url, data=payload)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
|
|
||||||
async def send_email_notification(
|
|
||||||
message: str, subject: str = "LNbits Notification"
|
|
||||||
) -> dict:
|
|
||||||
if not settings.lnbits_email_notifications_enabled:
|
|
||||||
return {"status": "error", "message": "Email notifications are disabled"}
|
|
||||||
try:
|
|
||||||
await send_email(
|
|
||||||
settings.lnbits_email_notifications_server,
|
|
||||||
settings.lnbits_email_notifications_port,
|
|
||||||
settings.lnbits_email_notifications_username,
|
|
||||||
settings.lnbits_email_notifications_password,
|
|
||||||
settings.lnbits_email_notifications_email,
|
|
||||||
settings.lnbits_email_notifications_to_emails,
|
|
||||||
subject,
|
|
||||||
message,
|
|
||||||
)
|
|
||||||
return {"status": "ok"}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error sending email notification: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
async def send_email(
|
|
||||||
server: str,
|
|
||||||
port: int,
|
|
||||||
username: str,
|
|
||||||
password: str,
|
|
||||||
from_email: str,
|
|
||||||
to_emails: list[str],
|
|
||||||
subject: str,
|
|
||||||
message: str,
|
|
||||||
) -> bool:
|
|
||||||
if not is_valid_email_address(from_email):
|
|
||||||
raise ValueError(f"Invalid from email address: {from_email}")
|
|
||||||
if len(to_emails) == 0:
|
|
||||||
raise ValueError("No email addresses provided")
|
|
||||||
for email in to_emails:
|
|
||||||
if not is_valid_email_address(email):
|
|
||||||
raise ValueError(f"Invalid email address: {email}")
|
|
||||||
msg = MIMEMultipart()
|
|
||||||
msg["From"] = from_email
|
|
||||||
msg["To"] = ", ".join(to_emails)
|
|
||||||
msg["Subject"] = subject
|
|
||||||
msg.attach(MIMEText(message, "plain"))
|
|
||||||
username = username if len(username) > 0 else from_email
|
|
||||||
with smtplib.SMTP(server, port) as smtp_server:
|
|
||||||
smtp_server.starttls()
|
|
||||||
smtp_server.login(username, password)
|
|
||||||
smtp_server.sendmail(from_email, to_emails, msg.as_string())
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
|
||||||
if message_type == NotificationType.balance_update:
|
|
||||||
return settings.lnbits_notification_credit_debit
|
|
||||||
if message_type == NotificationType.settings_update:
|
|
||||||
return settings.lnbits_notification_settings_update
|
|
||||||
if message_type == NotificationType.watchdog_check:
|
|
||||||
return settings.lnbits_notification_watchdog
|
|
||||||
if message_type == NotificationType.balance_delta:
|
|
||||||
return settings.notification_balance_delta_changed
|
|
||||||
if message_type == NotificationType.server_start_stop:
|
|
||||||
return settings.lnbits_notification_server_start_stop
|
|
||||||
if message_type == NotificationType.server_status:
|
|
||||||
return settings.lnbits_notification_server_status_hours > 0
|
|
||||||
if message_type == NotificationType.incoming_payment:
|
|
||||||
return settings.lnbits_notification_incoming_payment_amount_sats > 0
|
|
||||||
if message_type == NotificationType.outgoing_payment:
|
|
||||||
return settings.lnbits_notification_outgoing_payment_amount_sats > 0
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _notification_message_to_text(
|
|
||||||
notification_message: NotificationMessage,
|
|
||||||
) -> tuple[str, str]:
|
|
||||||
message_type = notification_message.message_type.value
|
|
||||||
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
|
|
||||||
try:
|
|
||||||
text = meesage_value.format(**notification_message.values)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error formatting notification message: {e}")
|
|
||||||
text = meesage_value
|
|
||||||
text = f"""[{settings.lnbits_site_title}]\n{text}"""
|
|
||||||
return message_type, text
|
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_webhook(payment: Payment):
|
|
||||||
"""
|
|
||||||
Dispatches the webhook to the webhook url.
|
|
||||||
"""
|
|
||||||
logger.debug("sending webhook", payment.webhook)
|
|
||||||
|
|
||||||
if not payment.webhook:
|
|
||||||
return await mark_webhook_sent(payment.payment_hash, "-1")
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
|
||||||
try:
|
|
||||||
check_callback_url(payment.webhook)
|
|
||||||
r = await client.post(payment.webhook, json=payment.json(), timeout=40)
|
|
||||||
r.raise_for_status()
|
|
||||||
await mark_webhook_sent(payment.payment_hash, str(r.status_code))
|
|
||||||
except httpx.HTTPStatusError as exc:
|
|
||||||
await mark_webhook_sent(payment.payment_hash, str(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")
|
|
||||||
logger.warning(f"Could not send webhook to {payment.webhook}")
|
|
||||||
|
|
||||||
|
|
||||||
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}")
|
|
||||||
try:
|
|
||||||
send_chat_payment_notification(wallet, payment)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error sending chat payment notification {e!s}")
|
|
||||||
try:
|
|
||||||
await send_payment_push_notification(wallet, payment)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error sending push payment notification {e!s}")
|
|
||||||
|
|
||||||
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}")
|
|
||||||
|
|
||||||
|
|
||||||
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
|
||||||
# TODO: websocket message should be a clean payment model
|
|
||||||
# await websocket_manager.send_data(payment.json(), wallet.inkey)
|
|
||||||
# TODO: figure out why we send the balance with the payment here.
|
|
||||||
# cleaner would be to have a separate message for the balance
|
|
||||||
# and send it with the id of the wallet so wallets can subscribe to it
|
|
||||||
payment_notification = json.dumps(
|
|
||||||
{
|
|
||||||
"wallet_balance": wallet.balance,
|
|
||||||
# use pydantic json serialization to get the correct datetime format
|
|
||||||
"payment": json.loads(payment.json()),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await websocket_manager.send_data(payment_notification, wallet.inkey)
|
|
||||||
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
|
||||||
|
|
||||||
await websocket_manager.send_data(
|
|
||||||
json.dumps({"pending": payment.pending, "status": payment.status}),
|
|
||||||
payment.payment_hash,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
|
||||||
amount_sats = abs(payment.sat)
|
|
||||||
values: dict = {
|
|
||||||
"wallet_id": wallet.id,
|
|
||||||
"wallet_name": wallet.name,
|
|
||||||
"amount_sats": amount_sats,
|
|
||||||
"fiat_value_fmt": "",
|
|
||||||
}
|
|
||||||
if payment.extra.get("wallet_fiat_currency", None):
|
|
||||||
amount_fiat = payment.extra.get("wallet_fiat_amount", None)
|
|
||||||
currency = payment.extra.get("wallet_fiat_currency", None)
|
|
||||||
values["fiat_value_fmt"] = f"`{amount_fiat}`*{currency}* / "
|
|
||||||
|
|
||||||
if payment.is_out:
|
|
||||||
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
|
|
||||||
enqueue_notification(NotificationType.outgoing_payment, values)
|
|
||||||
else:
|
|
||||||
if amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
|
|
||||||
enqueue_notification(NotificationType.incoming_payment, values)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_payment_push_notification(wallet: Wallet, payment: Payment):
|
|
||||||
subscriptions = await get_webpush_subscriptions_for_user(wallet.user)
|
|
||||||
|
|
||||||
amount = int(payment.amount / 1000)
|
|
||||||
|
|
||||||
title = f"LNbits: {wallet.name}"
|
|
||||||
body = f"You just received {amount} sat{'s'[:amount^1]}!"
|
|
||||||
|
|
||||||
if payment.memo:
|
|
||||||
body += f"\r\n{payment.memo}"
|
|
||||||
|
|
||||||
for subscription in subscriptions:
|
|
||||||
# todo: review permissions when user-id-only not allowed
|
|
||||||
# todo: replace all this logic with websockets?
|
|
||||||
url = f"https://{subscription.host}/wallet?usr={wallet.user}&wal={wallet.id}"
|
|
||||||
await send_push_notification(subscription, title, body, url)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_push_notification(subscription, title, body, url=""):
|
|
||||||
vapid = Vapid()
|
|
||||||
try:
|
|
||||||
logger.debug("sending push notification")
|
|
||||||
webpush(
|
|
||||||
json.loads(subscription.data),
|
|
||||||
json.dumps({"title": title, "body": body, "url": url}),
|
|
||||||
(
|
|
||||||
vapid.from_pem(bytes(settings.lnbits_webpush_privkey, "utf-8"))
|
|
||||||
if settings.lnbits_webpush_privkey
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
{"aud": "", "sub": "mailto:alan@lnbits.com"},
|
|
||||||
)
|
|
||||||
except WebPushException as e:
|
|
||||||
if e.response and e.response.status_code == HTTPStatus.GONE:
|
|
||||||
# cleanup unsubscribed or expired push subscriptions
|
|
||||||
await delete_webpush_subscriptions(subscription.endpoint)
|
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
f"failed sending push notification: "
|
|
||||||
f"{e.response.text if e.response else e}"
|
|
||||||
)
|
|
||||||
+109
-385
@@ -1,26 +1,17 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
|
||||||
from bolt11 import Bolt11, MilliSatoshi, Tags
|
from bolt11 import Bolt11, MilliSatoshi, Tags
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from bolt11 import encode as bolt11_encode
|
from bolt11 import encode as bolt11_encode
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud.payments import get_daily_stats
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
from lnbits.db import Connection
|
||||||
from lnbits.core.models.payments import CreateInvoice
|
|
||||||
from lnbits.db import Connection, Filters
|
|
||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError
|
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.settings import settings
|
||||||
from lnbits.tasks import create_task, internal_invoice_queue_put
|
|
||||||
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
|
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.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
||||||
from lnbits.wallets import fake_wallet, get_funding_source
|
from lnbits.wallets import fake_wallet, get_funding_source
|
||||||
@@ -47,10 +38,7 @@ from ..models import (
|
|||||||
PaymentState,
|
PaymentState,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from .notifications import send_payment_notification
|
from .websockets import websocket_manager
|
||||||
|
|
||||||
payment_lock = asyncio.Lock()
|
|
||||||
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
|
||||||
|
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
@@ -63,16 +51,14 @@ async def pay_invoice(
|
|||||||
tag: str = "",
|
tag: str = "",
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
if settings.lnbits_only_allow_incoming_payments:
|
|
||||||
raise PaymentError("Only incoming payments allowed.", status="failed")
|
|
||||||
invoice = _validate_payment_request(payment_request, max_sat)
|
invoice = _validate_payment_request(payment_request, max_sat)
|
||||||
assert invoice.amount_msat
|
assert invoice.amount_msat
|
||||||
|
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
amount_msat = invoice.amount_msat
|
amount_msat = invoice.amount_msat
|
||||||
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
|
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, conn)
|
||||||
|
|
||||||
if await is_internal_status_success(invoice.payment_hash, new_conn):
|
if await is_internal_status_success(invoice.payment_hash, conn):
|
||||||
raise PaymentError("Internal invoice already paid.", status="failed")
|
raise PaymentError("Internal invoice already paid.", status="failed")
|
||||||
|
|
||||||
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
|
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
|
||||||
@@ -87,132 +73,10 @@ async def pay_invoice(
|
|||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
payment = await _pay_invoice(wallet, create_payment_model, conn)
|
||||||
|
await _credit_service_fee_wallet(payment, conn)
|
||||||
|
|
||||||
service_fee_memo = f"""
|
return payment
|
||||||
Service fee for payment of {abs(payment.sat)} sats.
|
|
||||||
Wallet: '{wallet.name}' ({wallet.id})."""
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
@@ -236,7 +100,7 @@ async def create_invoice(
|
|||||||
if not user_wallet:
|
if not user_wallet:
|
||||||
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
|
|
||||||
invoice_memo = None if description_hash else memo[:640]
|
invoice_memo = None if description_hash else memo
|
||||||
|
|
||||||
# use the fake wallet if the invoice is for internal use only
|
# use the fake wallet if the invoice is for internal use only
|
||||||
funding_source = fake_wallet if internal else get_funding_source()
|
funding_source = fake_wallet if internal else get_funding_source()
|
||||||
@@ -245,12 +109,6 @@ async def create_invoice(
|
|||||||
amount, user_wallet, currency, extra
|
amount, user_wallet, currency, extra
|
||||||
)
|
)
|
||||||
|
|
||||||
if amount_sat > settings.lnbits_max_incoming_payment_amount_sats:
|
|
||||||
raise InvoiceError(
|
|
||||||
f"Invoice amount {amount_sat} sats is too high. Max allowed: "
|
|
||||||
f"{settings.lnbits_max_incoming_payment_amount_sats} sats.",
|
|
||||||
status="failed",
|
|
||||||
)
|
|
||||||
if settings.is_wallet_max_balance_exceeded(
|
if settings.is_wallet_max_balance_exceeded(
|
||||||
user_wallet.balance_msat / 1000 + amount_sat
|
user_wallet.balance_msat / 1000 + amount_sat
|
||||||
):
|
):
|
||||||
@@ -260,29 +118,29 @@ async def create_invoice(
|
|||||||
status="failed",
|
status="failed",
|
||||||
)
|
)
|
||||||
|
|
||||||
payment_response = await funding_source.create_invoice(
|
(
|
||||||
|
ok,
|
||||||
|
checking_id,
|
||||||
|
payment_request,
|
||||||
|
error_message,
|
||||||
|
) = await funding_source.create_invoice(
|
||||||
amount=amount_sat,
|
amount=amount_sat,
|
||||||
memo=invoice_memo,
|
memo=invoice_memo,
|
||||||
description_hash=description_hash,
|
description_hash=description_hash,
|
||||||
unhashed_description=unhashed_description,
|
unhashed_description=unhashed_description,
|
||||||
expiry=expiry or settings.lightning_invoice_expiry,
|
expiry=expiry or settings.lightning_invoice_expiry,
|
||||||
)
|
)
|
||||||
if (
|
if not ok or not payment_request or not checking_id:
|
||||||
not payment_response.ok
|
|
||||||
or not payment_response.payment_request
|
|
||||||
or not payment_response.checking_id
|
|
||||||
):
|
|
||||||
raise InvoiceError(
|
raise InvoiceError(
|
||||||
message=payment_response.error_message or "unexpected backend error.",
|
error_message or "unexpected backend error.", status="pending"
|
||||||
status="pending",
|
|
||||||
)
|
)
|
||||||
invoice = bolt11_decode(payment_response.payment_request)
|
|
||||||
|
invoice = bolt11_decode(payment_request)
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
bolt11=payment_response.payment_request,
|
bolt11=payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
preimage=payment_response.preimage,
|
|
||||||
amount_msat=amount_sat * 1000,
|
amount_msat=amount_sat * 1000,
|
||||||
expiry=invoice.expiry_date,
|
expiry=invoice.expiry_date,
|
||||||
memo=memo,
|
memo=memo,
|
||||||
@@ -291,7 +149,7 @@ async def create_invoice(
|
|||||||
)
|
)
|
||||||
|
|
||||||
payment = await create_payment(
|
payment = await create_payment(
|
||||||
checking_id=payment_response.checking_id,
|
checking_id=checking_id,
|
||||||
data=create_payment_model,
|
data=create_payment_model,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
)
|
)
|
||||||
@@ -306,20 +164,13 @@ async def update_pending_payments(wallet_id: str):
|
|||||||
exclude_uncheckable=True,
|
exclude_uncheckable=True,
|
||||||
)
|
)
|
||||||
for payment in pending_payments:
|
for payment in pending_payments:
|
||||||
await update_pending_payment(payment)
|
status = await payment.check_status()
|
||||||
|
if status.failed:
|
||||||
|
payment.status = PaymentState.FAILED
|
||||||
async def update_pending_payment(payment: Payment) -> bool:
|
await update_payment(payment)
|
||||||
status = await payment.check_status()
|
elif status.success:
|
||||||
if status.failed:
|
payment.status = PaymentState.SUCCESS
|
||||||
payment.status = PaymentState.FAILED
|
await update_payment(payment)
|
||||||
await update_payment(payment)
|
|
||||||
return True
|
|
||||||
if status.success:
|
|
||||||
payment.status = PaymentState.SUCCESS
|
|
||||||
await update_payment(payment)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
||||||
@@ -327,7 +178,6 @@ def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def fee_reserve(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)
|
return settings.fee_reserve(amount_msat, internal)
|
||||||
|
|
||||||
|
|
||||||
@@ -347,26 +197,6 @@ def service_fee(amount_msat: int, internal: bool = False) -> int:
|
|||||||
return 0
|
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(
|
async def update_wallet_balance(
|
||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
amount: int,
|
amount: int,
|
||||||
@@ -425,7 +255,31 @@ async def update_wallet_balance(
|
|||||||
)
|
)
|
||||||
payment.status = PaymentState.SUCCESS
|
payment.status = PaymentState.SUCCESS
|
||||||
await update_payment(payment, conn=conn)
|
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 send_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
|
# TODO: websocket message should be a clean payment model
|
||||||
|
# await websocket_manager.send_data(payment.json(), wallet.inkey)
|
||||||
|
# TODO: figure out why we send the balance with the payment here.
|
||||||
|
# cleaner would be to have a separate message for the balance
|
||||||
|
# and send it with the id of the wallet so wallets can subscribe to it
|
||||||
|
payment_notification = json.dumps(
|
||||||
|
{
|
||||||
|
"wallet_balance": wallet.balance,
|
||||||
|
# use pydantic json serialization to get the correct datetime format
|
||||||
|
"payment": json.loads(payment.json()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await websocket_manager.send_data(payment_notification, wallet.inkey)
|
||||||
|
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
||||||
|
|
||||||
|
await websocket_manager.send_data(
|
||||||
|
json.dumps({"pending": payment.pending}), payment.payment_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def check_wallet_limits(
|
async def check_wallet_limits(
|
||||||
@@ -500,22 +354,17 @@ async def calculate_fiat_amounts(
|
|||||||
fiat_amounts["fiat_currency"] = currency
|
fiat_amounts["fiat_currency"] = currency
|
||||||
fiat_amounts["fiat_amount"] = round(amount, ndigits=3)
|
fiat_amounts["fiat_amount"] = round(amount, ndigits=3)
|
||||||
fiat_amounts["fiat_rate"] = amount_sat / amount
|
fiat_amounts["fiat_rate"] = amount_sat / amount
|
||||||
fiat_amounts["btc_rate"] = (amount / amount_sat) * 100_000_000
|
|
||||||
else:
|
else:
|
||||||
amount_sat = int(amount)
|
amount_sat = int(amount)
|
||||||
|
|
||||||
if wallet_currency:
|
if wallet_currency:
|
||||||
try:
|
if wallet_currency == currency:
|
||||||
if wallet_currency == currency:
|
fiat_amount = amount
|
||||||
fiat_amount = amount
|
else:
|
||||||
else:
|
fiat_amount = await satoshis_amount_as_fiat(amount_sat, wallet_currency)
|
||||||
fiat_amount = await satoshis_amount_as_fiat(amount_sat, wallet_currency)
|
fiat_amounts["wallet_fiat_currency"] = wallet_currency
|
||||||
fiat_amounts["wallet_fiat_currency"] = wallet_currency
|
fiat_amounts["wallet_fiat_amount"] = round(fiat_amount, ndigits=3)
|
||||||
fiat_amounts["wallet_fiat_amount"] = round(fiat_amount, ndigits=3)
|
fiat_amounts["wallet_fiat_rate"] = amount_sat / fiat_amount
|
||||||
fiat_amounts["wallet_fiat_rate"] = amount_sat / fiat_amount
|
|
||||||
fiat_amounts["wallet_btc_rate"] = (fiat_amount / amount_sat) * 100_000_000
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error calculating fiat amount for wallet '{wallet.id}': {e}")
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Calculated fiat amounts {wallet.id=} {amount=} {currency=}: {fiat_amounts=}"
|
f"Calculated fiat amounts {wallet.id=} {amount=} {currency=}: {fiat_amounts=}"
|
||||||
@@ -539,75 +388,11 @@ async def check_transaction_status(
|
|||||||
return await payment.check_status()
|
return await payment.check_status()
|
||||||
|
|
||||||
|
|
||||||
async def get_payments_daily_stats(
|
async def _pay_invoice(wallet, create_payment_model, conn):
|
||||||
filters: Filters[PaymentFilters],
|
payment = await _pay_internal_invoice(wallet, create_payment_model, conn)
|
||||||
user_id: Optional[str] = None,
|
if not payment:
|
||||||
) -> list[PaymentDailyStats]:
|
payment = await _pay_external_invoice(wallet, create_payment_model, conn)
|
||||||
data_in, data_out = await get_daily_stats(filters, user_id=user_id)
|
return payment
|
||||||
balance_total: float = 0
|
|
||||||
|
|
||||||
_none = PaymentDailyStats(date=datetime.now(timezone.utc))
|
|
||||||
if len(data_in) == 0 and len(data_out) == 0:
|
|
||||||
return []
|
|
||||||
if len(data_in) == 0:
|
|
||||||
data_in = [_none]
|
|
||||||
if len(data_out) == 0:
|
|
||||||
data_out = [_none]
|
|
||||||
|
|
||||||
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))
|
|
||||||
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)
|
|
||||||
|
|
||||||
balance_total += data_in_point.balance + data_out_point.balance
|
|
||||||
data.append(
|
|
||||||
PaymentDailyStats(
|
|
||||||
date=start_date,
|
|
||||||
balance=balance_total // 1000,
|
|
||||||
balance_in=data_in_point.balance // 1000,
|
|
||||||
balance_out=data_out_point.balance // 1000,
|
|
||||||
payments_count=data_in_point.payments_count
|
|
||||||
+ data_out_point.payments_count,
|
|
||||||
count_in=data_in_point.payments_count,
|
|
||||||
count_out=data_out_point.payments_count,
|
|
||||||
fee=(data_in_point.fee + data_out_point.fee) // 1000,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date += delta
|
|
||||||
|
|
||||||
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_internal_invoice(
|
async def _pay_internal_invoice(
|
||||||
@@ -642,15 +427,12 @@ async def _pay_internal_invoice(
|
|||||||
):
|
):
|
||||||
raise PaymentError("Invalid invoice. Bolt11 changed.", status="failed")
|
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)
|
create_payment_model.fee = abs(fee_reserve_total_msat)
|
||||||
|
|
||||||
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
|
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
|
||||||
raise PaymentError("Insufficient balance.", status="failed")
|
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}"
|
internal_id = f"internal_{create_payment_model.payment_hash}"
|
||||||
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
||||||
payment = await create_payment(
|
payment = await create_payment(
|
||||||
@@ -688,8 +470,6 @@ async def _pay_external_invoice(
|
|||||||
|
|
||||||
fee_reserve_total_msat = fee_reserve_total(amount_msat, internal=False)
|
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:
|
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
|
||||||
raise PaymentError(
|
raise PaymentError(
|
||||||
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
|
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
|
||||||
@@ -711,55 +491,49 @@ async def _pay_external_invoice(
|
|||||||
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
|
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
|
||||||
service_fee_msat = service_fee(amount_msat, internal=False)
|
service_fee_msat = service_fee(amount_msat, internal=False)
|
||||||
|
|
||||||
task = create_task(
|
|
||||||
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
|
|
||||||
)
|
|
||||||
|
|
||||||
# make sure a hold invoice or deferred payment is not blocking the server
|
|
||||||
try:
|
|
||||||
wait_time = max(1, settings.lnbits_funding_source_pay_invoice_wait_seconds)
|
|
||||||
payment_response = await asyncio.wait_for(task, wait_time)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
# return pending payment on timeout
|
|
||||||
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
|
|
||||||
):
|
|
||||||
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}")
|
|
||||||
|
|
||||||
return payment
|
|
||||||
|
|
||||||
|
|
||||||
async def _fundingsource_pay_invoice(
|
|
||||||
checking_id: str, bolt11: str, fee_reserve_msat: int
|
|
||||||
) -> PaymentResponse:
|
|
||||||
logger.debug(f"fundingsource: sending payment {checking_id}")
|
|
||||||
funding_source = get_funding_source()
|
funding_source = get_funding_source()
|
||||||
|
|
||||||
|
logger.debug(f"fundingsource: sending payment {checking_id}")
|
||||||
payment_response: PaymentResponse = await funding_source.pay_invoice(
|
payment_response: PaymentResponse = await funding_source.pay_invoice(
|
||||||
bolt11, fee_reserve_msat
|
create_payment_model.bolt11, fee_reserve_msat
|
||||||
)
|
)
|
||||||
logger.debug(f"backend: pay_invoice finished {checking_id}, {payment_response}")
|
logger.debug(f"backend: pay_invoice finished {checking_id}, {payment_response}")
|
||||||
return payment_response
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
async def _verify_external_payment(
|
async def _verify_external_payment(
|
||||||
@@ -821,20 +595,14 @@ def _validate_payment_request(
|
|||||||
if not invoice.amount_msat or not invoice.amount_msat > 0:
|
if not invoice.amount_msat or not invoice.amount_msat > 0:
|
||||||
raise PaymentError("Amountless invoices not supported.", status="failed")
|
raise PaymentError("Amountless invoices not supported.", status="failed")
|
||||||
|
|
||||||
max_sat = max_sat or settings.lnbits_max_outgoing_payment_amount_sats
|
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||||
max_sat = min(max_sat, settings.lnbits_max_outgoing_payment_amount_sats)
|
raise PaymentError("Amount in invoice is too high.", status="failed")
|
||||||
if invoice.amount_msat > max_sat * 1000:
|
|
||||||
raise PaymentError(
|
|
||||||
f"Invoice amount {invoice.amount_msat // 1000} sats is too high. "
|
|
||||||
f"Max allowed: {max_sat} sats.",
|
|
||||||
status="failed",
|
|
||||||
)
|
|
||||||
|
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
|
|
||||||
async def _credit_service_fee_wallet(
|
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)
|
service_fee_msat = service_fee(payment.amount, internal=payment.is_internal)
|
||||||
if not settings.lnbits_service_fee_wallet or not service_fee_msat:
|
if not settings.lnbits_service_fee_wallet or not service_fee_msat:
|
||||||
@@ -845,7 +613,7 @@ async def _credit_service_fee_wallet(
|
|||||||
bolt11=payment.bolt11,
|
bolt11=payment.bolt11,
|
||||||
payment_hash=payment.payment_hash,
|
payment_hash=payment.payment_hash,
|
||||||
amount_msat=abs(service_fee_msat),
|
amount_msat=abs(service_fee_msat),
|
||||||
memo=memo,
|
memo="Service fee",
|
||||||
)
|
)
|
||||||
await create_payment(
|
await create_payment(
|
||||||
checking_id=f"service_fee_{payment.payment_hash}",
|
checking_id=f"service_fee_{payment.payment_hash}",
|
||||||
@@ -853,47 +621,3 @@ async def _credit_service_fee_wallet(
|
|||||||
status=PaymentState.SUCCESS,
|
status=PaymentState.SUCCESS,
|
||||||
conn=conn,
|
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.",
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from py_vapid.utils import b64urlencode
|
|||||||
from lnbits.db import dict_to_model
|
from lnbits.db import dict_to_model
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
UpdateSettings,
|
|
||||||
readonly_variables,
|
readonly_variables,
|
||||||
settings,
|
settings,
|
||||||
)
|
)
|
||||||
@@ -38,12 +37,8 @@ async def check_webpush_settings():
|
|||||||
logger.info(f"Pubkey: {settings.lnbits_webpush_pubkey}")
|
logger.info(f"Pubkey: {settings.lnbits_webpush_pubkey}")
|
||||||
|
|
||||||
|
|
||||||
def dict_to_settings(sets_dict: dict) -> UpdateSettings:
|
|
||||||
return dict_to_model(sets_dict, UpdateSettings)
|
|
||||||
|
|
||||||
|
|
||||||
def update_cached_settings(sets_dict: dict):
|
def update_cached_settings(sets_dict: dict):
|
||||||
editable_settings = dict_to_settings(sets_dict)
|
editable_settings = dict_to_model(sets_dict, EditableSettings)
|
||||||
for key in sets_dict.keys():
|
for key in sets_dict.keys():
|
||||||
if key in readonly_variables:
|
if key in readonly_variables:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from lnbits.core.models.extensions import UserExtension
|
|||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
SuperSettings,
|
SuperSettings,
|
||||||
|
send_admin_user_to_saas,
|
||||||
settings,
|
settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,9 +47,7 @@ async def create_user_account(
|
|||||||
|
|
||||||
|
|
||||||
async def create_user_account_no_ckeck(
|
async def create_user_account_no_ckeck(
|
||||||
account: Optional[Account] = None,
|
account: Optional[Account] = None, wallet_name: Optional[str] = None
|
||||||
wallet_name: Optional[str] = None,
|
|
||||||
default_exts: Optional[list[str]] = None,
|
|
||||||
) -> User:
|
) -> User:
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
@@ -71,13 +70,9 @@ async def create_user_account_no_ckeck(
|
|||||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
|
for ext_id in settings.lnbits_user_default_extensions:
|
||||||
for ext_id in user_extensions:
|
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
|
||||||
try:
|
await update_user_extension(user_ext)
|
||||||
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
|
|
||||||
await create_user_extension(user_ext)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error enabeling default extension {ext_id}: {e}")
|
|
||||||
|
|
||||||
user = await get_user_from_account(account)
|
user = await get_user_from_account(account)
|
||||||
assert user, "Cannot find user for account."
|
assert user, "Cannot find user for account."
|
||||||
@@ -159,6 +154,14 @@ async def check_admin_settings():
|
|||||||
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
|
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
|
||||||
file.write(settings.super_user)
|
file.write(settings.super_user)
|
||||||
|
|
||||||
|
# callback for saas
|
||||||
|
if (
|
||||||
|
settings.lnbits_saas_callback
|
||||||
|
and settings.lnbits_saas_secret
|
||||||
|
and settings.lnbits_saas_instance_id
|
||||||
|
):
|
||||||
|
send_admin_user_to_saas()
|
||||||
|
|
||||||
account = await get_account(settings.super_user)
|
account = await get_account(settings.super_user)
|
||||||
if account and account.extra and account.extra.provider == "env":
|
if account and account.extra and account.extra.provider == "env":
|
||||||
settings.first_install = True
|
settings.first_install = True
|
||||||
|
|||||||
+111
-91
@@ -1,93 +1,79 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import traceback
|
|
||||||
from collections.abc import Coroutine
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
from lnbits.core.crud import (
|
||||||
create_audit_entry,
|
create_audit_entry,
|
||||||
get_wallet,
|
get_wallet,
|
||||||
|
get_webpush_subscriptions_for_user,
|
||||||
|
mark_webhook_sent,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.audit import delete_expired_audit_entries
|
from lnbits.core.crud.audit import delete_expired_audit_entries
|
||||||
from lnbits.core.crud.payments import get_payments_status_count
|
from lnbits.core.models import AuditEntry, Payment
|
||||||
from lnbits.core.crud.users import get_accounts
|
from lnbits.core.services import (
|
||||||
from lnbits.core.crud.wallets import get_wallets_count
|
|
||||||
from lnbits.core.models import AuditEntry
|
|
||||||
from lnbits.core.models.extensions import InstallableExtension
|
|
||||||
from lnbits.core.models.notifications import NotificationType
|
|
||||||
from lnbits.core.services.funding_source import (
|
|
||||||
check_balance_delta_changed,
|
|
||||||
check_server_balance_against_node,
|
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
)
|
|
||||||
from lnbits.core.services.notifications import (
|
|
||||||
enqueue_notification,
|
|
||||||
process_next_notification,
|
|
||||||
send_payment_notification,
|
send_payment_notification,
|
||||||
|
switch_to_voidwallet,
|
||||||
)
|
)
|
||||||
from lnbits.db import Filters
|
from lnbits.settings import get_funding_source, settings
|
||||||
from lnbits.settings import settings
|
from lnbits.tasks import send_push_notification
|
||||||
from lnbits.tasks import create_unique_task
|
|
||||||
from lnbits.utils.exchange_rates import btc_rates
|
from lnbits.utils.exchange_rates import btc_rates
|
||||||
|
|
||||||
audit_queue: asyncio.Queue = asyncio.Queue()
|
audit_queue: asyncio.Queue = asyncio.Queue()
|
||||||
|
|
||||||
|
|
||||||
async def run_by_the_minute_tasks():
|
async def killswitch_task():
|
||||||
minute_counter = 0
|
"""
|
||||||
|
killswitch will check lnbits-status repository for a signal from
|
||||||
|
LNbits and will switch to VoidWallet if the killswitch is triggered.
|
||||||
|
"""
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
status_minutes = settings.lnbits_notification_server_status_hours * 60
|
funding_source = get_funding_source()
|
||||||
|
if (
|
||||||
|
settings.lnbits_killswitch
|
||||||
|
and funding_source.__class__.__name__ != "VoidWallet"
|
||||||
|
):
|
||||||
|
with httpx.Client() as client:
|
||||||
|
try:
|
||||||
|
r = client.get(settings.lnbits_status_manifest, timeout=4)
|
||||||
|
r.raise_for_status()
|
||||||
|
if r.status_code == 200:
|
||||||
|
ks = r.json().get("killswitch")
|
||||||
|
if ks and ks == 1:
|
||||||
|
logger.error(
|
||||||
|
"Switching to VoidWallet. Killswitch triggered."
|
||||||
|
)
|
||||||
|
await switch_to_voidwallet()
|
||||||
|
except (httpx.RequestError, httpx.HTTPStatusError):
|
||||||
|
logger.error(
|
||||||
|
"Cannot fetch lnbits status manifest."
|
||||||
|
f" {settings.lnbits_status_manifest}"
|
||||||
|
)
|
||||||
|
await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
|
||||||
|
|
||||||
if settings.notification_balance_delta_changed:
|
|
||||||
|
async def watchdog_task():
|
||||||
|
"""
|
||||||
|
Registers a watchdog which will check lnbits balance and nodebalance
|
||||||
|
and will switch to VoidWallet if the watchdog delta is reached.
|
||||||
|
"""
|
||||||
|
while settings.lnbits_running:
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
if (
|
||||||
|
settings.lnbits_watchdog
|
||||||
|
and funding_source.__class__.__name__ != "VoidWallet"
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
# runs by default every minute, the delta should not change that often
|
balance = await get_balance_delta()
|
||||||
await check_balance_delta_changed()
|
delta = balance.delta_msats
|
||||||
except Exception as ex:
|
logger.debug(f"Running watchdog task. current delta: {delta}")
|
||||||
logger.error(ex)
|
if delta + settings.lnbits_watchdog_delta <= 0:
|
||||||
|
logger.error(f"Switching to VoidWallet. current delta: {delta}")
|
||||||
if minute_counter % settings.lnbits_watchdog_interval_minutes == 0:
|
await switch_to_voidwallet()
|
||||||
try:
|
except Exception as e:
|
||||||
await check_server_balance_against_node()
|
logger.error("Error in watchdog task", e)
|
||||||
except Exception as ex:
|
await asyncio.sleep(settings.lnbits_watchdog_interval * 60)
|
||||||
logger.error(ex)
|
|
||||||
|
|
||||||
if minute_counter % status_minutes == 0:
|
|
||||||
try:
|
|
||||||
await _notify_server_status()
|
|
||||||
except Exception as ex:
|
|
||||||
logger.error(ex)
|
|
||||||
|
|
||||||
if minute_counter % 60 == 0:
|
|
||||||
try:
|
|
||||||
# initialize the list of all extensions
|
|
||||||
await InstallableExtension.get_installable_extensions()
|
|
||||||
except Exception as ex:
|
|
||||||
logger.error(ex)
|
|
||||||
|
|
||||||
minute_counter += 1
|
|
||||||
await asyncio.sleep(60)
|
|
||||||
|
|
||||||
|
|
||||||
async def _notify_server_status():
|
|
||||||
accounts = await get_accounts(filters=Filters(limit=0))
|
|
||||||
wallets_count = await get_wallets_count()
|
|
||||||
payments = await get_payments_status_count()
|
|
||||||
|
|
||||||
status = await get_balance_delta()
|
|
||||||
values = {
|
|
||||||
"up_time": settings.lnbits_server_up_time,
|
|
||||||
"accounts_count": accounts.total,
|
|
||||||
"wallets_count": wallets_count,
|
|
||||||
"in_payments_count": payments.incoming,
|
|
||||||
"out_payments_count": payments.outgoing,
|
|
||||||
"pending_payments_count": payments.pending,
|
|
||||||
"failed_payments_count": payments.failed,
|
|
||||||
"delta_sats": status.delta_sats,
|
|
||||||
"lnbits_balance_sats": status.lnbits_balance_sats,
|
|
||||||
"node_balance_sats": status.node_balance_sats,
|
|
||||||
}
|
|
||||||
enqueue_notification(NotificationType.server_status, values)
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||||
@@ -101,6 +87,61 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
|||||||
wallet = await get_wallet(payment.wallet_id)
|
wallet = await get_wallet(payment.wallet_id)
|
||||||
if wallet:
|
if wallet:
|
||||||
await send_payment_notification(wallet, payment)
|
await send_payment_notification(wallet, payment)
|
||||||
|
# dispatch webhook
|
||||||
|
if payment.webhook and not payment.webhook_status:
|
||||||
|
await dispatch_webhook(payment)
|
||||||
|
# dispatch push notification
|
||||||
|
await send_payment_push_notification(payment)
|
||||||
|
|
||||||
|
|
||||||
|
async def dispatch_webhook(payment: Payment):
|
||||||
|
"""
|
||||||
|
Dispatches the webhook to the webhook url.
|
||||||
|
"""
|
||||||
|
logger.debug("sending webhook", payment.webhook)
|
||||||
|
|
||||||
|
if not payment.webhook:
|
||||||
|
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:
|
||||||
|
r = await client.post(payment.webhook, json=data, timeout=40)
|
||||||
|
r.raise_for_status()
|
||||||
|
await mark_webhook_sent(payment.payment_hash, r.status_code)
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
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)
|
||||||
|
logger.warning(f"Could not send webhook to {payment.webhook}")
|
||||||
|
|
||||||
|
|
||||||
|
async def send_payment_push_notification(payment: Payment):
|
||||||
|
wallet = await get_wallet(payment.wallet_id)
|
||||||
|
|
||||||
|
if wallet:
|
||||||
|
subscriptions = await get_webpush_subscriptions_for_user(wallet.user)
|
||||||
|
|
||||||
|
amount = int(payment.amount / 1000)
|
||||||
|
|
||||||
|
title = f"LNbits: {wallet.name}"
|
||||||
|
body = f"You just received {amount} sat{'s'[:amount^1]}!"
|
||||||
|
|
||||||
|
if payment.memo:
|
||||||
|
body += f"\r\n{payment.memo}"
|
||||||
|
|
||||||
|
for subscription in subscriptions:
|
||||||
|
# todo: review permissions when user-id-only not allowed
|
||||||
|
# todo: replace all this logic with websockets?
|
||||||
|
url = (
|
||||||
|
f"https://{subscription.host}/wallet?usr={wallet.user}&wal={wallet.id}"
|
||||||
|
)
|
||||||
|
await send_push_notification(subscription, title, body, url)
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_audit_data():
|
async def wait_for_audit_data():
|
||||||
@@ -117,16 +158,6 @@ async def wait_for_audit_data():
|
|||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
|
||||||
async def wait_notification_messages():
|
|
||||||
|
|
||||||
while settings.lnbits_running:
|
|
||||||
try:
|
|
||||||
await process_next_notification()
|
|
||||||
except Exception as ex:
|
|
||||||
logger.log("error", ex)
|
|
||||||
await asyncio.sleep(3)
|
|
||||||
|
|
||||||
|
|
||||||
async def purge_audit_data():
|
async def purge_audit_data():
|
||||||
"""
|
"""
|
||||||
Remove audit entries which have passed their retention period.
|
Remove audit entries which have passed their retention period.
|
||||||
@@ -163,14 +194,3 @@ async def collect_exchange_rates_data():
|
|||||||
else:
|
else:
|
||||||
sleep_time = 60
|
sleep_time = 60
|
||||||
await asyncio.sleep(sleep_time)
|
await asyncio.sleep(sleep_time)
|
||||||
|
|
||||||
|
|
||||||
def _create_unique_task(name: str, func: Callable):
|
|
||||||
async def _to_coro(func: Callable[[], Coroutine]) -> Coroutine:
|
|
||||||
return await func()
|
|
||||||
|
|
||||||
try:
|
|
||||||
create_unique_task(name, _to_coro(func))
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in {name} task", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|||||||
@@ -15,8 +15,11 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-text="$t('enable_audit')"></q-item-label>
|
<q-item-label>Enable audit</q-item-label>
|
||||||
<q-item-label caption v-text="$t('audit_desc')"></q-item-label>
|
<q-item-label caption
|
||||||
|
>Record HTTP requests according with the specified
|
||||||
|
filters</q-item-label
|
||||||
|
>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,16 +48,14 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-text="$t('audit_record_req')"></q-item-label>
|
<q-item-label>Record Request Body</q-item-label>
|
||||||
<q-item-label caption>
|
<q-item-label caption
|
||||||
<span v-text="$t('audit_record_warning')"></span>
|
>Warning:
|
||||||
<br />
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><span v-text="$t('audit_record_req_warning_1')"></span></li>
|
<li>confidential data (like passwords) will be logged</li>
|
||||||
<li><span v-text="$t('audit_record_req_warning_2')"></span></li>
|
<li>the request body can have large size.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br />
|
Use it with caution.
|
||||||
<span v-text="$t('audit_record_use')"></span>
|
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -71,12 +72,8 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>
|
<q-item-label>Record IP Address</q-item-label>
|
||||||
<span v-text="$t('audit_ip')"></span>
|
<q-item-label caption>Save the client IP address.</q-item-label>
|
||||||
</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<span v-text="$t('audit_ip_desc')"></span>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,12 +89,8 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>
|
<q-item-label>Record Path Parameters</q-item-label>
|
||||||
<span v-text="$t('audit_path_params')"></span>
|
<q-item-label caption>Recommended. </q-item-label>
|
||||||
</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<span v-text="$t('recommended')"></span>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,12 +106,8 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>
|
<q-item-label>Record Query Parameters</q-item-label>
|
||||||
<span v-text="$t('audit_query_params')"></span>
|
<q-item-label caption>Recommended.</q-item-label>
|
||||||
</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<span v-text="$t('recommended')"></span>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,26 +115,25 @@
|
|||||||
<q-separator class="q-mb-xl q-mt-sm"></q-separator>
|
<q-separator class="q-mb-xl q-mt-sm"></q-separator>
|
||||||
<div class="row q-mb-lg">
|
<div class="row q-mb-lg">
|
||||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||||
<p><span v-text="$t('audit_http_methods')"></span></p>
|
<p>Include HTTP Methods</p>
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_audit_http_methods"
|
v-model="formData.lnbits_audit_http_methods"
|
||||||
multiple
|
multiple
|
||||||
:hint="$t('audit_http_methods_hint')"
|
hint="List of HTTP methods to be included. Empty lists means all."
|
||||||
:label="$t('audit_http_methods_label')"
|
label="HTTP Methods"
|
||||||
:options="['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']"
|
:options="['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']"
|
||||||
></q-select>
|
></q-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||||
<p><span v-text="$t('audit_resp_codes')"></span></p>
|
<p>Include HTTP Response Codes</p>
|
||||||
<q-input
|
<q-input
|
||||||
class="q-mb-md"
|
|
||||||
filled
|
filled
|
||||||
v-model="formAddIncludeResponseCode"
|
v-model="formAddIncludeResponseCode"
|
||||||
@keydown.enter="addIncludeResponseCode"
|
@keydown.enter="addIncludeResponseCode"
|
||||||
type="text"
|
type="text"
|
||||||
:label="$t('audit_resp_codes_label')"
|
label="HTTP Response code (regex)"
|
||||||
:hint="$t('audit_resp_codes_hint')"
|
hint="List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*"
|
||||||
>
|
>
|
||||||
<q-btn @click="addIncludeResponseCode" dense flat icon="add"></q-btn>
|
<q-btn @click="addIncludeResponseCode" dense flat icon="add"></q-btn>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -166,15 +154,14 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||||
<p><span v-text="$t('audit_paths')"></span></p>
|
<p>Include Paths</p>
|
||||||
<q-input
|
<q-input
|
||||||
class="q-mb-md"
|
|
||||||
filled
|
filled
|
||||||
v-model="formAddIncludePath"
|
v-model="formAddIncludePath"
|
||||||
@keydown.enter="addIncludePath"
|
@keydown.enter="addIncludePath"
|
||||||
type="text"
|
type="text"
|
||||||
:label="$t('audit_paths_label')"
|
label="HTTP Path (regex)"
|
||||||
:hint="$t('audit_paths_hint')"
|
hint="List of paths to be included (regex match). Empty list means all."
|
||||||
>
|
>
|
||||||
<q-btn @click="addIncludePath" dense flat icon="add"></q-btn>
|
<q-btn @click="addIncludePath" dense flat icon="add"></q-btn>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -193,17 +180,14 @@
|
|||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="col-md-6 col-sm-12">
|
||||||
<p>
|
<p>Exclude Paths</p>
|
||||||
<span v-text="$t('audit_paths_exclude')"></span>
|
|
||||||
</p>
|
|
||||||
<q-input
|
<q-input
|
||||||
class="q-mb-md"
|
|
||||||
filled
|
filled
|
||||||
v-model="formAddExcludePath"
|
v-model="formAddExcludePath"
|
||||||
@keydown.enter="addExcludePath"
|
@keydown.enter="addExcludePath"
|
||||||
type="text"
|
type="text"
|
||||||
:label="$t('audit_paths_exclude_label')"
|
label="HTTP Path (regex)"
|
||||||
:hint="$t('audit_paths_exclude_hint')"
|
hint="List of paths to be excluded (regex match). Empty list means none."
|
||||||
>
|
>
|
||||||
<q-btn @click="addExcludePath" dense flat icon="add"></q-btn>
|
<q-btn @click="addExcludePath" dense flat icon="add"></q-btn>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<q-tab-panel name="exchange_providers">
|
<q-tab-panel name="exchange_providers">
|
||||||
<h6 class="q-my-none q-mb-sm">
|
<h6 class="q-my-none q-mb-sm">Exchange Providers</h6>
|
||||||
<span v-text="$t('exchange_providers')"></span>
|
|
||||||
</h6>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 col-sm-12">
|
<div class="col-md-8 col-sm-12">
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
<q-tab-panel name="extensions">
|
<q-tab-panel name="extensions">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none">Extensions</h6>
|
||||||
<span v-text="$t('extensions')"></span>
|
|
||||||
</h6>
|
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 q-mb-md">
|
<div class="col-12">
|
||||||
<p>
|
<p>Extension Sources</p>
|
||||||
<span v-text="$t('extension_sources')"></span>
|
|
||||||
</p>
|
|
||||||
<q-input
|
<q-input
|
||||||
class="q-mb-md"
|
|
||||||
filled
|
filled
|
||||||
v-model="formAddExtensionsManifest"
|
v-model="formAddExtensionsManifest"
|
||||||
@keydown.enter="addExtensionsManifest"
|
@keydown.enter="addExtensionsManifest"
|
||||||
type="text"
|
type="text"
|
||||||
:label="$t('ext_sources_label')"
|
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
|
||||||
:hint="$t('ext_sources_hint')"
|
hint="Repositories from where the extensions can be downloaded"
|
||||||
>
|
>
|
||||||
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
|
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -35,44 +30,34 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p>
|
<p>Admin Extensions</p>
|
||||||
<span v-text="$t('admin_extensions')"></span>
|
|
||||||
</p>
|
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_admin_extensions"
|
v-model="formData.lnbits_admin_extensions"
|
||||||
multiple
|
multiple
|
||||||
:label="$t('admin_extensions_label')"
|
hint="Extensions only user with admin privileges can use"
|
||||||
:hint="$t('admin_extensions_hint')"
|
label="Admin extensions"
|
||||||
:options="g.extensions"
|
:options="g.extensions"
|
||||||
></q-select>
|
></q-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p>
|
<p>User Default Extensions</p>
|
||||||
<span v-text="$t('user_default_extensions')"></span>
|
|
||||||
</p>
|
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_user_default_extensions"
|
v-model="formData.lnbits_user_default_extensions"
|
||||||
multiple
|
multiple
|
||||||
:label="$t('user_default_extensions_label')"
|
hint="Extensions that will be enabled by default for the users."
|
||||||
:hint="$t('user_default_extensions_hint')"
|
label="User extensions"
|
||||||
:options="g.extensions"
|
:options="g.extensions"
|
||||||
></q-select>
|
></q-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p>
|
<p>Miscellaneous</p>
|
||||||
<span v-text="$t('miscellaneous')"></span>
|
|
||||||
</p>
|
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>
|
<q-item-label>Disable Extensions</q-item-label>
|
||||||
<span v-text="$t('misc_disable_extensions')"></span>
|
<q-item-label caption>Disables all extensions</q-item-label>
|
||||||
</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<span v-text="$t('misc_disable_extensions_label')"></span>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
@@ -86,12 +71,10 @@
|
|||||||
</q-item>
|
</q-item>
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>
|
<q-item-label>Hide API</q-item-label>
|
||||||
<span v-text="$t('misc_hide_api')"></span>
|
<q-item-label caption
|
||||||
</q-item-label>
|
>Hides wallet api, extensions can choose to honor</q-item-label
|
||||||
<q-item-label caption>
|
>
|
||||||
<span v-text="$t('misc_hide_api_label')"></span>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -1,104 +1,88 @@
|
|||||||
<q-tab-panel name="funding">
|
<q-tab-panel name="funding">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none">Wallets Management</h6>
|
||||||
<span v-text="$t('wallets_management')"></span>
|
|
||||||
</h6>
|
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<p>
|
<p>Funding Source Info</p>
|
||||||
<span v-text="$t('funding_source_info')"></span>
|
|
||||||
</p>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-text="$t('funding_source', {wallet_class: settings.lnbits_backend_wallet_class})"
|
v-text="'Funding Source: '+ settings.lnbits_backend_wallet_class"
|
||||||
></li>
|
></li>
|
||||||
<li
|
<li
|
||||||
v-text="$t('node_balance', {balance: (auditData.node_balance_sats || 0).toLocaleString()})"
|
v-text="'Node Balance: ' + (auditData.node_balance_msats /
|
||||||
|
1000).toLocaleString() + ' sats'"
|
||||||
></li>
|
></li>
|
||||||
<li
|
<li
|
||||||
v-text="$t('lnbits_balance', {balance: (auditData.lnbits_balance_sats || 0).toLocaleString()})"
|
v-text="'LNbits Balance: ' + (auditData.lnbits_balance_msats /
|
||||||
|
1000).toLocaleString() + ' sats'"
|
||||||
></li>
|
></li>
|
||||||
<li
|
<li
|
||||||
v-text="$t('funding_reserve_percent', {
|
v-text="'Reserve Percent: ' + (auditData.node_balance_msats /
|
||||||
percent: auditData.lnbits_balance_sats > 0
|
auditData.lnbits_balance_msats * 100).toFixed(2) + ' %'"
|
||||||
? (auditData.node_balance_sats / auditData.lnbits_balance_sats * 100).toFixed(2)
|
|
||||||
: 100
|
|
||||||
})"
|
|
||||||
></li>
|
></li>
|
||||||
</ul>
|
</ul>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% if LNBITS_NODE_UI_AVAILABLE %}
|
{% if LNBITS_NODE_UI_AVAILABLE %}
|
||||||
<p><span v-text="$t('node_management')"></span></p>
|
<p>Node Management</p>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
:label="$t('toggle_node_ui')"
|
label="Node UI"
|
||||||
v-model="formData.lnbits_node_ui"
|
v-model="formData.lnbits_node_ui"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-if="formData.lnbits_node_ui"
|
v-if="formData.lnbits_node_ui"
|
||||||
:label="$t('toggle_public_node_ui')"
|
label="Public node UI"
|
||||||
v-model="formData.lnbits_public_node_ui"
|
v-model="formData.lnbits_public_node_ui"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
<br />
|
<br />
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-if="formData.lnbits_node_ui"
|
v-if="formData.lnbits_node_ui"
|
||||||
:label="$t('toggle_transactions_node_ui')"
|
label="Transactions Tab (Disable on large CLN nodes)"
|
||||||
v-model="formData.lnbits_node_ui_transactions"
|
v-model="formData.lnbits_node_ui_transactions"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><span v-text="$t('node_management_not_supported')"></span></p>
|
<p>Node Management not supported by active funding source</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-3">
|
<div class="col-12 col-md-4">
|
||||||
<p><span v-text="$t('fee_reserve')"></span></p>
|
<p>Invoice Expiry</p>
|
||||||
|
|
||||||
<q-input
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_reserve_fee_min"
|
|
||||||
:label="$t('fee_reserve_msats')"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-3">
|
|
||||||
<p><span v-text="$t('fee_reserve_percent')"></span></p>
|
|
||||||
<q-input
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
name="lnbits_reserve_fee_percent"
|
|
||||||
v-model="formData.lnbits_reserve_fee_percent"
|
|
||||||
:label="$t('reserve_fee_in_percent')"
|
|
||||||
step="0.1"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-3">
|
|
||||||
<p><span v-text="$t('invoice_expiry')"></span></p>
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model.number="formData.lightning_invoice_expiry"
|
v-model.number="formData.lightning_invoice_expiry"
|
||||||
type="number"
|
type="number"
|
||||||
:label="$t('invoice_expiry_label')"
|
label="Invoice expiry (seconds)"
|
||||||
mask="#######"
|
mask="#######"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-3">
|
<div class="col-12 col-md-8">
|
||||||
<p><span v-text="$t('payment_wait_time')"></span></p>
|
<p>Fee reserve</p>
|
||||||
<q-input
|
<div class="row q-col-gutter-md">
|
||||||
type="number"
|
<div class="col-6">
|
||||||
filled
|
<q-input
|
||||||
name="lnbits_funding_source_pay_invoice_wait_seconds"
|
type="number"
|
||||||
v-model="formData.lnbits_funding_source_pay_invoice_wait_seconds"
|
filled
|
||||||
:label="$t('payment_wait_time')"
|
v-model="formData.lnbits_reserve_fee_min"
|
||||||
:hint="$t('payment_wait_time_desc')"
|
label="Reserve fee in msats"
|
||||||
step="1"
|
>
|
||||||
min="0"
|
</q-input>
|
||||||
></q-input>
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-input
|
||||||
|
type="number"
|
||||||
|
filled
|
||||||
|
name="lnbits_reserve_fee_percent"
|
||||||
|
v-model="formData.lnbits_reserve_fee_percent"
|
||||||
|
label="Reserve fee in percent"
|
||||||
|
step="0.1"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isSuperUser">
|
<div v-if="isSuperUser">
|
||||||
@@ -107,90 +91,6 @@
|
|||||||
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-separator></q-separator>
|
|
||||||
<h6 class="q-mt-lg q-mb-sm">
|
|
||||||
<p v-text="$t('watchdog')"></p>
|
|
||||||
</h6>
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('enable_watchdog')"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('enable_watchdog_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_watchdog_switch_to_voidwallet"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notification_watchdog_limit')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notification_watchdog_limit_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_notification_watchdog"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('watchdog_interval')"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('watchdog_interval_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_watchdog_interval_minutes"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('watchdog_delta')"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('watchdog_delta_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_watchdog_delta"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
<q-tab-panel name="library">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<h6 class="q-my-none q-mb-sm">
|
|
||||||
<span v-text="$t('image_library')"></span>
|
|
||||||
</h6>
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
label="Add Image"
|
|
||||||
@click="$refs.imageInput.click()"
|
|
||||||
class="q-mb-md"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref="imageInput"
|
|
||||||
accept="image/png, image/jpeg, image/gif"
|
|
||||||
style="display: none"
|
|
||||||
@change="onImageInput"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
<div class="row q-col-gutter-sm q-pa-sm">
|
|
||||||
<div
|
|
||||||
v-for="image in library_images"
|
|
||||||
:key="image.filename"
|
|
||||||
class="col-6 col-sm-4 col-md-3 col-lg-2"
|
|
||||||
style="max-width: 200px"
|
|
||||||
>
|
|
||||||
<q-card class="q-mb-sm">
|
|
||||||
<q-img :src="image.url" style="height: 150px" />
|
|
||||||
|
|
||||||
<q-card-section
|
|
||||||
class="q-pt-md q-pb-md row items-center justify-between"
|
|
||||||
>
|
|
||||||
<small
|
|
||||||
><div
|
|
||||||
class="text-caption ellipsis"
|
|
||||||
style="max-width: 100px"
|
|
||||||
:title="image.filename"
|
|
||||||
v-text="image.filename"
|
|
||||||
></div
|
|
||||||
></small>
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
size="sm"
|
|
||||||
icon="content_copy"
|
|
||||||
@click="copyText(image.url)"
|
|
||||||
:title="$t('copy')"
|
|
||||||
><q-tooltip>Copy image link</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
size="sm"
|
|
||||||
icon="delete"
|
|
||||||
color="negative"
|
|
||||||
@click="deleteImage(image.filename)"
|
|
||||||
:title="$t('delete')"
|
|
||||||
><q-tooltip>Delete image</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="library_images.length === 0" class="q-pa-xl">
|
|
||||||
<div class="text-subtitle2 text-grey">No images uploaded yet.</div>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
@@ -1,504 +1,139 @@
|
|||||||
<q-tab-panel name="notifications">
|
<q-tab-panel name="notifications">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none q-mb-sm">
|
<div>
|
||||||
<span v-text="$t('notifications_configure')"></span>
|
<div class="row q-col-gutter-md">
|
||||||
<q-btn
|
<div class="col-12 col-md-6">
|
||||||
round
|
<q-item tag="label" v-ripple>
|
||||||
flat
|
<q-item-section>
|
||||||
@click="hideInputsToggle()"
|
<q-item-label v-text="$t('enable_notifications')"></q-item-label>
|
||||||
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
|
<q-item-label
|
||||||
></q-btn>
|
caption
|
||||||
</h6>
|
v-text="$t('enable_notifications_desc')"
|
||||||
|
></q-item-label>
|
||||||
<q-separator class="q-mt-md q-mb-sm"></q-separator>
|
</q-item-section>
|
||||||
<div class="row q-col-gutter-md">
|
<q-item-section avatar>
|
||||||
<div class="col-sm-12 col-md-6">
|
<q-toggle
|
||||||
<strong v-text="$t('notifications_nostr_config')"></strong>
|
size="md"
|
||||||
<q-item tag="label" v-ripple>
|
v-model="formData.lnbits_notifications"
|
||||||
<q-item-section>
|
checked-icon="check"
|
||||||
<q-item-label
|
color="green"
|
||||||
v-text="$t('notifications_enable_nostr')"
|
unchecked-icon="clear"
|
||||||
></q-item-label>
|
/>
|
||||||
<q-item-label
|
</q-item-section>
|
||||||
caption
|
</q-item>
|
||||||
v-text="$t('notifications_enable_nostr_desc')"
|
<br />
|
||||||
></q-item-label>
|
<p
|
||||||
</q-item-section>
|
v-if="!formData.lnbits_notifications"
|
||||||
<q-item-section avatar>
|
v-text="$t('notifications_disabled')"
|
||||||
<q-toggle
|
></p>
|
||||||
size="md"
|
<div v-if="formData.lnbits_notifications">
|
||||||
v-model="formData.lnbits_nostr_notifications_enabled"
|
{% include "admin/_tab_security_notifications.html" %}
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notifications_nostr_private_key')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_nostr_private_key_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
:type="hideInputToggle ? 'password' : 'text'"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_nostr_notifications_private_key"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notifications_nostr_identifiers')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_nostr_identifiers_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="nostrNotificationIdentifier"
|
|
||||||
@keydown.enter="addNostrNotificationIdentifier"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="addNostrNotificationIdentifier()"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
icon="add"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
<div>
|
|
||||||
<q-chip
|
|
||||||
v-for="identifier in formData.lnbits_nostr_notifications_identifiers"
|
|
||||||
:key="identifier"
|
|
||||||
removable
|
|
||||||
@remove="removeNostrNotificationIdentifier(identifier)"
|
|
||||||
color="primary"
|
|
||||||
text-color="white"
|
|
||||||
><span class="ellipsis" v-text="identifier"></span
|
|
||||||
></q-chip>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-12 col-md-6">
|
|
||||||
<strong v-text="$t('notifications_telegram_config')"></strong>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notifications_enable_telegram')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_enable_telegram_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_telegram_notifications_enabled"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notifications_telegram_access_token')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_telegram_access_token_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
:type="hideInputToggle ? 'password' : 'text'"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_telegram_notifications_access_token"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('notifications_chat_id')"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_chat_id_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_telegram_notifications_chat_id"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<q-separator></q-separator>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<strong v-text="$t('notifications_email_config')"></strong>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notifications_enable_email')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_enable_email_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_email_notifications_enabled"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<div v-if="formData.lnbits_email_notifications_enabled" class="row">
|
|
||||||
<div class="col">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notifications_send_email')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_send_email_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
:type="hideInputToggle ? 'password' : 'text'"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_email_notifications_email"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notifications_send_email_username')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_send_email_username_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
:type="hideInputToggle ? 'password' : 'text'"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_email_notifications_username"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notifications_send_email_password')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_send_email_password_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
:type="hideInputToggle ? 'password' : 'text'"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_email_notifications_password"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item>
|
|
||||||
<q-btn
|
|
||||||
@click="sendTestEmail()"
|
|
||||||
:label="$t('notifications_send_test_email')"
|
|
||||||
color="primary"
|
|
||||||
class="q-mt-md"
|
|
||||||
></q-btn>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
</div>
|
||||||
|
<br />
|
||||||
<div class="col">
|
<div>
|
||||||
<q-item tag="label" v-ripple>
|
<p v-text="$t('notification_source')"></p>
|
||||||
<q-item-section>
|
<q-input
|
||||||
<q-item-label
|
filled
|
||||||
v-text="$t('notifications_send_to_emails')"
|
v-model="formData.lnbits_status_manifest"
|
||||||
></q-item-label>
|
type="text"
|
||||||
<q-item-label
|
:label="$t('notification_source_label')"
|
||||||
caption
|
/>
|
||||||
v-text="$t('notifications_send_to_emails_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="emailNotificationAddress"
|
|
||||||
@keydown.enter="addEmailNotificationAddress"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="addEmailNotificationAddress()"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
icon="add"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
<div>
|
|
||||||
<q-chip
|
|
||||||
v-for="to_email in formData.lnbits_email_notifications_to_emails"
|
|
||||||
:key="to_email"
|
|
||||||
removable
|
|
||||||
@remove="removeEmailNotificationAddress(to_email)"
|
|
||||||
color="primary"
|
|
||||||
text-color="white"
|
|
||||||
><span class="ellipsis" v-text="to_email"></span
|
|
||||||
></q-chip>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notifications_send_email_server_port')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_send_email_server_port_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
:type="hideInputToggle ? 'password' : 'text'"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_email_notifications_port"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notifications_send_email_server')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notifications_send_email_server_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
:type="hideInputToggle ? 'password' : 'text'"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_email_notifications_server"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
</div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p v-text="$t('killswitch')"></p>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label v-text="$t('enable_killswitch')"></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('enable_killswitch_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-toggle
|
||||||
|
size="md"
|
||||||
|
v-model="formData.lnbits_killswitch"
|
||||||
|
checked-icon="check"
|
||||||
|
color="green"
|
||||||
|
unchecked-icon="clear"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label v-text="$t('killswitch_interval')"></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('killswitch_interval_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_killswitch_interval"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<br />
|
||||||
|
<p v-text="$t('watchdog')"></p>
|
||||||
|
<q-item disabled tag="label" v-ripple>
|
||||||
|
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label v-text="$t('enable_watchdog')"></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('enable_watchdog_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-toggle
|
||||||
|
size="md"
|
||||||
|
v-model="formData.lnbits_watchdog"
|
||||||
|
checked-icon="check"
|
||||||
|
color="green"
|
||||||
|
unchecked-icon="clear"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item disabled tag="label" v-ripple>
|
||||||
|
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label v-text="$t('watchdog_interval')"></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('watchdog_interval_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_watchdog_interval"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item disabled tag="label" v-ripple>
|
||||||
|
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label v-text="$t('watchdog_delta')"></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('watchdog_delta_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_watchdog_delta"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator> </q-separator>
|
|
||||||
<h6 class="q-mb-sm">
|
|
||||||
<span v-text="$t('notifications')"></span>
|
|
||||||
</h6>
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notification_settings_update')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notification_settings_update_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_notification_settings_update"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notification_credit_debit')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notification_credit_debit_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_notification_credit_debit"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notification_server_start_stop')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notification_server_start_stop_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_notification_server_start_stop"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notification_balance_delta_changed')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notification_balance_delta_changed_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.notification_balance_delta_changed"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notification_watchdog_limit')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notification_watchdog_limit_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_notification_watchdog"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notification_server_status')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notification_server_status_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-input
|
|
||||||
class="flow-right"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_notification_server_status_hours"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notification_incoming_payment')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notification_incoming_payment_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-input
|
|
||||||
class="flow-right"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_notification_incoming_payment_amount_sats"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label
|
|
||||||
v-text="$t('notification_outgoing_payment')"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('notification_outgoing_payment_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-input
|
|
||||||
class="flow-right"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_notification_outgoing_payment_amount_sats"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
<q-tab-panel name="security">
|
<q-tab-panel name="security">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none"><span v-text="$t('server_management')"></span></h6>
|
<h6 class="q-my-none">Server Management</h6>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<p><span v-text="$t('base_url')"></span></p>
|
<p>Base URL</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model.number="formData.lnbits_baseurl"
|
v-model.number="formData.lnbits_baseurl"
|
||||||
:label="$t('base_url_label')"
|
label="Static/Base url for the server"
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h6 class="q-my-none q-mb-sm">
|
<h6 class="q-my-none q-mb-sm">Authentication</h6>
|
||||||
<span v-text="$t('authentication')"></span>
|
|
||||||
</h6>
|
|
||||||
<div class="row q-col-gutter-sm">
|
<div class="row q-col-gutter-sm">
|
||||||
<div class="col-12 col-sm-6">
|
<div class="col-12 col-sm-6">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="formData.auth_token_expire_minutes"
|
v-model="formData.auth_token_expire_minutes"
|
||||||
type="number"
|
type="number"
|
||||||
:label="$t('auth_token_expiry_label')"
|
label="Token expire minutes"
|
||||||
:hint="$t('auth_token_expiry_hint')"
|
hint="Time in minutes until the token expires"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,8 +29,8 @@
|
|||||||
filled
|
filled
|
||||||
v-model="formData.auth_allowed_methods"
|
v-model="formData.auth_allowed_methods"
|
||||||
multiple
|
multiple
|
||||||
:hint="$t('auth_allowed_methods_hint')"
|
hint="Allowed authorization methods"
|
||||||
:label="$t('auth_allowed_methods_label')"
|
label="Select authorization methods"
|
||||||
:options="formData.auth_all_methods"
|
:options="formData.auth_all_methods"
|
||||||
></q-select>
|
></q-select>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,8 +50,8 @@
|
|||||||
v-model="nostrAcceptedUrl"
|
v-model="nostrAcceptedUrl"
|
||||||
@keydown.enter="addNostrUrl"
|
@keydown.enter="addNostrUrl"
|
||||||
type="text"
|
type="text"
|
||||||
:label="$t('auth_nostr_label')"
|
label="Nostr Request URL"
|
||||||
:hint="$t('auth_nostr_hint')"
|
hint="Absolute URL that the clients will use to login."
|
||||||
>
|
>
|
||||||
<q-btn @click="addNostrUrl" dense flat icon="add"></q-btn>
|
<q-btn @click="addNostrUrl" dense flat icon="add"></q-btn>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -83,8 +81,8 @@
|
|||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="formData.google_client_id"
|
v-model="formData.google_client_id"
|
||||||
:label="$t('auth_google_ci_label')"
|
label="Google Client ID"
|
||||||
:hint="$t('auth_google_ci_hint')"
|
hint="Make sure thant the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +91,7 @@
|
|||||||
filled
|
filled
|
||||||
v-model="formData.google_client_secret"
|
v-model="formData.google_client_secret"
|
||||||
type="password"
|
type="password"
|
||||||
:label="$t('auth_google_cs_label')"
|
label="Google Client Secret"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,8 +108,8 @@
|
|||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="formData.github_client_id"
|
v-model="formData.github_client_id"
|
||||||
:label="$t('auth_gh_client_id_label')"
|
label="GitHub Client ID"
|
||||||
:hint="$t('auth_gh_client_id_hint')"
|
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/github/token"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,7 +118,7 @@
|
|||||||
filled
|
filled
|
||||||
v-model="formData.github_client_secret"
|
v-model="formData.github_client_secret"
|
||||||
type="password"
|
type="password"
|
||||||
:label="$t('auth_gh_client_secret_label')"
|
label="GitHub Client Secret"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,21 +130,21 @@
|
|||||||
>
|
>
|
||||||
<strong class="q-my-none q-mb-sm">Keycloak Auth</strong>
|
<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="row">
|
||||||
<div class="col-md-4 col-sm-12">
|
<div class="col-md-4 col-sm-12 q-pr-sm">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="formData.keycloak_discovery_url"
|
v-model="formData.keycloak_discovery_url"
|
||||||
:label="$t('auth_keycloak_label')"
|
label="Keycloak Discovey URL"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 col-sm-12">
|
<div class="col-md-4 col-sm-12 q-pr-sm">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="formData.keycloak_client_id"
|
v-model="formData.keycloak_client_id"
|
||||||
:label="$t('auth_keycloak_ci_label')"
|
label="Keycloak Client ID"
|
||||||
:hint="$t('auth_keycloak_ci_hint')"
|
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/keycloak/token"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,23 +153,7 @@
|
|||||||
filled
|
filled
|
||||||
v-model="formData.keycloak_client_secret"
|
v-model="formData.keycloak_client_secret"
|
||||||
type="password"
|
type="password"
|
||||||
:label="$t('auth_keycloak_cs_label')"
|
label="Keycloak Client Secret"
|
||||||
>
|
|
||||||
</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>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,37 +281,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-12">
|
<div class="col-12 col-md-12">
|
||||||
<p v-text="$t('callback_url_rules')"></p>
|
<p v-text="$t('wallet_limiter')"></p>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12">
|
<div class="col-4">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="formCallbackUrlRule"
|
type="number"
|
||||||
@keydown.enter="addCallbackUrlRule"
|
v-model.number="formData.lnbits_wallet_limit_max_balance"
|
||||||
type="text"
|
:label="$t('wallet_max_ballance')"
|
||||||
:label="$t('enter_callback_url_rule')"
|
></q-input>
|
||||||
:hint="$t('callback_url_rule_hint')"
|
</div>
|
||||||
>
|
<div class="col-4">
|
||||||
<q-btn
|
<q-input
|
||||||
@click="addCallbackUrlRule"
|
filled
|
||||||
dense
|
type="number"
|
||||||
flat
|
v-model.number="formData.lnbits_wallet_limit_daily_max_withdraw"
|
||||||
icon="add"
|
:label="$t('wallet_limit_max_withdraw_per_day')"
|
||||||
></q-btn>
|
></q-input>
|
||||||
</q-input>
|
</div>
|
||||||
<div>
|
<div class="col-4">
|
||||||
<q-chip
|
<q-input
|
||||||
v-for="rule in formData.lnbits_callback_url_rules"
|
filled
|
||||||
:key="rule"
|
type="number"
|
||||||
removable
|
v-model.number="formData.lnbits_wallet_limit_secs_between_trans"
|
||||||
@remove="removeCallbackUrlRule(rule)"
|
:label="$t('wallet_limit_secs_between_trans')"
|
||||||
color="primary"
|
></q-input>
|
||||||
text-color="white"
|
|
||||||
:label="rule"
|
|
||||||
class="ellipsis"
|
|
||||||
></q-chip>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<q-banner v-if="updateAvailable" class="bg-primary text-white">
|
||||||
|
<q-icon size="28px" name="update"></q-icon>
|
||||||
|
|
||||||
|
<span v-text="$t('update_available', {version: statusData.version})"></span>
|
||||||
|
<template v-slot:action>
|
||||||
|
<a
|
||||||
|
class="q-btn"
|
||||||
|
color="white"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://github.com/lnbits/lnbits/releases"
|
||||||
|
v-text="$t('releases')"
|
||||||
|
></a>
|
||||||
|
</template>
|
||||||
|
</q-banner>
|
||||||
|
<q-banner v-if="!updateAvailable" class="bg-green text-white">
|
||||||
|
<q-icon size="28px" name="checkmark"></q-icon>
|
||||||
|
<span v-text="$t('latest_update', {version: statusData.version})"></span>
|
||||||
|
</q-banner>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<q-table
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
:rows="statusData.notifications"
|
||||||
|
:columns="statusDataTable.columns"
|
||||||
|
:no-data-label="$t('no_notifications')"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width> </q-th>
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
v-text="col.label"
|
||||||
|
></q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width class="text-center">
|
||||||
|
<q-icon
|
||||||
|
v-if="props.row.type === 'update'"
|
||||||
|
size="18px"
|
||||||
|
name="update"
|
||||||
|
color="green"
|
||||||
|
></q-icon>
|
||||||
|
<q-icon
|
||||||
|
v-if="props.row.type === 'warning'"
|
||||||
|
size="18px"
|
||||||
|
name="warning"
|
||||||
|
color="red"
|
||||||
|
></q-icon>
|
||||||
|
</q-td>
|
||||||
|
<q-td
|
||||||
|
auto-width
|
||||||
|
key="date"
|
||||||
|
:props="props"
|
||||||
|
v-text="formatDate(props.row.date)"
|
||||||
|
>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="message" :props="props"
|
||||||
|
><span v-text="props.row.message"></span
|
||||||
|
><a
|
||||||
|
v-if="props.row.link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
:href="props.row.link"
|
||||||
|
v-text="$t('more')"
|
||||||
|
></a
|
||||||
|
></q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
@@ -1,144 +1,79 @@
|
|||||||
<q-tab-panel name="server">
|
<q-tab-panel name="server">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="q-my-none"><span v-text="$t('currency_settings')"></span></h6>
|
<h6 class="q-my-none">Currency Settings</h6>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('allowed_currencies')"></span></p>
|
<p>Allowed currencies</p>
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_allowed_currencies"
|
v-model="formData.lnbits_allowed_currencies"
|
||||||
multiple
|
multiple
|
||||||
:hint="$t('allowed_currencies_hint')"
|
hint="Limit the number of available fiat currencies"
|
||||||
:label="$t('allowed_currencies')"
|
label="Allowed currencies"
|
||||||
:options="{{ currencies | safe }}"
|
:options="{{ currencies | safe }}"
|
||||||
></q-select>
|
></q-select>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('default_account_currency')"></span></p>
|
<p>Default Accounting Currency</p>
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_default_accounting_currency"
|
v-model="formData.lnbits_default_accounting_currency"
|
||||||
clearable
|
clearable
|
||||||
:hint="$t('default_account_currency_hint')"
|
hint="Default currency for accounting"
|
||||||
:label="$t('currency')"
|
label="Currency"
|
||||||
:options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}"
|
:options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}"
|
||||||
></q-select>
|
></q-select>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
<br />
|
||||||
<h6 class="q-my-none"><span v-text="$t('payments')"></span></h6>
|
<h6 class="q-my-none">Service Fee</h6>
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-md-3">
|
|
||||||
<p><span v-text="$t('max_outgoing_payment_amount')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="number"
|
|
||||||
v-model.number="formData.lnbits_max_outgoing_payment_amount_sats"
|
|
||||||
:label="$t('max_outgoing_payment_amount')"
|
|
||||||
step="1"
|
|
||||||
min="0"
|
|
||||||
:hint="$t('max_outgoing_payment_amount_desc')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-3">
|
|
||||||
<p><span v-text="$t('max_incoming_payment_amount')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="number"
|
|
||||||
v-model.number="formData.lnbits_max_incoming_payment_amount_sats"
|
|
||||||
:label="$t('max_incoming_payment_amount')"
|
|
||||||
step="1"
|
|
||||||
min="0"
|
|
||||||
:hint="$t('max_incoming_payment_amount_desc')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
|
||||||
<h6 class="q-my-none"><span v-text="$t('wallet_limiter')"></span></h6>
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-md-3">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="number"
|
|
||||||
v-model.number="formData.lnbits_wallet_limit_max_balance"
|
|
||||||
:label="$t('wallet_max_ballance')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-3">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="number"
|
|
||||||
v-model.number="formData.lnbits_wallet_limit_daily_max_withdraw"
|
|
||||||
:label="$t('wallet_limit_max_withdraw_per_day')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-3">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="number"
|
|
||||||
v-model.number="formData.lnbits_wallet_limit_secs_between_trans"
|
|
||||||
:label="$t('wallet_limit_secs_between_trans')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-3">
|
|
||||||
<q-toggle
|
|
||||||
v-model="formData.lnbits_only_allow_incoming_payments"
|
|
||||||
:label="$t('only_incoming_payments_allowed')"
|
|
||||||
><q-tooltip v-text="$t('disable_outgoing_payments')"></q-tooltip
|
|
||||||
></q-toggle>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
|
||||||
<h6 class="q-my-none"><span v-text="$t('service_fee')"></span></h6>
|
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('service_fee')"></span></p>
|
<p>Service Fee</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="number"
|
type="number"
|
||||||
v-model.number="formData.lnbits_service_fee"
|
v-model.number="formData.lnbits_service_fee"
|
||||||
:label="$t('service_fee_label')"
|
label="Service fee (%)"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
:hint="$t('service_fee_hint')"
|
hint="Fee charged per tx (%)"
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('service_fee_max')"></span></p>
|
<p>Service fee max</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="number"
|
type="number"
|
||||||
v-model.number="formData.lnbits_service_fee_max"
|
v-model.number="formData.lnbits_service_fee_max"
|
||||||
:label="$t('service_fee_max_label')"
|
label="Service fee max (sats)"
|
||||||
:hint="$t('service_fee_max_hint')"
|
hint="Max service fee to charge in (sats)"
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('fee_wallet')"></span></p>
|
<p>Fee Wallet</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_service_fee_wallet"
|
v-model="formData.lnbits_service_fee_wallet"
|
||||||
:label="$t('fee_wallet_label')"
|
label="Fee wallet (wallet ID)"
|
||||||
:hint="$t('fee_wallet_hint')"
|
hint="Wallet ID to send funds to"
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('disable_fee_internal')"></span></p>
|
<p>Disable Service Fee for Internal Payments</p>
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-text="$t('disable_fee')"></q-item-label>
|
<q-item-label>Disable Fee</q-item-label>
|
||||||
<q-item-label
|
<q-item-label caption
|
||||||
caption
|
>Disable Service Fee for Internal Lightning
|
||||||
v-text="$t('disable_fee_internal_desc')"
|
Payments</q-item-label
|
||||||
></q-item-label>
|
>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
|
|||||||
@@ -1,72 +1,82 @@
|
|||||||
<q-tab-panel name="site_customisation">
|
<q-tab-panel name="site_customisation">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none"><span v-text="$t('ui_management')"></span></h6>
|
<h6 class="q-my-none">UI Management</h6>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-5">
|
<div class="col-12 col-md-5">
|
||||||
<p><span v-text="$t('ui_site_title')"></span></p>
|
<p>Site Title</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_site_title"
|
v-model="formData.lnbits_site_title"
|
||||||
:label="$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')"
|
label="Site title"
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-5">
|
<div class="col-12 col-md-5">
|
||||||
<p><span v-text="$t('ui_site_tagline')"></span></p>
|
<p>Site Tagline</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_site_tagline"
|
v-model="formData.lnbits_site_tagline"
|
||||||
:label="$t('ui_site_tagline')"
|
label="Site tagline"
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-2 q-mt-xl">
|
<div class="col-12 col-md-2 q-mt-xl">
|
||||||
<q-toggle
|
<q-toggle
|
||||||
:tip="$t('ui_toggle_elements_tip')"
|
tip="Remove homepage elements like 'runs on' etc"
|
||||||
v-model="formData.lnbits_show_home_page_elements"
|
v-model="formData.lnbits_show_home_page_elements"
|
||||||
:label="formData.lnbits_show_home_page_elements ? $t('ui_elements_enable') : $t('ui_elements_disable')"
|
:label="formData.lnbits_show_home_page_elements ? 'Enable elements on homepage' : 'Disable elements on homepage'"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p><span v-text="$t('ui_site_description')"></span></p>
|
<p>Site Description</p>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="formData.lnbits_site_description"
|
v-model="formData.lnbits_site_description"
|
||||||
filled
|
filled
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:hint="$t('ui_site_description_hint')"
|
hint="Use plain text, Markdown, or raw HTML"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<p><span v-text="$t('ui_default_wallet_name')"></span></p>
|
<p>Default Wallet Name</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_default_wallet_name"
|
v-model="formData.lnbits_default_wallet_name"
|
||||||
:label="$t('lnbits_wallet')"
|
label="LNbits wallet"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
<p>Denomination</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="text"
|
||||||
|
v-model="formData.lnbits_denomination"
|
||||||
|
label="sats"
|
||||||
|
hint="The name for the FakeWallet token"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<p>QR code logo</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_qr_logo"
|
v-model="formData.lnbits_qr_logo"
|
||||||
label="https://example.com/image.svg"
|
label="https://example.com/image.svg"
|
||||||
:hint="$t('ui_qr_code_logo_hint')"
|
hint="URL to logo image in QR code"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-4">
|
||||||
<p><span v-text="$t('ui_custom_badge')"></span></p>
|
<p>Custom Badge</p>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-8">
|
<div class="col-12 col-md-8">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -74,7 +84,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
tip="Custom Badge"
|
tip="Custom Badge"
|
||||||
v-model.trim="formData.lnbits_custom_badge"
|
v-model.trim="formData.lnbits_custom_badge"
|
||||||
:label="$t('ui_custom_badge_label')"
|
label="Custom Badge 'USE WITH CAUTION - LNbits wallet is still in BETA'"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
@@ -82,127 +92,67 @@
|
|||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_custom_badge_color"
|
v-model="formData.lnbits_custom_badge_color"
|
||||||
:options="colors"
|
:options="colors"
|
||||||
:label="$t('ui_custom_badge_color_label')"
|
label="Custom badge color"
|
||||||
></q-select>
|
></q-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<p><span v-text="$t('ui_custom_image')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="text"
|
|
||||||
tip="Custom Image"
|
|
||||||
v-model.trim="formData.lnbits_custom_image"
|
|
||||||
:label="$t('ui_custom_image_label')"
|
|
||||||
:hint="$t('ui_custom_image_hint')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('themes')"></span></p>
|
<p>Themes</p>
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_theme_options"
|
v-model="formData.lnbits_theme_options"
|
||||||
multiple
|
multiple
|
||||||
:hint="$t('themes_hint')"
|
hint="Choose themes available for users"
|
||||||
:options="lnbits_theme_options"
|
:options="lnbits_theme_options"
|
||||||
:label="$t('themes')"
|
label="Themes"
|
||||||
></q-select>
|
></q-select>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('custom_logo')"></span></p>
|
<p>Custom Logo</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_custom_logo"
|
v-model="formData.lnbits_custom_logo"
|
||||||
label="https://example.com/image.png"
|
label="https://example.com/image.png"
|
||||||
:hint="$t('custom_logo_hint')"
|
hint="URL to logo image"
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('ad_space_title')"></span></p>
|
<p>Ad Space Title</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_ad_space_title"
|
v-model="formData.lnbits_ad_space_title"
|
||||||
:label="$t('ad_space_title_label')"
|
label="Supported by"
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('ad_slots')"></span></p>
|
<p>Advertisement Slots</p>
|
||||||
<q-input
|
<q-input
|
||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_ad_space"
|
v-model="formData.lnbits_ad_space"
|
||||||
type="text"
|
type="text"
|
||||||
:label="$t('ad_slots_label')"
|
label="url;img_light_url;img_dark_url, url..."
|
||||||
:hint="$t('ad_slots_hint')"
|
hint="Ad url and image filepaths in CSV format, extensions can choose to honor"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formData.lnbits_ad_space_enabled"
|
v-model="formData.lnbits_ad_space_enabled"
|
||||||
:label="formData.lnbits_ad_space_enabled ? $t('ads_enabled') : $t('ads_disabled')"
|
:label="formData.lnbits_ad_space_enabled ? 'Ads enabled' : 'Ads disabled'"
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p><span v-text="$t('ui_default_theme')"></span></p>
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col">
|
|
||||||
<q-select
|
|
||||||
v-model="formData.lnbits_default_border"
|
|
||||||
:options="globalBorderOptions"
|
|
||||||
label="Border"
|
|
||||||
@update:model-value="applyGlobalBorder"
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-select
|
|
||||||
v-model="formData.lnbits_default_theme"
|
|
||||||
:options="lnbits_theme_options"
|
|
||||||
label="Theme"
|
|
||||||
@update:model-value="applyGlobalTheme"
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-select
|
|
||||||
v-model="formData.lnbits_default_reaction"
|
|
||||||
:options="reactionOptions"
|
|
||||||
label="Payment reaction"
|
|
||||||
@update:model-value="applyGlobalReaction"
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-input
|
|
||||||
type="text"
|
|
||||||
v-model="formData.lnbits_default_bgimage"
|
|
||||||
label="Background Image"
|
|
||||||
@update:model-value="applyGlobalBgimage"
|
|
||||||
hint="This must be a trusted source. It can change the content and it can log your IP address."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-toggle
|
|
||||||
type="bool"
|
|
||||||
v-model="formData.lnbits_default_gradient"
|
|
||||||
color="primary"
|
|
||||||
label="Gradient Background"
|
|
||||||
>
|
|
||||||
</q-toggle>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
<q-tab-panel name="users">
|
<q-tab-panel name="users">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none q-mb-sm">
|
<h6 class="q-my-none q-mb-sm">User Management</h6>
|
||||||
<span v-text="$t('user_management')"></span>
|
|
||||||
</h6>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-6 q-pr-sm">
|
<div class="col-12 col-md-6 q-pr-sm">
|
||||||
<p><span v-text="$t('admin_users')"></span></p>
|
<p>Admin Users</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="formAddAdmin"
|
v-model="formAddAdmin"
|
||||||
@keydown.enter="addAdminUser"
|
@keydown.enter="addAdminUser"
|
||||||
type="text"
|
type="text"
|
||||||
:label="$t('admin_users_label')"
|
label="User ID"
|
||||||
:hint="$t('admin_users_hint')"
|
hint="Users with admin privileges"
|
||||||
>
|
>
|
||||||
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
|
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -33,14 +31,14 @@
|
|||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('allowed_users')"></span></p>
|
<p>Allowed Users</p>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="formAddUser"
|
v-model="formAddUser"
|
||||||
@keydown.enter="addAllowedUser"
|
@keydown.enter="addAllowedUser"
|
||||||
type="text"
|
type="text"
|
||||||
:label="$t('allowed_users_label')"
|
label="User ID"
|
||||||
:hint="$t('allowed_users_hint')"
|
hint="Only these users can use LNbits"
|
||||||
>
|
>
|
||||||
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
|
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -60,11 +58,10 @@
|
|||||||
<br />
|
<br />
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-text="$t('allow_creation_user')"></q-item-label>
|
<q-item-label>Allow creation of new users</q-item-label>
|
||||||
<q-item-label
|
<q-item-label caption
|
||||||
caption
|
>Allow creation of new users on the index page</q-item-label
|
||||||
v-text="$t('allow_creation_user_desc')"
|
>
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
|
|||||||
@@ -1,75 +1,60 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %} {% from "macros.jinja"
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
%} {% block page %}
|
||||||
{% endblock %} {% block page %}
|
<div class="row q-col-gutter-md justify-center">
|
||||||
|
<div class="col q-mb-md">
|
||||||
|
<q-btn
|
||||||
|
:label="$t('save')"
|
||||||
|
color="primary"
|
||||||
|
@click="updateSettings"
|
||||||
|
:disabled="!checkChanges"
|
||||||
|
>
|
||||||
|
<q-tooltip v-if="checkChanges">
|
||||||
|
<span v-text="$t('save_tooltip')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
<q-badge
|
||||||
<div class="col-12">
|
v-if="checkChanges"
|
||||||
<q-card>
|
color="red"
|
||||||
<div class="q-pa-sm">
|
rounded
|
||||||
<div class="row items-center justify-between q-gutter-xs">
|
floating
|
||||||
<div class="col">
|
style="padding: 6px; border-radius: 6px"
|
||||||
<q-btn
|
/>
|
||||||
:label="$t('save')"
|
</q-btn>
|
||||||
color="primary"
|
|
||||||
@click="updateSettings"
|
|
||||||
:disabled="!checkChanges"
|
|
||||||
>
|
|
||||||
<q-tooltip v-if="checkChanges">
|
|
||||||
<span v-text="$t('save_tooltip')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
|
|
||||||
<q-badge
|
<q-btn
|
||||||
v-if="checkChanges"
|
v-if="isSuperUser"
|
||||||
color="red"
|
:label="$t('restart')"
|
||||||
rounded
|
color="primary"
|
||||||
floating
|
@click="restartServer"
|
||||||
style="padding: 6px; border-radius: 6px"
|
class="q-ml-md"
|
||||||
/>
|
>
|
||||||
</q-btn>
|
<q-tooltip v-if="needsRestart">
|
||||||
|
<span v-text="$t('restart_tooltip')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
|
||||||
<q-btn
|
<q-badge
|
||||||
v-if="isSuperUser"
|
v-if="needsRestart"
|
||||||
:label="$t('restart')"
|
color="red"
|
||||||
color="primary"
|
rounded
|
||||||
@click="restartServer"
|
floating
|
||||||
class="q-ml-md"
|
style="padding: 6px; border-radius: 6px"
|
||||||
>
|
/>
|
||||||
<q-tooltip v-if="needsRestart">
|
</q-btn>
|
||||||
<span v-text="$t('restart_tooltip')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
|
|
||||||
<q-badge
|
<q-btn :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
|
||||||
v-if="needsRestart"
|
|
||||||
color="red"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
style="padding: 6px; border-radius: 6px"
|
|
||||||
/>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
:label="$t('download_backup')"
|
flat
|
||||||
flat
|
v-if="isSuperUser"
|
||||||
@click="downloadBackup"
|
:label="$t('reset_defaults')"
|
||||||
></q-btn>
|
color="primary"
|
||||||
|
@click="deleteSettings"
|
||||||
<q-btn
|
class="float-right"
|
||||||
flat
|
>
|
||||||
v-if="isSuperUser"
|
<q-tooltip>
|
||||||
:label="$t('reset_defaults')"
|
<span v-text="$t('reset_defaults_tooltip')"></span>
|
||||||
color="primary"
|
</q-tooltip>
|
||||||
@click="deleteSettings"
|
</q-btn>
|
||||||
class="float-right"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="$t('reset_defaults_tooltip')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -115,13 +100,6 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
><span v-text="$t('exchanges')"></span></q-tooltip
|
><span v-text="$t('exchanges')"></span></q-tooltip
|
||||||
></q-tab>
|
></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
|
<q-tab
|
||||||
name="users"
|
name="users"
|
||||||
icon="group"
|
icon="group"
|
||||||
@@ -156,14 +134,6 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
><span v-text="$t('audit')"></span></q-tooltip
|
><span v-text="$t('audit')"></span></q-tooltip
|
||||||
></q-tab>
|
></q-tab>
|
||||||
<q-tab
|
|
||||||
name="library"
|
|
||||||
icon="image"
|
|
||||||
:label="$q.screen.gt.sm ? $t('library') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('library')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
<q-tab
|
||||||
style="word-break: break-all"
|
style="word-break: break-all"
|
||||||
name="site_customisation"
|
name="site_customisation"
|
||||||
@@ -178,27 +148,22 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
|
|
||||||
<template v-slot:after>
|
<template v-slot:after>
|
||||||
<q-form name="settings_form" id="settings_form">
|
<q-form name="settings_form" id="settings_form">
|
||||||
<q-scroll-area style="height: 100vh">
|
<q-tab-panels
|
||||||
<q-tab-panels
|
v-model="tab"
|
||||||
v-model="tab"
|
animated
|
||||||
animated
|
swipeable
|
||||||
swipeable
|
vertical
|
||||||
vertical
|
transition-prev="jump-up"
|
||||||
scroll
|
transition-next="jump-up"
|
||||||
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_funding.html" %} {% include
|
{% include "admin/_tab_exchange_providers.html" %} {% include
|
||||||
"admin/_tab_users.html" %} {% include "admin/_tab_server.html"
|
"admin/_tab_extensions.html" %} {% include
|
||||||
%} {% include "admin/_tab_exchange_providers.html" %}{% include
|
"admin/_tab_notifications.html" %} {% include
|
||||||
"admin/_tab_fiat_providers.html" %} {% include
|
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
||||||
"admin/_tab_extensions.html" %} {% include
|
%}{% include "admin/_tab_audit.html"%}
|
||||||
"admin/_tab_notifications.html" %} {% include
|
</q-tab-panels>
|
||||||
"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-form>
|
</q-form>
|
||||||
</template>
|
</template>
|
||||||
</q-splitter>
|
</q-splitter>
|
||||||
@@ -208,9 +173,7 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
|
|
||||||
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
|
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
|
||||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||||
<div class="q-mb-md">
|
<strong>Create Currecny Ticker Converter</strong>
|
||||||
<strong v-text="$t('create_ticker_converter')"></strong>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 q-mb-md">
|
<div class="col-12 q-mb-md">
|
||||||
<q-select
|
<q-select
|
||||||
@@ -248,4 +211,7 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,63 +1,36 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
<!---->
|
%} {% block page %}
|
||||||
{% 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">
|
|
||||||
<!-- Optional: Add content here if needed -->
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
v-if="g.user.admin"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
icon="settings"
|
|
||||||
to="/admin#audit"
|
|
||||||
>
|
|
||||||
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center q-mb-lg">
|
<div class="row q-col-gutter-md justify-center q-mb-lg">
|
||||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
||||||
<q-card class="q-pt-sm">
|
<q-card class="q-pt-sm">
|
||||||
<strong v-text="$t('components')"></strong>
|
<strong>Components</strong>
|
||||||
<div style="width: 250px" class="q-pa-sm">
|
<div style="width: 250px" class="q-pa-sm">
|
||||||
<canvas v-if="chartsReady" ref="componentUseChart"></canvas>
|
<canvas ref="componentUseChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
||||||
<q-card class="q-pt-sm">
|
<q-card class="q-pt-sm">
|
||||||
<strong v-text="$t('long_running_endpoints')"></strong>
|
<strong>To 5 Long Running Endpoints</strong>
|
||||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
||||||
<canvas v-if="chartsReady" ref="longDurationChart"></canvas>
|
<canvas ref="longDurationChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
||||||
<q-card class="q-pt-sm">
|
<q-card class="q-pt-sm">
|
||||||
<strong v-text="$t('http_request_methods')"></strong>
|
<strong>HTTP Request Methods</strong>
|
||||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
||||||
<canvas v-if="chartsReady" ref="requestMethodChart"></canvas>
|
<canvas ref="requestMethodChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
||||||
<q-card class="q-pt-sm">
|
<q-card class="q-pt-sm">
|
||||||
<strong v-text="$t('http_response_codes')"></strong>
|
<strong>HTTP Response Codes</strong>
|
||||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
||||||
<canvas v-if="chartsReady" ref="responseCodeChart"></canvas>
|
<canvas ref="responseCodeChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,6 +83,11 @@
|
|||||||
|
|
||||||
<span v-else v-text="col.label"></span>
|
<span v-else v-text="col.label"></span>
|
||||||
</q-th>
|
</q-th>
|
||||||
|
<q-th>
|
||||||
|
<q-btn flat round icon="settings" tag="a" href="/admin#audit"
|
||||||
|
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
||||||
|
></q-btn>
|
||||||
|
</q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
@@ -124,9 +102,7 @@
|
|||||||
class="cursor-pointer q-mr-xs"
|
class="cursor-pointer q-mr-xs"
|
||||||
@click="showDetailsDialog(props.row)"
|
@click="showDetailsDialog(props.row)"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip>Request Details</q-tooltip>
|
||||||
><span v-text="$t('request_details')"></span
|
|
||||||
></q-tooltip>
|
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<span v-text="formatDate(props.row.created_at)"></span>
|
<span v-text="formatDate(props.row.created_at)"></span>
|
||||||
@@ -170,7 +146,7 @@
|
|||||||
|
|
||||||
<q-dialog v-model="auditDetailsDialog.show" position="top">
|
<q-dialog v-model="auditDetailsDialog.show" position="top">
|
||||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||||
<strong v-text="$t('http_request_details')"></strong>
|
<strong>HTTP Request Details</strong>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
@@ -198,4 +174,6 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script src="{{ static_url_for('static', 'js/audit.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -48,17 +48,6 @@
|
|||||||
class="cursor-pointer q-ml-sm"
|
class="cursor-pointer q-ml-sm"
|
||||||
@click="copyText(wallet.adminkey)"
|
@click="copyText(wallet.adminkey)"
|
||||||
></q-icon>
|
></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>
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -81,17 +70,6 @@
|
|||||||
class="cursor-pointer q-ml-sm"
|
class="cursor-pointer q-ml-sm"
|
||||||
@click="copyText(wallet.inkey)"
|
@click="copyText(wallet.inkey)"
|
||||||
></q-icon>
|
></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>
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -107,12 +85,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
|
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code
|
<code>{"X-Api-Key": "<i v-text="wallet.inkey"></i>"}</code><br />
|
||||||
>{"X-Api-Key": "<i
|
|
||||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
|
||||||
></i
|
|
||||||
>"}</code
|
|
||||||
><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 200 OK (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
@@ -123,8 +96,7 @@
|
|||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
|
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
|
||||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i
|
<i v-text="wallet.inkey"></i>"</code
|
||||||
>"</code
|
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -140,12 +112,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
|
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code
|
<code>{"X-Api-Key": "<i v-text="wallet.inkey"></i>"}</code><br />
|
||||||
>{"X-Api-Key": "<i
|
|
||||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
|
||||||
></i
|
|
||||||
>"}</code
|
|
||||||
><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
<code
|
<code
|
||||||
>{"out": false, "amount": <int>, "memo": <string>,
|
>{"out": false, "amount": <int>, "memo": <string>,
|
||||||
@@ -163,9 +130,8 @@
|
|||||||
<code
|
<code
|
||||||
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
||||||
'{"out": false, "amount": <int>, "memo": <string>}' -H
|
'{"out": false, "amount": <int>, "memo": <string>}' -H
|
||||||
"X-Api-Key:
|
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
|
||||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
application/json"</code
|
||||||
"Content-type: application/json"</code
|
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -252,12 +218,7 @@
|
|||||||
/api/v1/payments/<payment_hash></code
|
/api/v1/payments/<payment_hash></code
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code
|
<code>{"X-Api-Key": "{{ wallet.inkey }}"}</code>
|
||||||
>{"X-Api-Key": "<i
|
|
||||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
|
||||||
></i
|
|
||||||
>"}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 200 OK (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
@@ -266,42 +227,10 @@
|
|||||||
<code
|
<code
|
||||||
>curl -X GET
|
>curl -X GET
|
||||||
<span v-text="baseUrl"></span>api/v1/payments/<payment_hash> -H
|
<span v-text="baseUrl"></span>api/v1/payments/<payment_hash> -H
|
||||||
"X-Api-Key:
|
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
|
||||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
application/json"</code
|
||||||
"Content-type: application/json"</code
|
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-pink">WS</span>
|
|
||||||
/api/v1/ws/<invoice_key></code
|
|
||||||
>
|
|
||||||
<h5
|
|
||||||
class="text-caption q-mt-sm q-mb-none"
|
|
||||||
v-text="$t('websocket_example')"
|
|
||||||
></h5>
|
|
||||||
<code
|
|
||||||
>wscat -c <span v-text="websocketUrl"></span>/<span
|
|
||||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
|
||||||
></span
|
|
||||||
></code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)/payments
|
|
||||||
</h5>
|
|
||||||
<code>{"balance": <int>, "payment": <object>}</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-card-section>
|
|
||||||
<p v-text="$t('reset_wallet_keys_desc')"></p>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="red-10"
|
|
||||||
@click="resetKeys()"
|
|
||||||
:label="$t('reset_wallet_keys')"
|
|
||||||
></q-btn>
|
|
||||||
</q-card-section>
|
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|||||||
@@ -1,51 +1,39 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
{% extends "base.html" %}
|
||||||
<!---->
|
<!---->
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
{% from "macros.jinja" import window_vars with context %}
|
||||||
<!---->
|
<!---->
|
||||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
{% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script src="{{ static_url_for('static', 'js/account.js') }}"></script>
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
{% endblock %} {% block page %}
|
||||||
<div class="col-12">
|
|
||||||
<q-card>
|
|
||||||
<div>
|
|
||||||
<div class="q-gutter-y-md">
|
|
||||||
<q-tabs v-model="tab" align="left">
|
|
||||||
<q-tab
|
|
||||||
name="user"
|
|
||||||
:label="$t('account_settings')"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="theme"
|
|
||||||
:label="$t('look_and_feel')"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="api_acls"
|
|
||||||
:label="$t('access_control_list')"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
></q-tab>
|
|
||||||
</q-tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div v-if="user" class="col-md-12 col-lg-6 q-gutter-y-md">
|
<div v-if="user" class="col-12 col-md-6 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
<q-tabs v-model="tab" align="justify">
|
||||||
|
<q-tab
|
||||||
|
name="user"
|
||||||
|
:label="$t('account_settings')"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
></q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="theme"
|
||||||
|
:label="$t('look_and_feel')"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
></q-tab>
|
||||||
|
</q-tabs>
|
||||||
<q-tab-panels v-model="tab">
|
<q-tab-panels v-model="tab">
|
||||||
<q-tab-panel name="user">
|
<q-tab-panel name="user">
|
||||||
<div v-if="credentialsData.show">
|
<div v-if="credentialsData.show">
|
||||||
|
<q-separator></q-separator>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="q-my-none">
|
<h4 class="q-my-none">
|
||||||
<span v-text="$t('password')"></span>
|
<span v-text="$t('password_config')"></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -102,7 +90,7 @@
|
|||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
:label="$t('update_password')"
|
:label="$t('change_password')"
|
||||||
>
|
>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -110,7 +98,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="col q-mb-sm">
|
<div class="col q-mb-sm">
|
||||||
<h4 class="q-my-none">
|
<h4 class="q-my-none">
|
||||||
Nostr <span v-text="$t('pubkey')"></span>
|
<span v-text="$t('pubkey')"></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -141,10 +129,11 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<q-card-section v-if="user.extra.picture">
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-img
|
<q-img
|
||||||
|
v-if="user.extra.picture"
|
||||||
style="max-width: 100px"
|
style="max-width: 100px"
|
||||||
:src="user.extra.picture"
|
:src="user.extra.picture"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
@@ -152,6 +141,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -272,21 +262,10 @@
|
|||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-input
|
|
||||||
v-model="user.external_id"
|
|
||||||
:label="$t('external_id')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
readonly
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="user.extra.picture"
|
v-model="user.extra.picture"
|
||||||
:label="$t('picture')"
|
:label="$t('picture')"
|
||||||
filled
|
filled
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -298,7 +277,7 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showUpdateCredentials()"
|
@click="showUpdateCredentials()"
|
||||||
:label="$t('change_password')"
|
:label="$t('update_credentials')"
|
||||||
filled
|
filled
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
@@ -313,7 +292,8 @@
|
|||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
icon="settings"
|
icon="settings"
|
||||||
to="/admin#site_customisation"
|
tag="a"
|
||||||
|
href="/admin#site_customisation"
|
||||||
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
@@ -346,111 +326,95 @@
|
|||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</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="row q-mb-md">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span v-text="$t('color_scheme')"></span>
|
<span v-text="$t('color_scheme')"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="allowedThemes.includes('classic')"
|
v-if="g.allowedThemes.includes('classic')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="changeTheme('classic')"
|
@click="changeColor('classic')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="deep-purple"
|
color="deep-purple"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>classic</q-tooltip>
|
><q-tooltip>classic</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="allowedThemes.includes('bitcoin')"
|
v-if="g.allowedThemes.includes('bitcoin')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="changeTheme('bitcoin')"
|
@click="changeColor('bitcoin')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="deep-orange"
|
color="orange"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>bitcoin</q-tooltip>
|
><q-tooltip>bitcoin</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="allowedThemes.includes('mint')"
|
v-if="g.allowedThemes.includes('mint')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="changeTheme('mint')"
|
@click="changeColor('mint')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="green"
|
color="green"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>mint</q-tooltip> </q-btn
|
><q-tooltip>mint</q-tooltip> </q-btn
|
||||||
><q-btn
|
><q-btn
|
||||||
v-if="allowedThemes.includes('autumn')"
|
v-if="g.allowedThemes.includes('autumn')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="changeTheme('autumn')"
|
@click="changeColor('autumn')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="brown"
|
color="brown"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>autumn</q-tooltip>
|
><q-tooltip>autumn</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="allowedThemes.includes('monochrome')"
|
v-if="g.allowedThemes.includes('monochrome')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="changeTheme('monochrome')"
|
@click="changeColor('monochrome')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="grey"
|
color="grey"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>monochrome</q-tooltip>
|
><q-tooltip>monochrome</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="allowedThemes.includes('salvador')"
|
v-if="g.allowedThemes.includes('salvador')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="changeTheme('salvador')"
|
@click="changeColor('salvador')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="blue-10"
|
color="blue-10"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>elSalvador</q-tooltip>
|
><q-tooltip>elSalvador</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="allowedThemes.includes('freedom')"
|
v-if="g.allowedThemes.includes('freedom')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="changeTheme('freedom')"
|
@click="changeColor('freedom')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="pink-13"
|
color="pink-13"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>freedom</q-tooltip>
|
><q-tooltip>freedom</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="allowedThemes.includes('cyber')"
|
v-if="g.allowedThemes.includes('cyber')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="changeTheme('cyber')"
|
@click="changeColor('cyber')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="light-green-9"
|
color="light-green-9"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>cyber</q-tooltip>
|
><q-tooltip>cyber</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="allowedThemes.includes('flamingo')"
|
v-if="g.allowedThemes.includes('flamingo')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="changeTheme('flamingo')"
|
@click="changeColor('flamingo')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="pink-3"
|
color="pink-3"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -464,9 +428,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="bgimageChoice"
|
v-model="backgroundImage"
|
||||||
:label="$t('background_image')"
|
:label="$t('background_image')"
|
||||||
@update:model-value="applyBackgroundImage"
|
@update:model-value="backgroundImageFunc"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('background_image')"></span
|
><span v-text="$t('background_image')"></span
|
||||||
@@ -479,18 +443,19 @@
|
|||||||
<span v-text="$t('gradient_background')"></span>
|
<span v-text="$t('gradient_background')"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<q-toggle
|
<q-btn
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
|
@click="toggleGradient"
|
||||||
icon="gradient"
|
icon="gradient"
|
||||||
|
size="sm"
|
||||||
v-model="gradientChoice"
|
v-model="gradientChoice"
|
||||||
@update:model-value="applyGradient"
|
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('toggle_gradient')"></span
|
><span v-text="$t('toggle_gradient')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-toggle>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -499,11 +464,10 @@
|
|||||||
<span v-text="$t('toggle_darkmode')"></span>
|
<span v-text="$t('toggle_darkmode')"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<q-toggle
|
<q-btn
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
v-model="darkChoice"
|
|
||||||
@click="toggleDarkMode"
|
@click="toggleDarkMode"
|
||||||
:icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'"
|
:icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -511,7 +475,7 @@
|
|||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('toggle_darkmode')"></span
|
><span v-text="$t('toggle_darkmode')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-toggle>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
@@ -522,7 +486,7 @@
|
|||||||
<q-select
|
<q-select
|
||||||
v-model="borderChoice"
|
v-model="borderChoice"
|
||||||
:options="borderOptions"
|
:options="borderOptions"
|
||||||
label="Borders"
|
label="Reactions"
|
||||||
@update:model-value="applyBorder"
|
@update:model-value="applyBorder"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
@@ -532,9 +496,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
<div class="col-4">
|
<div class="col-4">Notifications</div>
|
||||||
<span v-text="$t('notifications')"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<lnbits-notifications-btn
|
<lnbits-notifications-btn
|
||||||
v-if="g.user"
|
v-if="g.user"
|
||||||
@@ -559,236 +521,6 @@
|
|||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
|
||||||
<q-badge v-if="user.admin">
|
|
||||||
<span
|
|
||||||
v-text="$t('access_control_list_admin_warning')"
|
|
||||||
></span>
|
|
||||||
</q-badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row q-mb-md q-gutter-y-md">
|
|
||||||
<div class="col-sm-12 col-md-6">
|
|
||||||
<q-select
|
|
||||||
v-model="selectedApiAcl.id"
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
@update:model-value="handleApiACLSelected"
|
|
||||||
:options="apiAcl.data.map(t => ({label: t.name, value: t.id}))"
|
|
||||||
:label="$t('access_control_list')"
|
|
||||||
dense
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 col-md-6">
|
|
||||||
<q-btn
|
|
||||||
@click="askPasswordAndRunFunction('newApiAclDialog')"
|
|
||||||
filled
|
|
||||||
outline
|
|
||||||
icon="add"
|
|
||||||
:label="$t('access_control_list')"
|
|
||||||
color="grey"
|
|
||||||
class="float-right"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedApiAcl.id">
|
|
||||||
<div class="row q-mb-md">
|
|
||||||
<div class="col-sm-12 col-md-6">
|
|
||||||
<q-select
|
|
||||||
:options="selectedApiAcl.token_id_list.map(t => ({label: t.name, value: t.id}))"
|
|
||||||
v-model="apiAcl.selectedTokenId"
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
:label="$t('api_tokens')"
|
|
||||||
dense
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-12 col-md-6 q-pl-sm">
|
|
||||||
<q-btn
|
|
||||||
v-if="apiAcl.selectedTokenId"
|
|
||||||
@click="askPasswordAndRunFunction('deleteToken')"
|
|
||||||
icon="delete"
|
|
||||||
filled
|
|
||||||
color="negative"
|
|
||||||
class="float-left"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
@click="askPasswordAndRunFunction('newTokenAclDialog')"
|
|
||||||
outline
|
|
||||||
icon="add"
|
|
||||||
:label="$t('api_token')"
|
|
||||||
filled
|
|
||||||
color="grey"
|
|
||||||
class="float-right"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="apiAcl.apiToken" class="row q-mb-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-badge>
|
|
||||||
<span>Use this token in the HTTP</span>
|
|
||||||
<strong>
|
|
||||||
<code>Authorization</code>
|
|
||||||
|
|
||||||
</strong>
|
|
||||||
<span> header.</span>
|
|
||||||
</q-badge>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<table
|
|
||||||
class="full-width lnbits__table-bordered"
|
|
||||||
style="
|
|
||||||
border-collapse: collapse;
|
|
||||||
background-color: grey;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<span class="float-left">Header Name</span>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<span class="float-left">Header Value</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong>Authorization</strong>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="row q-pt-sm">
|
|
||||||
<div class="col-2 q-mt-sm">
|
|
||||||
<strong>Bearer </strong>
|
|
||||||
</div>
|
|
||||||
<div class="col-10">
|
|
||||||
<q-input
|
|
||||||
v-model="apiAcl.apiToken"
|
|
||||||
:label="$t('api_token_id')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
readonly
|
|
||||||
:type="selectedApiAcl.showId ? 'text': 'password'"
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="selectedApiAcl.showId = !selectedApiAcl.showId"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:icon="selectedApiAcl.showId ? 'visibility_off' : 'visibility'"
|
|
||||||
color="black"
|
|
||||||
></q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
@click="copyText(apiAcl.apiToken)"
|
|
||||||
icon="content_copy"
|
|
||||||
color="black"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-badge>
|
|
||||||
<span
|
|
||||||
>Please store this token. It cannot be later
|
|
||||||
retrieved, only revoked.</span
|
|
||||||
>
|
|
||||||
</q-badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-table
|
|
||||||
row-key="path"
|
|
||||||
:rows="selectedApiAcl.endpoints"
|
|
||||||
:columns="apiAcl.columns"
|
|
||||||
v-model:pagination="apiAcl.pagination"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th
|
|
||||||
v-for="col in props.cols"
|
|
||||||
:key="col.name"
|
|
||||||
:props="props"
|
|
||||||
>
|
|
||||||
<q-toggle
|
|
||||||
v-if="col.name == 'read'"
|
|
||||||
v-model="selectedApiAcl.allRead"
|
|
||||||
@update:model-value="handleAllEndpointsReadAccess()"
|
|
||||||
:label="$t('read')"
|
|
||||||
size="sm"
|
|
||||||
></q-toggle>
|
|
||||||
<q-toggle
|
|
||||||
v-else-if="col.name == 'write'"
|
|
||||||
v-model="selectedApiAcl.allWrite"
|
|
||||||
@update:model-value="handleAllEndpointsWriteAccess()"
|
|
||||||
:label="$t('write')"
|
|
||||||
size="sm"
|
|
||||||
></q-toggle>
|
|
||||||
<span v-else v-text="col.label"></span>
|
|
||||||
</q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td>
|
|
||||||
<span v-text="props.row.name"></span>
|
|
||||||
</q-td>
|
|
||||||
<q-td>
|
|
||||||
<span v-text="props.row.path"></span>
|
|
||||||
</q-td>
|
|
||||||
<q-td>
|
|
||||||
<q-toggle size="sm" v-model="props.row.read" />
|
|
||||||
</q-td>
|
|
||||||
<q-td>
|
|
||||||
<q-toggle size="sm" v-model="props.row.write" />
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedApiAcl.id" class="row q-mt-md">
|
|
||||||
<div class="col-sm-12 col-md-6">
|
|
||||||
<q-btn
|
|
||||||
@click="askPasswordAndRunFunction('updateApiACLs')"
|
|
||||||
:label="$t('update')"
|
|
||||||
filled
|
|
||||||
color="primary"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 col-md-6">
|
|
||||||
<q-btn
|
|
||||||
@click="askPasswordAndRunFunction('deleteApiACL')"
|
|
||||||
:label="$t('delete')"
|
|
||||||
icon="delete"
|
|
||||||
color="negative"
|
|
||||||
class="float-right"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
</div>
|
</div>
|
||||||
@@ -804,126 +536,4 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog v-model="apiAcl.showPasswordDialog" position="top">
|
|
||||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
|
||||||
<strong>User Password</strong>
|
|
||||||
<div class="row q-mt-md q-col-gutter-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
v-model="apiAcl.password"
|
|
||||||
type="password"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
label="Password"
|
|
||||||
hint="User password is required for this action."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
@click="runPasswordGuardedFunction()"
|
|
||||||
:label="$t('ok')"
|
|
||||||
color="primary"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
v-text="$t('cancel')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog v-model="apiAcl.showNewAclDialog" position="top">
|
|
||||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
|
||||||
<strong>New API Access Control List</strong>
|
|
||||||
<div class="row q-mt-md q-col-gutter-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input v-model="apiAcl.newAclName" dense filled label="ACL Name">
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn @click="addApiACL()" label="Create" color="primary"></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
v-text="$t('close')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<q-dialog v-model="apiAcl.showNewTokenDialog" position="top">
|
|
||||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
|
||||||
<strong>New API Token</strong>
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input v-model="apiAcl.newTokenName" dense filled label="Token Name">
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
v-model="apiAcl.newTokenExpiry"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
label="Expiration"
|
|
||||||
hit="Expiration time in minutes (default xxx)"
|
|
||||||
>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<q-icon name="event" class="cursor-pointer">
|
|
||||||
<q-popup-proxy
|
|
||||||
cover
|
|
||||||
transition-show="scale"
|
|
||||||
transition-hide="scale"
|
|
||||||
>
|
|
||||||
<q-date v-model="apiAcl.newTokenExpiry" mask="YYYY-MM-DD HH:mm">
|
|
||||||
<div class="row items-center justify-end">
|
|
||||||
<q-btn v-close-popup label="Close" color="primary" flat />
|
|
||||||
</div>
|
|
||||||
</q-date>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon name="access_time" class="cursor-pointer">
|
|
||||||
<q-popup-proxy
|
|
||||||
cover
|
|
||||||
transition-show="scale"
|
|
||||||
transition-hide="scale"
|
|
||||||
>
|
|
||||||
<q-time
|
|
||||||
v-model="apiAcl.newTokenExpiry"
|
|
||||||
mask="YYYY-MM-DD HH:mm"
|
|
||||||
format24h
|
|
||||||
>
|
|
||||||
<div class="row items-center justify-end">
|
|
||||||
<q-btn v-close-popup label="Close" color="primary" flat />
|
|
||||||
</div>
|
|
||||||
</q-time>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn @click="generateApiToken()" label="Create" color="primary"></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
v-text="$t('close')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
<!---->
|
%} {{ window_vars(user, extensions) }}{% block page %}
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
|
||||||
<!---->
|
|
||||||
{% block scripts %} {{ window_vars(user, wallet, extensions, extension_data)
|
|
||||||
}}{% endblock %} {% block page %}
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
<div class="row q-col-gutter-md q-mb-md">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-card>
|
<q-card>
|
||||||
<div>
|
<div class="q-pa-xs">
|
||||||
<div class="q-gutter-y-md">
|
<div class="q-gutter-y-md">
|
||||||
<q-tabs
|
<q-tabs
|
||||||
:model-value="tab"
|
:model-value="tab"
|
||||||
@@ -58,7 +54,8 @@
|
|||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
icon="settings"
|
icon="settings"
|
||||||
to="/admin#extensions"
|
tag="a"
|
||||||
|
href="/admin#extensions"
|
||||||
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
@@ -69,7 +66,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div
|
<div
|
||||||
v-if="filteredExtensions"
|
|
||||||
class="col-12 col-sm-6 col-md-6 col-lg-4"
|
class="col-12 col-sm-6 col-md-6 col-lg-4"
|
||||||
v-for="extension in filteredExtensions"
|
v-for="extension in filteredExtensions"
|
||||||
:key="extension.id + extension.hash"
|
:key="extension.id + extension.hash"
|
||||||
@@ -118,7 +114,6 @@
|
|||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
<div
|
<div
|
||||||
v-if="extension.name"
|
|
||||||
class="text-h5"
|
class="text-h5"
|
||||||
style="cursor: pointer"
|
style="cursor: pointer"
|
||||||
@click="showExtensionDetails(extension.id, extension.details_link)"
|
@click="showExtensionDetails(extension.id, extension.details_link)"
|
||||||
@@ -191,7 +186,7 @@
|
|||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
<div v-if="!extension.inProgress">
|
<div v-if="!extension.inProgress">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
|
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
type="a"
|
type="a"
|
||||||
@@ -199,20 +194,20 @@
|
|||||||
:label="$t('open')"
|
:label="$t('open')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
|
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
|
||||||
flat
|
flat
|
||||||
color="grey-5"
|
color="grey-5"
|
||||||
@click="disableExtension(extension)"
|
@click="disableExtension(extension)"
|
||||||
:label="$t('disable')"
|
:label="$t('disable')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="extension.isAdminOnly && !g.user.admin"
|
v-if="extension.isAdminOnly && !user.admin"
|
||||||
v-text="$t('admin_only')"
|
v-text="$t('admin_only')"
|
||||||
>
|
>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-else-if="extension.isInstalled && extension.isActive && !g.user.extensions.includes(extension.id)"
|
v-else-if="extension.isInstalled && extension.isActive && !user.extensions.includes(extension.id)"
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="enableExtensionForUser(extension)"
|
@click="enableExtensionForUser(extension)"
|
||||||
@@ -235,7 +230,7 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -337,7 +332,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
@@ -436,10 +431,7 @@
|
|||||||
:header-class="getReleaseIconColor(release)"
|
:header-class="getReleaseIconColor(release)"
|
||||||
>
|
>
|
||||||
<div v-if="release.inProgress">
|
<div v-if="release.inProgress">
|
||||||
<q-spinner-bars
|
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||||
color="primary"
|
|
||||||
size="2.55em"
|
|
||||||
></q-spinner-bars>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="release.error">
|
<div v-else-if="release.error">
|
||||||
<q-icon
|
<q-icon
|
||||||
@@ -450,7 +442,7 @@
|
|||||||
></q-icon>
|
></q-icon>
|
||||||
<span v-text="$t('release_details_error')"></span>
|
<span v-text="$t('release_details_error')"></span>
|
||||||
</div>
|
</div>
|
||||||
<q-card class="no-border" v-else>
|
<q-card v-else>
|
||||||
<q-card-section v-if="release.is_version_compatible">
|
<q-card-section v-if="release.is_version_compatible">
|
||||||
<span
|
<span
|
||||||
v-if="release.requiresPayment && !release.paid_sats"
|
v-if="release.requiresPayment && !release.paid_sats"
|
||||||
@@ -625,7 +617,7 @@
|
|||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-spinner-bars v-else color="primary" size="2.55em"></q-spinner-bars>
|
<q-spinner v-else color="primary" size="2.55em"></q-spinner>
|
||||||
<div
|
<div
|
||||||
v-if="selectedExtension"
|
v-if="selectedExtension"
|
||||||
v-show="manageExtensionTab === 'sell'"
|
v-show="manageExtensionTab === 'sell'"
|
||||||
@@ -711,7 +703,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="col">
|
<div v-else class="col">
|
||||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
@@ -959,12 +951,12 @@
|
|||||||
value="false"
|
value="false"
|
||||||
:label="item.name + ` (v${item.latestRelease?.version})`"
|
:label="item.name + ` (v${item.latestRelease?.version})`"
|
||||||
>
|
>
|
||||||
<q-spinner-bars
|
<q-spinner
|
||||||
v-if="item.inProgress"
|
v-if="item.inProgress"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="1.5em"
|
size="1.5em"
|
||||||
class="q-ml-md"
|
class="q-ml-md"
|
||||||
></q-spinner-bars>
|
></q-spinner>
|
||||||
</q-checkbox>
|
</q-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -988,4 +980,674 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script>
|
||||||
|
window.app = Vue.createApp({
|
||||||
|
el: '#vue',
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
slide: 0,
|
||||||
|
fullscreen: false,
|
||||||
|
autoplay: true,
|
||||||
|
searchTerm: '',
|
||||||
|
tab: 'all',
|
||||||
|
manageExtensionTab: 'releases',
|
||||||
|
filteredExtensions: null,
|
||||||
|
updatableExtensions: [],
|
||||||
|
showUninstallDialog: false,
|
||||||
|
showManageExtensionDialog: false,
|
||||||
|
showExtensionDetailsDialog: false,
|
||||||
|
showDropDbDialog: false,
|
||||||
|
showPayToEnableDialog: false,
|
||||||
|
showUpdateAllDialog: false,
|
||||||
|
dropDbExtensionId: '',
|
||||||
|
selectedExtension: null,
|
||||||
|
selectedImage: null,
|
||||||
|
selectedExtensionDetails: null,
|
||||||
|
selectedExtensionRepos: null,
|
||||||
|
selectedRelease: null,
|
||||||
|
uninstallAndDropDb: false,
|
||||||
|
maxStars: 5,
|
||||||
|
paylinkWebsocket: null,
|
||||||
|
user: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
searchTerm(term) {
|
||||||
|
this.filterExtensions(term, this.tab)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTabChanged: function (tab) {
|
||||||
|
this.filterExtensions(this.searchTerm, tab)
|
||||||
|
},
|
||||||
|
filterExtensions: function (term, tab) {
|
||||||
|
// Filter the extensions list
|
||||||
|
function extensionNameContains(searchTerm) {
|
||||||
|
return function (extension) {
|
||||||
|
return (
|
||||||
|
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
extension.shortDescription
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filteredExtensions = this.extensions
|
||||||
|
.filter(e => (tab === 'all' ? !e.isInstalled : true))
|
||||||
|
.filter(e => (tab === 'installed' ? e.isInstalled : true))
|
||||||
|
.filter(e =>
|
||||||
|
tab === 'installed'
|
||||||
|
? e.isActive
|
||||||
|
? true
|
||||||
|
: !!this.g.user.admin
|
||||||
|
: true
|
||||||
|
)
|
||||||
|
.filter(e => (tab === 'featured' ? e.isFeatured : true))
|
||||||
|
.filter(extensionNameContains(term))
|
||||||
|
.map(e => ({
|
||||||
|
...e,
|
||||||
|
details_link:
|
||||||
|
e.installedRelease?.details_link || e.latestRelease?.details_link
|
||||||
|
}))
|
||||||
|
this.tab = tab
|
||||||
|
},
|
||||||
|
|
||||||
|
installExtension: async function (release) {
|
||||||
|
// no longer required to check if the invoice was paid
|
||||||
|
// the install logic has been triggered one way or another
|
||||||
|
this.unsubscribeFromPaylinkWs()
|
||||||
|
|
||||||
|
const extension = this.selectedExtension
|
||||||
|
extension.inProgress = true
|
||||||
|
this.showManageExtensionDialog = false
|
||||||
|
release.payment_hash =
|
||||||
|
release.payment_hash || this.getPaylinkHash(release.pay_link)
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'POST',
|
||||||
|
`/api/v1/extension`,
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
{
|
||||||
|
ext_id: extension.id,
|
||||||
|
archive: release.archive,
|
||||||
|
source_repo: release.source_repo,
|
||||||
|
payment_hash: release.payment_hash,
|
||||||
|
version: release.version
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
extension.isAvailable = true
|
||||||
|
extension.isInstalled = true
|
||||||
|
extension.installedRelease = release
|
||||||
|
this.toggleExtension(extension)
|
||||||
|
extension.inProgress = false
|
||||||
|
this.filteredExtensions = this.extensions.concat([])
|
||||||
|
this.handleTabChanged('installed')
|
||||||
|
this.tab = 'installed'
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.warn(err)
|
||||||
|
extension.inProgress = false
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
uninstallExtension: async function () {
|
||||||
|
const extension = this.selectedExtension
|
||||||
|
this.showManageExtensionDialog = false
|
||||||
|
this.showUninstallDialog = false
|
||||||
|
extension.inProgress = true
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
`/api/v1/extension/${extension.id}`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
extension.isAvailable = false
|
||||||
|
extension.isInstalled = false
|
||||||
|
extension.inProgress = false
|
||||||
|
extension.installedRelease = null
|
||||||
|
this.filteredExtensions = this.extensions.concat([])
|
||||||
|
this.handleTabChanged('installed')
|
||||||
|
this.tab = 'installed'
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Extension uninstalled!'
|
||||||
|
})
|
||||||
|
if (this.uninstallAndDropDb) {
|
||||||
|
this.showDropDb()
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
extension.inProgress = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
dropExtensionDb: async function () {
|
||||||
|
const extension = this.selectedExtension
|
||||||
|
this.showManageExtensionDialog = false
|
||||||
|
this.showDropDbDialog = false
|
||||||
|
this.dropDbExtensionId = ''
|
||||||
|
extension.inProgress = true
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
`/api/v1/extension/${extension.id}/db`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
extension.installedRelease = null
|
||||||
|
extension.inProgress = false
|
||||||
|
extension.hasDatabaseTables = false
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Extension DB deleted!'
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
extension.inProgress = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
toggleExtension(extension) {
|
||||||
|
const action = extension.isActive ? 'activate' : 'deactivate'
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/v1/extension/${extension.id}/${action}`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
timeout: 2000,
|
||||||
|
type: 'positive',
|
||||||
|
message: `Extension '${extension.id}' ${action}d!`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
extension.isActive = false
|
||||||
|
extension.inProgress = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
enableExtensionForUser: function (extension) {
|
||||||
|
if (extension.isPaymentRequired) {
|
||||||
|
this.showPayToEnable(extension)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.enableExtension(extension)
|
||||||
|
},
|
||||||
|
enableExtension: function (extension) {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/v1/extension/${extension.id}/enable`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Extension enabled!'
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.warn(err)
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
disableExtension: function (extension) {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/v1/extension/${extension.id}/disable`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Extension disabled!'
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showPayToEnable: function (extension) {
|
||||||
|
this.selectedExtension = extension
|
||||||
|
this.selectedExtension.payToEnable.paidAmount =
|
||||||
|
extension.payToEnable.amount
|
||||||
|
this.selectedExtension.payToEnable.showQRCode = false
|
||||||
|
this.showPayToEnableDialog = true
|
||||||
|
},
|
||||||
|
updatePayToInstallData: function (extension) {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/v1/extension/${extension.id}/sell`,
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
{
|
||||||
|
required: extension.payToEnable.required,
|
||||||
|
amount: extension.payToEnable.amount,
|
||||||
|
wallet: extension.payToEnable.wallet
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Payment info updated!'
|
||||||
|
})
|
||||||
|
this.showManageExtensionDialog = false
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
extension.inProgress = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
showUninstall: function () {
|
||||||
|
this.showManageExtensionDialog = false
|
||||||
|
this.showUninstallDialog = true
|
||||||
|
this.uninstallAndDropDb = false
|
||||||
|
},
|
||||||
|
|
||||||
|
showDropDb: function () {
|
||||||
|
this.showDropDbDialog = true
|
||||||
|
},
|
||||||
|
|
||||||
|
showManageExtension: async function (extension) {
|
||||||
|
this.selectedExtension = extension
|
||||||
|
this.selectedRelease = null
|
||||||
|
this.selectedExtensionRepos = null
|
||||||
|
this.manageExtensionTab = 'releases'
|
||||||
|
this.showManageExtensionDialog = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/extension/${extension.id}/releases`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
|
||||||
|
this.selectedExtensionRepos = data.reduce((repos, release) => {
|
||||||
|
repos[release.source_repo] = repos[release.source_repo] || {
|
||||||
|
releases: [],
|
||||||
|
isInstalled: false,
|
||||||
|
repo: release.repo
|
||||||
|
}
|
||||||
|
release.inProgress = false
|
||||||
|
release.error = null
|
||||||
|
release.loaded = false
|
||||||
|
release.isInstalled = this.isInstalledVersion(
|
||||||
|
this.selectedExtension,
|
||||||
|
release
|
||||||
|
)
|
||||||
|
if (release.isInstalled) {
|
||||||
|
repos[release.source_repo].isInstalled = true
|
||||||
|
}
|
||||||
|
if (release.pay_link) {
|
||||||
|
release.requiresPayment = true
|
||||||
|
release.paidAmount = release.cost_sats
|
||||||
|
release.payment_hash = this.getPaylinkHash(release.pay_link)
|
||||||
|
}
|
||||||
|
|
||||||
|
repos[release.source_repo].releases.push(release)
|
||||||
|
return repos
|
||||||
|
}, {})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
extension.inProgress = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showExtensionDetails: async function (extId, detailsLink) {
|
||||||
|
if (!detailsLink) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.selectedExtensionDetails = null
|
||||||
|
this.showExtensionDetailsDialog = true
|
||||||
|
this.slide = 0
|
||||||
|
this.fullscreen = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`,
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
|
||||||
|
this.selectedExtensionDetails = data
|
||||||
|
this.selectedExtensionDetails.description_md =
|
||||||
|
LNbits.utils.convertMarkdown(data.description_md)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async payAndInstall(release) {
|
||||||
|
try {
|
||||||
|
this.selectedExtension.inProgress = true
|
||||||
|
this.showManageExtensionDialog = false
|
||||||
|
const paymentInfo = await this.requestPaymentForInstall(
|
||||||
|
this.selectedExtension.id,
|
||||||
|
release
|
||||||
|
)
|
||||||
|
this.rememberPaylinkHash(release.pay_link, paymentInfo.payment_hash)
|
||||||
|
const wallet = this.g.user.wallets.find(w => w.id === release.wallet)
|
||||||
|
const {data} = await LNbits.api.payInvoice(
|
||||||
|
wallet,
|
||||||
|
paymentInfo.payment_request
|
||||||
|
)
|
||||||
|
|
||||||
|
release.payment_hash = data.payment_hash
|
||||||
|
|
||||||
|
await this.installExtension(release)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err)
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
} finally {
|
||||||
|
this.selectedExtension.inProgress = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async payAndEnable(extension) {
|
||||||
|
try {
|
||||||
|
const paymentInfo = await this.requestPaymentForEnable(
|
||||||
|
extension.id,
|
||||||
|
extension.payToEnable.paidAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
const wallet = this.g.user.wallets.find(
|
||||||
|
w => w.id === extension.payToEnable.paymentWallet
|
||||||
|
)
|
||||||
|
const {data} = await LNbits.api.payInvoice(
|
||||||
|
wallet,
|
||||||
|
paymentInfo.payment_request
|
||||||
|
)
|
||||||
|
this.enableExtension(extension)
|
||||||
|
this.showPayToEnableDialog = false
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err)
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async showInstallQRCode(release) {
|
||||||
|
this.selectedRelease = release
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.requestPaymentForInstall(
|
||||||
|
this.selectedExtension.id,
|
||||||
|
release
|
||||||
|
)
|
||||||
|
|
||||||
|
this.selectedRelease.paymentRequest = data.payment_request
|
||||||
|
this.selectedRelease.payment_hash = data.payment_hash
|
||||||
|
this.selectedRelease = _.clone(this.selectedRelease)
|
||||||
|
this.rememberPaylinkHash(
|
||||||
|
this.selectedRelease.pay_link,
|
||||||
|
this.selectedRelease.payment_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
this.subscribeToPaylinkWs(
|
||||||
|
this.selectedRelease.pay_link,
|
||||||
|
data.payment_hash
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err)
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async showEnableQRCode(extension) {
|
||||||
|
try {
|
||||||
|
extension.payToEnable.showQRCode = true
|
||||||
|
this.selectedExtension = _.clone(extension)
|
||||||
|
|
||||||
|
const data = await this.requestPaymentForEnable(
|
||||||
|
extension.id,
|
||||||
|
extension.payToEnable.paidAmount
|
||||||
|
)
|
||||||
|
extension.payToEnable.paymentRequest = data.payment_request
|
||||||
|
this.selectedExtension = _.clone(extension)
|
||||||
|
|
||||||
|
const url = new URL(window.location)
|
||||||
|
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
|
url.pathname = `/api/v1/ws/${data.payment_hash}`
|
||||||
|
const ws = new WebSocket(url)
|
||||||
|
ws.addEventListener('message', async ({data}) => {
|
||||||
|
const payment = JSON.parse(data)
|
||||||
|
if (payment.pending === false) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Invoice Paid!'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.enableExtension(extension)
|
||||||
|
ws.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err)
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async requestPaymentForInstall(extId, release) {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/v1/extension/${extId}/invoice/install`,
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
{
|
||||||
|
ext_id: extId,
|
||||||
|
archive: release.archive,
|
||||||
|
source_repo: release.source_repo,
|
||||||
|
cost_sats: release.paidAmount,
|
||||||
|
version: release.version
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
async requestPaymentForEnable(extId, amount) {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/v1/extension/${extId}/invoice/enable`,
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
{
|
||||||
|
amount
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
clearHangingInvoice(release) {
|
||||||
|
this.forgetPaylinkHash(release.pay_link)
|
||||||
|
release.payment_hash = null
|
||||||
|
},
|
||||||
|
|
||||||
|
rememberPaylinkHash(pay_link, payment_hash) {
|
||||||
|
this.$q.localStorage.set(
|
||||||
|
`lnbits.extensions.paylink.${pay_link}`,
|
||||||
|
payment_hash
|
||||||
|
)
|
||||||
|
},
|
||||||
|
getPaylinkHash(pay_link) {
|
||||||
|
return this.$q.localStorage.getItem(
|
||||||
|
`lnbits.extensions.paylink.${pay_link}`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
forgetPaylinkHash(pay_link) {
|
||||||
|
this.$q.localStorage.remove(`lnbits.extensions.paylink.${pay_link}`)
|
||||||
|
},
|
||||||
|
subscribeToPaylinkWs(pay_link, payment_hash) {
|
||||||
|
const url = new URL(`${pay_link}/${payment_hash}`)
|
||||||
|
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
|
this.paylinkWebsocket = new WebSocket(url)
|
||||||
|
this.paylinkWebsocket.addEventListener('message', async ({data}) => {
|
||||||
|
const resp = JSON.parse(data)
|
||||||
|
if (resp.paid) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Invoice Paid!'
|
||||||
|
})
|
||||||
|
this.installExtension(this.selectedRelease)
|
||||||
|
} else {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Invoice tracking lost!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
unsubscribeFromPaylinkWs() {
|
||||||
|
try {
|
||||||
|
this.paylinkWebsocket && this.paylinkWebsocket.close()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hasNewVersion: function (extension) {
|
||||||
|
if (extension.installedRelease && extension.latestRelease) {
|
||||||
|
return (
|
||||||
|
extension.installedRelease.version !==
|
||||||
|
extension.latestRelease.version
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isInstalledVersion: function (extension, release) {
|
||||||
|
if (extension.installedRelease) {
|
||||||
|
return (
|
||||||
|
extension.installedRelease.source_repo === release.source_repo &&
|
||||||
|
extension.installedRelease.version === release.version
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getReleaseIcon: function (release) {
|
||||||
|
if (!release.is_version_compatible) return 'block'
|
||||||
|
if (release.isInstalled) return 'download_done'
|
||||||
|
|
||||||
|
return 'download'
|
||||||
|
},
|
||||||
|
getReleaseIconColor: function (release) {
|
||||||
|
if (!release.is_version_compatible) return 'text-red'
|
||||||
|
if (release.isInstalled) return 'text-green'
|
||||||
|
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
getGitHubReleaseDetails: async function (release) {
|
||||||
|
if (!release.is_github_release || release.loaded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const [org, repo] = release.source_repo.split('/')
|
||||||
|
release.inProgress = true
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/extension/release/${org}/${repo}/${release.version}`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
release.loaded = true
|
||||||
|
release.is_version_compatible = data.is_version_compatible
|
||||||
|
release.min_lnbits_version = data.min_lnbits_version
|
||||||
|
release.warning = data.warning
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
release.error = error
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
} finally {
|
||||||
|
release.inProgress = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectAllUpdatableExtensionss: async function () {
|
||||||
|
this.updatableExtensions.forEach(e => (e.selectedForUpdate = true))
|
||||||
|
},
|
||||||
|
updateSelectedExtensions: async function () {
|
||||||
|
let count = 0
|
||||||
|
for (const ext of this.updatableExtensions) {
|
||||||
|
try {
|
||||||
|
if (!ext.selectedForUpdate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ext.inProgress = true
|
||||||
|
await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
`/api/v1/extension`,
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
{
|
||||||
|
ext_id: ext.id,
|
||||||
|
archive: ext.latestRelease.archive,
|
||||||
|
source_repo: ext.latestRelease.source_repo,
|
||||||
|
payment_hash: ext.latestRelease.payment_hash,
|
||||||
|
version: ext.latestRelease.version
|
||||||
|
}
|
||||||
|
)
|
||||||
|
count++
|
||||||
|
ext.isAvailable = true
|
||||||
|
ext.isInstalled = true
|
||||||
|
ext.isUpgraded = true
|
||||||
|
ext.inProgress = false
|
||||||
|
ext.installedRelease = ext.latestRelease
|
||||||
|
this.toggleExtension(ext)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err)
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Failed to update ${ext.code}!`
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
ext.inProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: `${count} extensions updated!`
|
||||||
|
})
|
||||||
|
this.showUpdateAllDialog = false
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created: function () {
|
||||||
|
this.extensions = JSON.parse('{{extensions | tojson | safe}}').map(e => ({
|
||||||
|
...e,
|
||||||
|
inProgress: false,
|
||||||
|
selectedForUpdate: false
|
||||||
|
}))
|
||||||
|
this.filteredExtensions = this.extensions.concat([])
|
||||||
|
for (let i = 0; i < this.filteredExtensions.length; i++) {
|
||||||
|
if (this.filteredExtensions[i].isInstalled != false) {
|
||||||
|
this.handleTabChanged('installed')
|
||||||
|
this.tab = 'installed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (window.user) {
|
||||||
|
this.user = LNbits.map.user(window.user)
|
||||||
|
}
|
||||||
|
this.updatableExtensions = this.extensions.filter(ext =>
|
||||||
|
this.hasNewVersion(ext)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
mixins: [windowMixin]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,150 +1,139 @@
|
|||||||
{% extends "public.html" %} {% block page_container %}
|
{% extends "public.html" %} {% block page %}
|
||||||
<q-page-container>
|
<div class="row q-col-gutter-md justify-center main">
|
||||||
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
|
<div class="col-10 col-md-8 col-lg-6 q-gutter-y-md">
|
||||||
{% block page %}
|
<q-card>
|
||||||
<div class="row q-col-gutter-md justify-center main">
|
<q-card-section class="grid">
|
||||||
<div class="col-10 col-md-8 col-lg-6 q-gutter-y-md">
|
<div>
|
||||||
<q-card>
|
<h6 class="q-my-none text-center">
|
||||||
<q-card-section class="grid">
|
<strong>Welcome to LNbits</strong>
|
||||||
<div>
|
<p>Set up the Superuser account below.</p>
|
||||||
<h6 class="q-my-none text-center">
|
</h6>
|
||||||
<strong v-text="$t('welcome_lnbits')"></strong>
|
<br />
|
||||||
<p><span v-text="$t('setup_su_account')"></span></p>
|
<q-form class="q-gutter-md">
|
||||||
</h6>
|
<q-input
|
||||||
<br />
|
filled
|
||||||
<q-form class="q-gutter-md">
|
v-model="loginData.username"
|
||||||
<q-input
|
:label="$t('username')"
|
||||||
filled
|
></q-input>
|
||||||
v-model="loginData.username"
|
<q-input
|
||||||
:label="$t('username')"
|
filled
|
||||||
></q-input>
|
v-model.trim="loginData.password"
|
||||||
<q-input
|
:type="loginData.isPwd ? 'password' : 'text'"
|
||||||
filled
|
autocomplete="off"
|
||||||
v-model.trim="loginData.password"
|
:label="$t('password')"
|
||||||
:type="loginData.isPwd ? 'password' : 'text'"
|
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||||
autocomplete="off"
|
><template v-slot:append>
|
||||||
:label="$t('password')"
|
<q-icon
|
||||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
|
||||||
><template v-slot:append>
|
class="cursor-pointer"
|
||||||
<q-icon
|
@click="loginData.isPwd = !loginData.isPwd"
|
||||||
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
|
/> </template
|
||||||
class="cursor-pointer"
|
></q-input>
|
||||||
@click="loginData.isPwd = !loginData.isPwd"
|
<q-input
|
||||||
/> </template
|
filled
|
||||||
></q-input>
|
v-model.trim="loginData.passwordRepeat"
|
||||||
<q-input
|
:type="loginData.isPwdRepeat ? 'password' : 'text'"
|
||||||
filled
|
autocomplete="off"
|
||||||
v-model.trim="loginData.passwordRepeat"
|
:label="$t('password_repeat')"
|
||||||
:type="loginData.isPwdRepeat ? 'password' : 'text'"
|
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || 'Passwords_dont_match']"
|
||||||
autocomplete="off"
|
><template v-slot:append>
|
||||||
:label="$t('password_repeat')"
|
<q-icon
|
||||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || $t('invalid_password_repeat')]"
|
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
|
||||||
><template v-slot:append>
|
class="cursor-pointer"
|
||||||
<q-icon
|
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
|
||||||
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
|
/> </template
|
||||||
class="cursor-pointer"
|
></q-input>
|
||||||
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
|
<q-btn
|
||||||
/> </template
|
@click="setPassword()"
|
||||||
></q-input>
|
unelevated
|
||||||
<q-btn
|
color="primary"
|
||||||
@click="setPassword()"
|
:label="$t('login')"
|
||||||
unelevated
|
:disable="checkPasswordsMatch || !loginData.username || !loginData.password || !loginData.passwordRepeat"
|
||||||
color="primary"
|
></q-btn>
|
||||||
:label="$t('login')"
|
</q-form>
|
||||||
:disable="checkPasswordsMatch || !loginData.username || !loginData.password || !loginData.passwordRepeat"
|
</div>
|
||||||
></q-btn>
|
<div class="hero-wrapper">
|
||||||
</q-form>
|
<div class="hero q-mx-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-wrapper">
|
</q-card-section>
|
||||||
<div class="hero q-mx-auto"></div>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</div>
|
||||||
</q-card>
|
{% endblock %} {% block scripts %}
|
||||||
</div>
|
<style>
|
||||||
</div>
|
main {
|
||||||
{% endblock %} {% block scripts %}
|
display: flex;
|
||||||
<style>
|
flex-direction: column;
|
||||||
main {
|
justify-content: center;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
.grid {
|
||||||
justify-content: center;
|
display: block;
|
||||||
}
|
}
|
||||||
.grid {
|
.hero-wrapper {
|
||||||
display: block;
|
display: none;
|
||||||
}
|
}
|
||||||
.hero-wrapper {
|
.hero {
|
||||||
display: none;
|
display: block;
|
||||||
}
|
height: 100%;
|
||||||
.hero {
|
max-width: 250px;
|
||||||
display: block;
|
background-image: url("{{ static_url_for('static', 'images/logos/lnbits.svg') }}");
|
||||||
height: 100%;
|
background-position: center;
|
||||||
max-width: 250px;
|
background-size: contain;
|
||||||
background-image: url("{{ static_url_for('static', 'images/logos/lnbits.svg') }}");
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
}
|
||||||
background-size: contain;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-gap: 1rem;
|
grid-gap: 1rem;
|
||||||
}
|
}
|
||||||
.hero-wrapper {
|
.hero-wrapper {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
window.app = Vue.createApp({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
loginData: {
|
||||||
|
isPwd: true,
|
||||||
|
isPwdRepeat: true,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
passwordRepeat: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
},
|
||||||
<script>
|
created() {
|
||||||
window.app = Vue.createApp({
|
this.hasAdminUI = '{{ LNBITS_ADMIN_UI | tojson}}'
|
||||||
el: '#vue',
|
},
|
||||||
mixins: [window.windowMixin],
|
computed: {
|
||||||
data: function () {
|
checkPasswordsMatch() {
|
||||||
return {
|
return this.loginData.password !== this.loginData.passwordRepeat
|
||||||
loginData: {
|
}
|
||||||
isPwd: true,
|
},
|
||||||
isPwdRepeat: true,
|
methods: {
|
||||||
username: '',
|
async setPassword() {
|
||||||
password: '',
|
try {
|
||||||
passwordRepeat: ''
|
await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
|
||||||
}
|
username: this.loginData.username,
|
||||||
}
|
password: this.loginData.password,
|
||||||
},
|
password_repeat: this.loginData.passwordRepeat
|
||||||
created() {
|
})
|
||||||
this.hasAdminUI = '{{ LNBITS_ADMIN_UI | tojson}}'
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
checkPasswordsMatch() {
|
|
||||||
return this.loginData.password !== this.loginData.passwordRepeat
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async setPassword() {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
'/api/v1/auth/first_install',
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
username: this.loginData.username,
|
|
||||||
password: this.loginData.password,
|
|
||||||
password_repeat: this.loginData.passwordRepeat
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
window.location.href = '/admin'
|
window.location.href = '/admin'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
</script>
|
}
|
||||||
{% endblock %}
|
})
|
||||||
</q-page>
|
</script>
|
||||||
</q-page-container>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,16 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
{% extends "base.html" %}
|
||||||
<!---->
|
<!---->
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
{% from "macros.jinja" import window_vars with context %}
|
||||||
<!---->
|
<!---->
|
||||||
{% block scripts %} {{ window_vars(user, wallet) }}{% endblock %} {% block page
|
{% block scripts %} {{ window_vars(user, wallet) }}
|
||||||
%}
|
<script src="{{ static_url_for('static', 'js/wallet.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
<!---->
|
||||||
|
{% block title %}{{ wallet_name }} - {{ SITE_TITLE }} {% endblock %}
|
||||||
|
<!---->
|
||||||
|
{% block page %}
|
||||||
<div class="row q-col-gutter-md">
|
<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">
|
<div class="col-12 col-md-8 q-gutter-y-md">
|
||||||
{% elif HIDE_API %}
|
{% elif HIDE_API %}
|
||||||
<div class="col-12 q-gutter-y-md">
|
<div class="col-12 q-gutter-y-md">
|
||||||
@@ -20,51 +25,44 @@
|
|||||||
} : ''"
|
} : ''"
|
||||||
>
|
>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-9 col-12">
|
||||||
<q-card
|
<q-card
|
||||||
:style="$q.screen.lt.md ? {
|
:style="$q.screen.lt.md ? {
|
||||||
background: $q.screen.lt.md ? 'none !important': ''
|
background: $q.screen.lt.md ? 'none !important': ''
|
||||||
, boxShadow: $q.screen.lt.md ? 'none !important': ''
|
, boxShadow: $q.screen.lt.md ? 'none !important': ''
|
||||||
, border: $q.screen.lt.md ? 'none !important': ''
|
|
||||||
, width: $q.screen.lt.md && mobileSimple ? '90% !important': ''
|
, width: $q.screen.lt.md && mobileSimple ? '90% !important': ''
|
||||||
} : ''"
|
} : ''"
|
||||||
>
|
>
|
||||||
<q-card-section style="height: 130px">
|
<q-card-section>
|
||||||
<div class="row q-gutter-md">
|
<div class="row">
|
||||||
<div
|
<div class="col-1" style="max-width: 30px">
|
||||||
v-if="isSatsDenomination"
|
|
||||||
class="col-1"
|
|
||||||
style="max-width: 30px"
|
|
||||||
>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.fiatTracking"
|
v-if="update.currency"
|
||||||
@click="swapBalancePriority"
|
@click="swapBalancePriority"
|
||||||
style="height: 50px"
|
style="height: 50px"
|
||||||
class="q-mt-lg"
|
class="q-mt-lg"
|
||||||
color="primary"
|
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
icon="swap_vert"
|
icon="swap_vert"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-11">
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
:class="{
|
:class="{
|
||||||
'q-pt-sm': g.fiatTracking,
|
'q-pt-sm': update.currency,
|
||||||
'q-pt-lg': !g.fiatTracking
|
'q-pt-lg': !update.currency
|
||||||
}"
|
}"
|
||||||
v-if="!isFiatPriority || !g.fiatTracking"
|
v-if="!isPrioritySwapped"
|
||||||
style="height: 100px"
|
style="height: 90px"
|
||||||
>
|
>
|
||||||
<div class="col-7">
|
<div class="col-7">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div class="text-h3 q-my-none full-width">
|
<div class="text-h3 q-my-none text-no-wrap">
|
||||||
<strong
|
<strong v-text="formattedBalance"></strong>
|
||||||
v-text="walletFormatBalance(this.g.wallet.sat)"
|
<small> {{LNBITS_DENOMINATION}}</small>
|
||||||
class="text-no-wrap"
|
|
||||||
:style="{fontSize: 'clamp(0.75rem, 10vw, 3rem)', display: 'inline-block', maxWidth: '100%'}"
|
|
||||||
></strong>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@@ -78,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<div v-if="g.fiatTracking">
|
<div v-if="update.currency">
|
||||||
<span
|
<span
|
||||||
class="text-h5 text-italic"
|
class="text-h5 text-italic"
|
||||||
v-text="formattedFiatAmount"
|
v-text="formattedFiatAmount"
|
||||||
@@ -90,18 +88,18 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
v-if="isFiatPriority && g.fiatTracking"
|
v-if="isPrioritySwapped"
|
||||||
:class="{
|
:class="{
|
||||||
'q-pt-sm': g.fiatTracking,
|
'q-pt-sm': update.currency,
|
||||||
'q-pt-lg': !g.fiatTracking
|
'q-pt-lg': !update.currency
|
||||||
}"
|
}"
|
||||||
style="height: 100px"
|
style="height: 100px"
|
||||||
>
|
>
|
||||||
<div class="col-7">
|
<div class="col-7">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div
|
<div
|
||||||
v-if="g.fiatTracking"
|
v-if="update.currency"
|
||||||
class="text-h3 q-my-none text-no-wrap"
|
class="text-h3 q-my-none text-no-wrap"
|
||||||
>
|
>
|
||||||
<strong v-text="formattedFiatAmount"></strong>
|
<strong v-text="formattedFiatAmount"></strong>
|
||||||
@@ -129,7 +127,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute-right q-pa-md"
|
class="absolute-right q-pa-md"
|
||||||
v-if="$q.screen.gt.md && g.fiatTracking && '{{ LNBITS_DENOMINATION }}' == 'sats'"
|
v-if="$q.screen.gt.md && update.currency"
|
||||||
>
|
>
|
||||||
<div class="text-bold text-italic">BTC Price</div>
|
<div class="text-bold text-italic">BTC Price</div>
|
||||||
<span
|
<span
|
||||||
@@ -139,7 +137,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="$q.screen.lt.md"
|
v-if="$q.screen.lt.md"
|
||||||
@click="simpleMobile()"
|
@click="mobileSimple = !mobileSimple"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="q-ml-xl absolute-right"
|
class="q-ml-xl absolute-right"
|
||||||
dense
|
dense
|
||||||
@@ -168,12 +166,7 @@
|
|||||||
@click="showReceiveDialog"
|
@click="showReceiveDialog"
|
||||||
:label="$t('create_invoice')"
|
:label="$t('create_invoice')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn unelevated color="secondary" icon="qr_code_scanner">
|
||||||
unelevated
|
|
||||||
color="secondary"
|
|
||||||
icon="qr_code_scanner"
|
|
||||||
@click="showCamera"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('camera_tooltip')"></span
|
><span v-text="$t('camera_tooltip')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
@@ -187,14 +180,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-3" v-show="$q.screen.gt.md">
|
||||||
|
<q-card class="q-ml-md q-pa-none" style="height: 172px">
|
||||||
|
<q-card-section style="padding: 0">
|
||||||
|
<canvas
|
||||||
|
ref="transactionChart"
|
||||||
|
style="
|
||||||
|
height: 172px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
transform: translate(-2%, 4%);
|
||||||
|
"
|
||||||
|
></canvas>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<q-card
|
<q-card
|
||||||
:style="
|
:style="
|
||||||
$q.screen.lt.md
|
$q.screen.lt.md
|
||||||
? {
|
? {
|
||||||
background: $q.screen.lt.md ? 'none !important' : '',
|
background: $q.screen.lt.md ? 'none !important' : '',
|
||||||
boxShadow: $q.screen.lt.md ? 'none !important' : '',
|
boxShadow: $q.screen.lt.md ? 'none !important' : '',
|
||||||
border: $q.screen.lt.md ? 'none !important': '',
|
|
||||||
marginTop: $q.screen.lt.md ? '0px !important' : ''
|
marginTop: $q.screen.lt.md ? '0px !important' : ''
|
||||||
}
|
}
|
||||||
: ''
|
: ''
|
||||||
@@ -202,69 +211,23 @@
|
|||||||
>
|
>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<payment-list
|
<payment-list
|
||||||
@filter-changed="handleFilterChange"
|
|
||||||
:update="updatePayments"
|
:update="updatePayments"
|
||||||
|
:wallet="this.g.wallet"
|
||||||
:mobile-simple="mobileSimple"
|
:mobile-simple="mobileSimple"
|
||||||
:expand-details="expandDetails"
|
|
||||||
></payment-list>
|
></payment-list>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</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()"
|
|
||||||
></lnbits-qrcode>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% if HIDE_API %}
|
{% if HIDE_API %}
|
||||||
<div class="col-12 col-md-4 q-gutter-y-md">
|
<div class="col-12 col-md-4 q-gutter-y-md">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div v-if="!mobileSimple" class="col-12 col-md-5 q-gutter-y-md">
|
<div v-if="!mobileSimple || $q.screen.gt.sm" class="col-12 col-md-5 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section class="q-pb-xs">
|
<q-card-section>
|
||||||
<div class="row items-center">
|
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
||||||
<q-avatar
|
{{ SITE_TITLE }} Wallet:
|
||||||
size="lg"
|
<strong><em>{{wallet_name}}</em></strong>
|
||||||
:icon="g.wallet.extra.icon"
|
</h6>
|
||||||
:text-color="$q.dark.isActive ? 'black' : 'grey-3'"
|
|
||||||
:color="g.wallet.extra.color"
|
|
||||||
>
|
|
||||||
</q-avatar>
|
|
||||||
<q-btn
|
|
||||||
@click="icon.show = true"
|
|
||||||
round
|
|
||||||
color="grey-5"
|
|
||||||
text-color="black"
|
|
||||||
size="xs"
|
|
||||||
icon="edit"
|
|
||||||
style="position: relative; left: -15px; bottom: -10px"
|
|
||||||
></q-btn>
|
|
||||||
<div class="text-subtitle1 q-mt-none q-mb-none">
|
|
||||||
{{ 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>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
@@ -291,196 +254,102 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
|
|
||||||
group="extras"
|
group="extras"
|
||||||
icon="phone_android"
|
icon="settings_cell"
|
||||||
:label="$t('access_wallet_on_mobile')"
|
:label="$t('export_to_phone')"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="text-center">
|
||||||
|
<p v-text="$t('export_to_phone_desc')"></p>
|
||||||
|
<lnbits-qrcode :value="exportUrl"></lnbits-qrcode>
|
||||||
|
</q-card-section>
|
||||||
|
<span v-text="exportWalletQR"></span>
|
||||||
|
<q-card-actions class="flex-center q-pb-md">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
:label="$t('copy_wallet_url')"
|
||||||
|
@click="copyText(exportUrl)"
|
||||||
|
></q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-expansion-item
|
||||||
|
group="extras"
|
||||||
|
icon="edit"
|
||||||
|
:label="$t('rename_wallet')"
|
||||||
>
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
You can connect to this wallet from a mobile app:
|
<div class="" style="max-width: 320px">
|
||||||
<ul>
|
<q-input
|
||||||
<li>
|
filled
|
||||||
Download
|
v-model.trim="update.name"
|
||||||
<a class="text-secondary" href="https://zeusln.app"
|
label="Name"
|
||||||
>Zeus</a
|
dense
|
||||||
>
|
/>
|
||||||
or
|
</div>
|
||||||
<a
|
<q-btn
|
||||||
class="text-secondary"
|
:disable="!update.name.length"
|
||||||
href="https://bluewallet.io/"
|
unelevated
|
||||||
>BlueWallet</a
|
class="q-mt-sm"
|
||||||
>
|
color="primary"
|
||||||
from App Store or Google Play
|
:label="$t('update_name')"
|
||||||
</li>
|
@click="updateWallet({ name: update.name })"
|
||||||
<li>
|
></q-btn>
|
||||||
Enable the
|
|
||||||
<a class="text-secondary" href="/lndhub">LndHub </a>
|
|
||||||
extension for this account
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Scan the QR code in the
|
|
||||||
<a class="text-secondary" href="/lndhub">LndHub </a>
|
|
||||||
extensions with your mobile app
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
Or you can access the wallet directly from your mobile
|
|
||||||
browser using:
|
|
||||||
<q-expansion-item
|
|
||||||
icon="mobile_friendly"
|
|
||||||
:label="$t('export_to_phone')"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section class="text-center">
|
|
||||||
<p v-text="$t('export_to_phone_desc')"></p>
|
|
||||||
<lnbits-qrcode
|
|
||||||
:value="`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`"
|
|
||||||
></lnbits-qrcode>
|
|
||||||
</q-card-section>
|
|
||||||
<span v-text="exportWalletQR"></span>
|
|
||||||
<q-card-actions class="flex-center q-pb-md">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
:label="$t('copy_wallet_url')"
|
|
||||||
@click="copyText(`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`)"
|
|
||||||
></q-btn>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
group="extras"
|
group="extras"
|
||||||
icon="settings"
|
icon="attach_money"
|
||||||
:label="$t('wallet_config')"
|
:label="$t('fiat_tracking')"
|
||||||
>
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row">
|
<div style="max-width: 360px">
|
||||||
<div class="col-6">
|
<div class="row">
|
||||||
<q-input
|
<div class="col-auto">
|
||||||
filled
|
<q-toggle
|
||||||
v-model.trim="update.name"
|
v-model="fiatTracking"
|
||||||
label="Name"
|
color="primary"
|
||||||
dense
|
@update:model-value="handleFiatTracking"
|
||||||
/>
|
></q-toggle>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="update.currency"
|
||||||
|
type="text"
|
||||||
|
:label="!fiatTracking ? 'Disabled' : $t('currency')"
|
||||||
|
:options="receive.units.filter((u) => u !== 'sat')"
|
||||||
|
:disable="!fiatTracking"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 q-pl-sm">
|
|
||||||
<q-btn
|
|
||||||
:disable="!update.name.length"
|
|
||||||
unelevated
|
|
||||||
class="q-mt-xs full-width"
|
|
||||||
color="primary"
|
|
||||||
:label="$t('update_name')"
|
|
||||||
dense
|
|
||||||
@click="updateWallet({ name: update.name })"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="col-2"></div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section
|
|
||||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
|
|
||||||
>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-6">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="update.currency"
|
|
||||||
type="text"
|
|
||||||
:disable="g.fiatTracking"
|
|
||||||
:options="receive.units.filter((u) => u !== 'sat')"
|
|
||||||
:label="$t('currency_settings')"
|
|
||||||
></q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-4 q-pl-sm">
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
color="primary"
|
|
||||||
class="q-mt-xs full-width"
|
|
||||||
@click="handleFiatTracking()"
|
|
||||||
:disable="update.currency == ''"
|
|
||||||
:label="g.fiatTracking ? 'Remove' : 'Add'"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="col-2">
|
|
||||||
<q-btn
|
|
||||||
v-if="g.user.admin"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
icon="settings"
|
|
||||||
class="float-right q-mb-lg"
|
|
||||||
to="/admin#exchange_providers"
|
|
||||||
><q-tooltip
|
|
||||||
v-text="$t('exchange_providers')"
|
|
||||||
></q-tooltip
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-6">
|
|
||||||
<p v-text="$t('delete_wallet_desc')"></p>
|
|
||||||
</div>
|
|
||||||
<div class="col-4 q-pl-sm">
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="red-10"
|
|
||||||
class="full-width"
|
|
||||||
@click="deleteWallet()"
|
|
||||||
:label="$t('delete_wallet')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="col-2"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-expansion-item
|
|
||||||
group="charts"
|
|
||||||
icon="insights"
|
|
||||||
:label="$t('wallet_charts')"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartConfig.showBalance"
|
|
||||||
:label="$t('payments_balance_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-12">
|
<q-separator></q-separator>
|
||||||
<q-checkbox
|
<q-expansion-item
|
||||||
dense
|
group="extras"
|
||||||
@click="saveChartsPreferences"
|
icon="remove_circle"
|
||||||
v-model="chartConfig.showBalanceInOut"
|
:label="$t('delete_wallet')"
|
||||||
:label="$t('payments_balance_in_out_chart')"
|
>
|
||||||
>
|
<q-card>
|
||||||
</q-checkbox>
|
<q-card-section>
|
||||||
</div>
|
<p v-text="$t('delete_wallet_desc')"></p>
|
||||||
<div class="col-md-4 col-sm-12">
|
<q-btn
|
||||||
<q-checkbox
|
unelevated
|
||||||
dense
|
color="red-10"
|
||||||
@click="saveChartsPreferences"
|
@click="deleteWallet()"
|
||||||
v-model="chartConfig.showPaymentCountInOut"
|
:label="$t('delete_wallet')"
|
||||||
:label="$t('payments_count_in_out_chart')"
|
></q-btn>
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
@@ -490,7 +359,7 @@
|
|||||||
</q-list>
|
</q-list>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
{% endif %} {% if AD_SPACE_ENABLED and AD_SPACE %}
|
{% endif %} {% if AD_SPACE %}
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
||||||
@@ -504,6 +373,7 @@
|
|||||||
style="display: inline-block"
|
style="display: inline-block"
|
||||||
href="{{ AD[0] }}"
|
href="{{ AD[0] }}"
|
||||||
class="q-ml-md q-mb-xs q-mr-md"
|
class="q-ml-md q-mb-xs q-mr-md"
|
||||||
|
style="max-width: 80%"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
style="max-width: 100%; height: auto"
|
style="max-width: 100%; height: auto"
|
||||||
@@ -519,84 +389,9 @@
|
|||||||
>{% endfor %}
|
>{% endfor %}
|
||||||
</q-card>
|
</q-card>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div
|
|
||||||
v-show="chartDataPointCount"
|
|
||||||
class="col-12 col-md-5 q-gutter-y-md"
|
|
||||||
>
|
|
||||||
<q-card v-if="chartConfig.showBalance">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<div style="height: 200px" class="q-pa-sm">
|
|
||||||
<canvas ref="walletBalanceChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card v-if="chartConfig.showBalanceInOut">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<div style="height: 200px" class="q-pa-sm">
|
|
||||||
<canvas ref="walletBalanceInOut"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card v-if="chartConfig.showPaymentCountInOut">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<div style="height: 200px" class="q-pa-sm">
|
|
||||||
<canvas ref="walletPaymentsInOut"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div v-if="hasChartActive && !chartDataPointCount">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section> No chart data available</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog v-model="icon.show" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<q-form @submit="setIcon" class="q-gutter-md">
|
|
||||||
<div class="q-gutter-sm q-pa-sm flex flex-wrap justify-center">
|
|
||||||
<!-- Loop through all icons -->
|
|
||||||
<q-btn
|
|
||||||
v-for="(thisIcon, index) in icon.options"
|
|
||||||
:key="index"
|
|
||||||
@click="setSelectedIcon(thisIcon)"
|
|
||||||
round
|
|
||||||
text-color="black"
|
|
||||||
:color="icon.data.icon === thisIcon ? icon.data.color || 'primary' : 'grey-5'"
|
|
||||||
size="md"
|
|
||||||
:icon="thisIcon"
|
|
||||||
class="q-mb-sm"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="q-pa-sm flex justify-between items-center">
|
|
||||||
<div class="flex q-pl-lg">
|
|
||||||
<!-- Color options -->
|
|
||||||
<q-btn
|
|
||||||
v-for="(color, index) in icon.colorOptions"
|
|
||||||
:key="'color-' + index"
|
|
||||||
@click="setSelectedColor(color)"
|
|
||||||
round
|
|
||||||
:color="color"
|
|
||||||
size="xs"
|
|
||||||
style="width: 24px; height: 24px; min-width: 24px; padding: 0"
|
|
||||||
class="q-mr-xs"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="!icon.data.icon"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Save Icon
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="receive.show"
|
v-model="receive.show"
|
||||||
position="top"
|
position="top"
|
||||||
@@ -614,7 +409,7 @@
|
|||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model="receive.data.amount"
|
v-model.number="receive.data.amount"
|
||||||
:label="$t('amount')+' ({{LNBITS_DENOMINATION}}) *'"
|
:label="$t('amount')+' ({{LNBITS_DENOMINATION}}) *'"
|
||||||
mask="#.##"
|
mask="#.##"
|
||||||
fill-mask="0"
|
fill-mask="0"
|
||||||
@@ -624,30 +419,14 @@
|
|||||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||||
></q-input>
|
></q-input>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="row">
|
<q-select
|
||||||
<div class="col-10">
|
filled
|
||||||
<q-select
|
dense
|
||||||
filled
|
v-model="receive.unit"
|
||||||
dense
|
type="text"
|
||||||
v-model="receive.unit"
|
:label="$t('unit')"
|
||||||
type="text"
|
:options="receive.units"
|
||||||
:label="$t('unit')"
|
></q-select>
|
||||||
: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-input
|
<q-input
|
||||||
ref="setAmount"
|
ref="setAmount"
|
||||||
filled
|
filled
|
||||||
@@ -665,59 +444,9 @@
|
|||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
type="textarea"
|
|
||||||
rows="2"
|
|
||||||
v-model.trim="receive.data.memo"
|
v-model.trim="receive.data.memo"
|
||||||
:label="$t('memo')"
|
:label="$t('memo')"
|
||||||
></q-input>
|
></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">
|
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
@@ -727,7 +456,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="receive.lnurl"
|
v-if="receive.lnurl"
|
||||||
v-text="`${$t('withdraw_from')} ${receive.lnurl.domain}`"
|
v-text="$t('withdraw_from') + receive.lnurl.domain"
|
||||||
></span>
|
></span>
|
||||||
<span v-else v-text="$t('create_invoice')"></span>
|
<span v-else v-text="$t('create_invoice')"></span>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
@@ -739,11 +468,11 @@
|
|||||||
:label="$t('cancel')"
|
:label="$t('cancel')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
</div>
|
||||||
<q-spinner-bars
|
<q-spinner
|
||||||
v-if="receive.status == 'loading'"
|
v-if="receive.status == 'loading'"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="2.55em"
|
size="2.55em"
|
||||||
></q-spinner-bars>
|
></q-spinner>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
<q-card
|
<q-card
|
||||||
@@ -751,15 +480,10 @@
|
|||||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||||
>
|
>
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<a
|
<a :href="'lightning:' + receive.paymentReq">
|
||||||
v-if="receive.fiatPaymentReq"
|
<lnbits-qrcode
|
||||||
:href="receive.fiatPaymentReq"
|
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||||
target="_blank"
|
></lnbits-qrcode>
|
||||||
>
|
|
||||||
<div v-html="invoiceQrCode"></div>
|
|
||||||
</a>
|
|
||||||
<a v-else :href="'lightning:' + receive.paymentReq">
|
|
||||||
<div v-html="invoiceQrCode"></div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -769,27 +493,21 @@
|
|||||||
<h5 v-if="receive.unit != 'sat'" class="q-mt-none q-mb-sm">
|
<h5 v-if="receive.unit != 'sat'" class="q-mt-none q-mb-sm">
|
||||||
<span v-text="formattedSatAmount"></span>
|
<span v-text="formattedSatAmount"></span>
|
||||||
</h5>
|
</h5>
|
||||||
<div v-if="!receive.fiatPaymentReq">
|
<q-chip v-if="hasNfc" outline square color="positive">
|
||||||
<q-chip v-if="hasNfc" outline square color="positive">
|
<q-avatar
|
||||||
<q-avatar
|
icon="nfc"
|
||||||
icon="nfc"
|
color="positive"
|
||||||
color="positive"
|
text-color="white"
|
||||||
text-color="white"
|
></q-avatar>
|
||||||
></q-avatar>
|
NFC supported
|
||||||
<span v-text="$t('nfc_supported')"></span>
|
</q-chip>
|
||||||
</q-chip>
|
<span v-else class="text-caption text-grey">NFC not supported</span>
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="text-caption text-grey"
|
|
||||||
v-text="$t('nfc_not_supported')"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
color="grey"
|
color="grey"
|
||||||
@click="copyText(receive.fiatPaymentReq || receive.paymentReq)"
|
@click="copyText(receive.paymentReq)"
|
||||||
:label="$t('copy_invoice')"
|
:label="$t('copy_invoice')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -806,131 +524,27 @@
|
|||||||
<q-dialog v-model="parse.show" @hide="closeParseDialog" position="top">
|
<q-dialog v-model="parse.show" @hide="closeParseDialog" position="top">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<div v-if="parse.invoice">
|
<div v-if="parse.invoice">
|
||||||
<div class="column content-center text-center q-mb-md">
|
<h6
|
||||||
<div v-if="!isFiatPriority">
|
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
|
||||||
<h4 class="q-my-none text-bold">
|
class="q-my-none"
|
||||||
<span v-text="walletFormatBalance(parse.invoice.sat)"></span>
|
v-text="parseFloat(String(parse.invoice.fsat).replaceAll(',', '')) / 100 + '{{LNBITS_DENOMINATION}}'"
|
||||||
</h4>
|
></h6>
|
||||||
</div>
|
<h6
|
||||||
<div v-else>
|
v-else
|
||||||
<h4
|
class="q-my-none"
|
||||||
class="q-my-none text-bold"
|
v-text="parse.invoice.fsat + '{{LNBITS_DENOMINATION}}'"
|
||||||
v-text="parse.invoice.fiatAmount"
|
></h6>
|
||||||
></h4>
|
<q-separator class="q-my-sm"></q-separator>
|
||||||
</div>
|
<p class="text-wrap">
|
||||||
<div class="q-my-md absolute">
|
<strong v-text="$t('memo') + ': '"></strong>
|
||||||
<q-btn
|
<span v-text="parse.invoice.description"></span>
|
||||||
v-if="g.fiatTracking"
|
<br />
|
||||||
@click="swapBalancePriority"
|
<strong>Expire date: </strong>
|
||||||
flat
|
<span v-text="parse.invoice.expireDate"></span>
|
||||||
dense
|
<br />
|
||||||
icon="swap_vert"
|
<strong>Hash: </strong>
|
||||||
color="primary"
|
<span v-text="parse.invoice.hash"></span>
|
||||||
></q-btn>
|
</p>
|
||||||
</div>
|
|
||||||
<div v-if="g.fiatTracking">
|
|
||||||
<div v-if="isFiatPriority">
|
|
||||||
<h5 class="q-my-none text-bold">
|
|
||||||
<span
|
|
||||||
v-text="walletFormatBalance(parse.invoice.sat)"
|
|
||||||
></span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div v-else style="opacity: 0.75">
|
|
||||||
<div class="text-h5 text-italic">
|
|
||||||
<span
|
|
||||||
v-text="parse.invoice.fiatAmount"
|
|
||||||
style="opacity: 0.75"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<h6 class="text-center" v-text="parse.invoice.description"></h6>
|
|
||||||
<q-list separator bordered dense class="q-mb-md">
|
|
||||||
<q-expansion-item expand-separator icon="info" label="Details">
|
|
||||||
<q-list separator>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('created')"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="parse.invoice.createdDate"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side top>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="parse.invoice.createdDateFrom"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('expire_date')"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="parse.invoice.expireDate"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section side top>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="parse.invoice.expireDateFrom"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('payment_hash')"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="`${parse.invoice.hash.slice(0, 12)}...${parse.invoice.hash.slice(-12)}`"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section side>
|
|
||||||
<q-item-label>
|
|
||||||
<q-icon
|
|
||||||
name="content_copy"
|
|
||||||
@click="copyText(parse.invoice.hash)"
|
|
||||||
size="1em"
|
|
||||||
color="grey"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</q-item-label>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="parse.invoice.hash"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('Invoice')"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="`${parse.invoice.bolt11.slice(0, 12)}...${parse.invoice.bolt11.slice(-12)}`"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section side>
|
|
||||||
<q-item-label>
|
|
||||||
<q-icon
|
|
||||||
name="content_copy"
|
|
||||||
@click="copyText(parse.invoice.bolt11)"
|
|
||||||
size="1em"
|
|
||||||
color="grey"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</q-item-label>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="parse.invoice.bolt11"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-list>
|
|
||||||
<div v-if="canPay" class="row q-mt-lg">
|
<div v-if="canPay" class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
@@ -1058,8 +672,8 @@
|
|||||||
dense
|
dense
|
||||||
v-model.number="parse.data.amount"
|
v-model.number="parse.data.amount"
|
||||||
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
|
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
|
||||||
:mask="parse.data.unit == 'sat' ? '#' : ''"
|
:mask="parse.data.unit != 'sat' ? '#.##' : '#'"
|
||||||
:step="parse.data.unit == 'sat' ? '1': '0.01'"
|
:step="parse.data.unit != 'sat' ? '0.01' : '1'"
|
||||||
fill-mask="0"
|
fill-mask="0"
|
||||||
reverse-fill-mask
|
reverse-fill-mask
|
||||||
:min="parse.lnurlpay.minSendable / 1000"
|
:min="parse.lnurlpay.minSendable / 1000"
|
||||||
|
|||||||
@@ -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 %}
|
|
||||||
@@ -29,46 +29,7 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="setFeeDialog.show">
|
<q-dialog v-model="openChannelDialog.show" position="top">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<label class="text-h6">Set Channel Fee</label>
|
|
||||||
<p class="text-caption" v-text="setFeeDialog.channel_id"></p>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-form class="q-gutter-md">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
v-model.number="setFeeDialog.data.fee_ppm"
|
|
||||||
label="Fee Rate PPM"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
v-model.number="setFeeDialog.data.fee_base_msat"
|
|
||||||
label="Fee Base msat"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
:label="$t('set')"
|
|
||||||
color="primary"
|
|
||||||
@click="setChannelFee(setFeeDialog.channel_id)"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
:label="$t('cancel')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog v-model="openChannelDialog.show">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<q-form class="q-gutter-md">
|
<q-form class="q-gutter-md">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -210,41 +171,10 @@
|
|||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<div class="q-pb-sm">
|
<div class="q-pb-sm">
|
||||||
<div class="row items-center q-gutter-sm">
|
<div class="row items-center q-gutter-sm">
|
||||||
<div class="text-subtitle1" v-text="props.row.name"></div>
|
<div
|
||||||
<div class="text-caption" v-if="props.row.peer_id">
|
class="text-subtitle1 col-grow"
|
||||||
<span>Peer ID</span>
|
v-text="props.row.name"
|
||||||
<q-btn
|
></div>
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="content_paste"
|
|
||||||
@click="copyText(props.row.peer_id)"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="text-caption col-grow">
|
|
||||||
<span>Fees</span>
|
|
||||||
<q-btn
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="settings"
|
|
||||||
@click="showSetFeeDialog(props.row.id)"
|
|
||||||
></q-btn>
|
|
||||||
<span v-if="props.row.fee_ppm">
|
|
||||||
<span v-text="props.row.fee_ppm"></span> ppm,
|
|
||||||
<span v-text="props.row.fee_base_msat"></span> msat
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-caption" v-if="props.row.id">
|
|
||||||
<span>Channel ID</span>
|
|
||||||
<q-btn
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="content_paste"
|
|
||||||
@click="copyText(props.row.id)"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="text-caption" v-if="props.row.short_id">
|
<div class="text-caption" v-if="props.row.short_id">
|
||||||
<span v-text="props.row.short_id"></span>
|
<span v-text="props.row.short_id"></span>
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -271,7 +201,6 @@
|
|||||||
color="pink"
|
color="pink"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<lnbits-channel-balance
|
<lnbits-channel-balance
|
||||||
:balance="props.row.balance"
|
:balance="props.row.balance"
|
||||||
:color="props.row.color"
|
:color="props.row.color"
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
<!---->
|
%} {% block page %}
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
|
||||||
<!---->
|
|
||||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
|
||||||
|
|
||||||
<q-dialog v-model="nodeInfoDialog.show" position="top">
|
<q-dialog v-model="nodeInfoDialog.show" position="top">
|
||||||
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
|
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
|
||||||
@@ -43,4 +40,336 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script>
|
||||||
|
window.app = Vue.createApp({
|
||||||
|
el: '#vue',
|
||||||
|
config: {
|
||||||
|
globalProperties: {
|
||||||
|
LNbits,
|
||||||
|
msg: 'hello'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mixins: [window.windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
isSuperUser: false,
|
||||||
|
wallet: {},
|
||||||
|
tab: 'dashboard',
|
||||||
|
payments: 1000,
|
||||||
|
info: {},
|
||||||
|
channel_stats: {},
|
||||||
|
|
||||||
|
channels: {
|
||||||
|
data: [],
|
||||||
|
filter: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
activeBalance: {},
|
||||||
|
ranks: {},
|
||||||
|
|
||||||
|
peers: {
|
||||||
|
data: [],
|
||||||
|
filter: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
connectPeerDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
openChannelDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeChannelDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
nodeInfoDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
transactionDetailsDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
states: [
|
||||||
|
{label: 'Active', value: 'active', color: 'green'},
|
||||||
|
{label: 'Pending', value: 'pending', color: 'orange'},
|
||||||
|
{label: 'Inactive', value: 'inactive', color: 'grey'},
|
||||||
|
{label: 'Closed', value: 'closed', color: 'red'}
|
||||||
|
],
|
||||||
|
|
||||||
|
stateFilters: [
|
||||||
|
{label: 'Active', value: 'active'},
|
||||||
|
{label: 'Pending', value: 'pending'}
|
||||||
|
],
|
||||||
|
|
||||||
|
paymentsTable: {
|
||||||
|
data: [],
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'pending',
|
||||||
|
label: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'memo',
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('memo'),
|
||||||
|
field: 'memo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('date'),
|
||||||
|
field: 'date',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sat',
|
||||||
|
align: 'right',
|
||||||
|
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
||||||
|
field: row => this.formatMsat(row.amount),
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fee',
|
||||||
|
align: 'right',
|
||||||
|
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
|
||||||
|
field: 'fee'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'destination',
|
||||||
|
align: 'right',
|
||||||
|
label: 'Destination',
|
||||||
|
field: 'destination'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10,
|
||||||
|
page: 1,
|
||||||
|
rowsNumber: 10
|
||||||
|
},
|
||||||
|
filter: null
|
||||||
|
},
|
||||||
|
invoiceTable: {
|
||||||
|
data: [],
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'pending',
|
||||||
|
label: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'memo',
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('memo'),
|
||||||
|
field: 'memo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'paid_at',
|
||||||
|
field: 'paid_at',
|
||||||
|
align: 'right',
|
||||||
|
label: 'Paid at',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'expiry',
|
||||||
|
label: this.$t('expiry'),
|
||||||
|
field: 'expiry',
|
||||||
|
align: 'right',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
||||||
|
field: row => this.formatMsat(row.amount),
|
||||||
|
sortable: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10,
|
||||||
|
page: 1,
|
||||||
|
rowsNumber: 10
|
||||||
|
},
|
||||||
|
filter: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
this.getInfo()
|
||||||
|
this.get1MLStats()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
tab: function (val) {
|
||||||
|
if (val === 'transactions' && !this.paymentsTable.data.length) {
|
||||||
|
this.getPayments()
|
||||||
|
this.getInvoices()
|
||||||
|
} else if (val === 'channels' && !this.channels.data.length) {
|
||||||
|
this.getChannels()
|
||||||
|
this.getPeers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
checkChanges() {
|
||||||
|
return !_.isEqual(this.settings, this.formData)
|
||||||
|
},
|
||||||
|
filteredChannels: function () {
|
||||||
|
return this.stateFilters
|
||||||
|
? this.channels.data.filter(channel => {
|
||||||
|
return this.stateFilters.find(({value}) => value == channel.state)
|
||||||
|
})
|
||||||
|
: this.channels.data
|
||||||
|
},
|
||||||
|
totalBalance: function () {
|
||||||
|
return this.filteredChannels.reduce(
|
||||||
|
(balance, channel) => {
|
||||||
|
balance.local_msat += channel.balance.local_msat
|
||||||
|
balance.remote_msat += channel.balance.remote_msat
|
||||||
|
balance.total_msat += channel.balance.total_msat
|
||||||
|
return balance
|
||||||
|
},
|
||||||
|
{local_msat: 0, remote_msat: 0, total_msat: 0}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatMsat: function (msat) {
|
||||||
|
return LNbits.utils.formatMsat(msat)
|
||||||
|
},
|
||||||
|
api: function (method, url, options) {
|
||||||
|
const params = new URLSearchParams(options?.query)
|
||||||
|
return LNbits.api
|
||||||
|
.request(method, `/node/api/v1${url}?${params}`, {}, options?.data)
|
||||||
|
.catch(error => {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getChannels: function () {
|
||||||
|
return this.api('GET', '/channels').then(response => {
|
||||||
|
this.channels.data = response.data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getInfo: function () {
|
||||||
|
return this.api('GET', '/info').then(response => {
|
||||||
|
this.info = response.data
|
||||||
|
this.channel_stats = response.data.channel_stats
|
||||||
|
})
|
||||||
|
},
|
||||||
|
get1MLStats: function () {
|
||||||
|
return this.api('GET', '/rank').then(response => {
|
||||||
|
this.ranks = response.data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getPayments: function (props) {
|
||||||
|
if (props) {
|
||||||
|
this.paymentsTable.pagination = props.pagination
|
||||||
|
}
|
||||||
|
let pagination = this.paymentsTable.pagination
|
||||||
|
const query = {
|
||||||
|
limit: pagination.rowsPerPage,
|
||||||
|
offset: (pagination.page - 1) * pagination.rowsPerPage ?? 0
|
||||||
|
}
|
||||||
|
return this.api('GET', '/payments', {query}).then(response => {
|
||||||
|
this.paymentsTable.data = response.data.data
|
||||||
|
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getInvoices: function (props) {
|
||||||
|
if (props) {
|
||||||
|
this.invoiceTable.pagination = props.pagination
|
||||||
|
}
|
||||||
|
let pagination = this.invoiceTable.pagination
|
||||||
|
const query = {
|
||||||
|
limit: pagination.rowsPerPage,
|
||||||
|
offset: (pagination.page - 1) * pagination.rowsPerPage ?? 0
|
||||||
|
}
|
||||||
|
return this.api('GET', '/invoices', {query}).then(response => {
|
||||||
|
this.invoiceTable.data = response.data.data
|
||||||
|
this.invoiceTable.pagination.rowsNumber = response.data.total
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getPeers: function () {
|
||||||
|
return this.api('GET', '/peers').then(response => {
|
||||||
|
this.peers.data = response.data
|
||||||
|
console.log('peers', this.peers)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
connectPeer: function () {
|
||||||
|
console.log('peer', this.connectPeerDialog)
|
||||||
|
this.api('POST', '/peers', {data: this.connectPeerDialog.data}).then(
|
||||||
|
() => {
|
||||||
|
this.connectPeerDialog.show = false
|
||||||
|
this.getPeers()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
disconnectPeer: function (id) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Do you really wanna disconnect this peer?')
|
||||||
|
.onOk(() => {
|
||||||
|
this.api('DELETE', `/peers/${id}`).then(response => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'Disconnected',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
this.needsRestart = true
|
||||||
|
this.getPeers()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openChannel: function () {
|
||||||
|
this.api('POST', '/channels', {data: this.openChannelDialog.data})
|
||||||
|
.then(response => {
|
||||||
|
this.openChannelDialog.show = false
|
||||||
|
this.getChannels()
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showCloseChannelDialog: function (channel) {
|
||||||
|
this.closeChannelDialog.show = true
|
||||||
|
this.closeChannelDialog.data = {
|
||||||
|
force: false,
|
||||||
|
short_id: channel.short_id,
|
||||||
|
...channel.point
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeChannel: function () {
|
||||||
|
this.api('DELETE', '/channels', {
|
||||||
|
query: this.closeChannelDialog.data
|
||||||
|
}).then(response => {
|
||||||
|
this.closeChannelDialog.show = false
|
||||||
|
this.getChannels()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showOpenChannelDialog: function (peer_id) {
|
||||||
|
this.openChannelDialog.show = true
|
||||||
|
this.openChannelDialog.data = {peer_id, funding_amount: 0}
|
||||||
|
},
|
||||||
|
showNodeInfoDialog: function (node) {
|
||||||
|
this.nodeInfoDialog.show = true
|
||||||
|
this.nodeInfoDialog.data = node
|
||||||
|
},
|
||||||
|
showTransactionDetailsDialog: function (details) {
|
||||||
|
this.transactionDetailsDialog.show = true
|
||||||
|
this.transactionDetailsDialog.data = details
|
||||||
|
console.log('details', details)
|
||||||
|
},
|
||||||
|
shortenNodeId(nodeId) {
|
||||||
|
return nodeId
|
||||||
|
? nodeId.substring(0, 5) + '...' + nodeId.substring(nodeId.length - 5)
|
||||||
|
: '...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<script src="{{ static_url_for('static', 'js/node.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,432 +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"></div>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showPaymentStatus"
|
|
||||||
:label="$t('payments_status_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showPaymentTags"
|
|
||||||
:label="$t('payments_tag_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showBalance"
|
|
||||||
:label="$t('payments_balance_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showWalletsSize"
|
|
||||||
:label="$t('payments_wallets_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showBalanceInOut"
|
|
||||||
:label="$t('payments_balance_in_out_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showPaymentCountInOut"
|
|
||||||
:label="$t('payments_count_in_out_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<q-btn icon="event" outline flat>
|
|
||||||
<q-popup-proxy
|
|
||||||
cover
|
|
||||||
transition-show="scale"
|
|
||||||
transition-hide="scale"
|
|
||||||
>
|
|
||||||
<q-date v-model="searchDate" mask="YYYY-MM-DD" range />
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-6">
|
|
||||||
<q-btn
|
|
||||||
label="Search"
|
|
||||||
@click="searchByDate()"
|
|
||||||
color="primary"
|
|
||||||
flat
|
|
||||||
class="float-left"
|
|
||||||
v-close-popup
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
@click="clearDateSeach()"
|
|
||||||
label="Clear"
|
|
||||||
class="float-right"
|
|
||||||
color="grey"
|
|
||||||
flat
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-popup-proxy>
|
|
||||||
<q-badge
|
|
||||||
v-if="searchDate?.to || searchDate?.from"
|
|
||||||
class="q-mt-lg q-mr-md"
|
|
||||||
color="primary"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
style="border-radius: 6px"
|
|
||||||
/>
|
|
||||||
</q-btn>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
v-if="g.user.admin"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
icon="settings"
|
|
||||||
to="/admin#server"
|
|
||||||
>
|
|
||||||
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-show="!showDetails">
|
|
||||||
<div class="row q-col-gutter-md justify-center q-mb-md">
|
|
||||||
<div
|
|
||||||
v-show="chartData.showPaymentStatus"
|
|
||||||
class="col-lg-3 col-md-6 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('payment_chart_status')"></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsStatusChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="chartData.showPaymentStatus"
|
|
||||||
class="col-lg-3 col-md-6 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('payment_chart_tags')"></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsTagsChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="chartData.showBalance"
|
|
||||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong
|
|
||||||
v-text="$t('lnbits_balance', {balance: (lnbitsBalance || 0).toLocaleString()})"
|
|
||||||
></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsDailyChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="chartData.showWalletsSize"
|
|
||||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('payment_chart_tx_per_wallet')"></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsWalletsChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-show="chartData.showBalanceInOut"
|
|
||||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('payments_balance_in_out')"></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsBalanceInOutChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="chartData.showPaymentCountInOut"
|
|
||||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('payments_count_in_out')"></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsCountInOutChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
|
||||||
<div class="col">
|
|
||||||
<q-card class="q-pa-md">
|
|
||||||
<q-table
|
|
||||||
row-key="payment_hash"
|
|
||||||
:rows="payments"
|
|
||||||
:columns="paymentsTable.columns"
|
|
||||||
v-model:pagination="paymentsTable.pagination"
|
|
||||||
:filter="paymentsTable.search"
|
|
||||||
@request="fetchPayments"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
<q-input
|
|
||||||
v-if="['wallet_id', 'payment_hash', 'memo'].includes(col.name)"
|
|
||||||
v-model="searchData[col.name]"
|
|
||||||
@keydown.enter="searchPaymentsBy()"
|
|
||||||
@update:model-value="searchPaymentsBy()"
|
|
||||||
dense
|
|
||||||
type="text"
|
|
||||||
filled
|
|
||||||
clearable
|
|
||||||
:label="col.label"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
name="search"
|
|
||||||
@click="searchPaymentsBy()"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
<q-btn
|
|
||||||
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>
|
|
||||||
<q-select
|
|
||||||
v-else-if="['tag'].includes(col.name)"
|
|
||||||
v-model="searchData[col.name]"
|
|
||||||
:options="searchOptions[col.name]"
|
|
||||||
@update:model-value="searchPaymentsBy()"
|
|
||||||
:label="col.label"
|
|
||||||
clearable
|
|
||||||
style="width: 100px"
|
|
||||||
></q-select>
|
|
||||||
<span v-else v-text="col.label"></span>
|
|
||||||
</q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr auto-width :props="props">
|
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
<div v-if="col.name == 'status'">
|
|
||||||
<q-tooltip
|
|
||||||
><span v-text="$t('payment_details')"></span
|
|
||||||
></q-tooltip>
|
|
||||||
<q-icon
|
|
||||||
@click="showDetailsToggle(props.row)"
|
|
||||||
v-if="props.row.status === 'success'"
|
|
||||||
size="14px"
|
|
||||||
:name="props.row.outgoing ? 'call_made' : 'call_received'"
|
|
||||||
:color="props.row.outgoing ? 'pink' : 'green'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon
|
|
||||||
v-else-if="props.row.status === 'pending'"
|
|
||||||
@click="showDetailsToggle(props.row)"
|
|
||||||
name="downloading"
|
|
||||||
:style="props.row.outgoing ? 'transform: rotate(225deg)' : 'transform: scaleX(-1) rotate(315deg)'"
|
|
||||||
color="grey"
|
|
||||||
class="cursor-pointer"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon
|
|
||||||
v-else
|
|
||||||
@click="showDetailsToggle(props.row)"
|
|
||||||
name="warning"
|
|
||||||
color="yellow"
|
|
||||||
class="cursor-pointer"
|
|
||||||
></q-icon>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="col.name == 'created_at'">
|
|
||||||
<div>
|
|
||||||
<q-tooltip anchor="top middle">
|
|
||||||
<span v-text="formatDate(props.row.created_at)"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
<span v-text="props.row.timeFrom"> </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="['wallet_id', 'payment_hash', 'memo'].includes(col.name)"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-if="props.row[col.name]"
|
|
||||||
icon="content_copy"
|
|
||||||
size="sm"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-mr-xs"
|
|
||||||
@click="copyText(props.row[col.name])"
|
|
||||||
>
|
|
||||||
<q-tooltip anchor="top middle">Copy</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<span v-text="shortify(props.row[col.name], col.max_length)">
|
|
||||||
</span>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="props.row[col.name]"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
v-text="props.row[col.name]"
|
|
||||||
class="cursor-pointer"
|
|
||||||
></span>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-show="showDetails">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section class="flex">
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
icon="arrow_back"
|
|
||||||
class="q-mr-md"
|
|
||||||
@click="showDetailsToggle(null)"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="self-center text-h6 text-weight-bolder text-grey-5">
|
|
||||||
<span v-text="$t('payment_details_back')"></span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-card-section class="text-h6">
|
|
||||||
<q-item>
|
|
||||||
<q-item-section avatar class="">
|
|
||||||
<q-icon color="primary" name="receipt" size="44px"></q-icon>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>
|
|
||||||
<div class="text-h6">
|
|
||||||
<span v-text="$t('payment_details')"></span>
|
|
||||||
</div>
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label caption v-text="$t('payment_details_desc')">
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-list separator>
|
|
||||||
<q-item v-for="(value, key) in paymentDetails" :key="key">
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="key"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="value"
|
|
||||||
style="word-wrap: break-word"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section side>
|
|
||||||
<q-btn
|
|
||||||
v-show="value"
|
|
||||||
icon="content_copy"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-ml-sm"
|
|
||||||
@click="copyText(value)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-item-section>
|
|
||||||
<!-- <q-separator></q-separator> -->
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
:label="$t('set_password')"
|
:label="$t('set_password')"
|
||||||
v-model="activeUser.setPassword"
|
v-model="activeUser.setPassword"
|
||||||
>
|
>
|
||||||
<q-tooltip v-text="$t('set_password_tooltip')"></q-tooltip>
|
<q-tooltip>Toggle Admin</q-tooltip>
|
||||||
</q-toggle>
|
</q-toggle>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
@@ -105,12 +105,11 @@
|
|||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="activeUser.data.pubkey"
|
v-model="activeUser.data.pubkey"
|
||||||
:label="'Nostr '+ $t('pubkey')"
|
:label="$t('pubkey')"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
<q-tooltip v-text="$t('nostr_pubkey_tooltip')"></q-tooltip>
|
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="activeUser.data.email"
|
v-model="activeUser.data.email"
|
||||||
@@ -147,14 +146,6 @@
|
|||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-input
|
|
||||||
v-model="activeUser.data.external_id"
|
|
||||||
:label="$t('external_id')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="activeUser.data.extra.picture"
|
v-model="activeUser.data.extra.picture"
|
||||||
:label="$t('picture')"
|
:label="$t('picture')"
|
||||||
|
|||||||
@@ -15,36 +15,19 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="activeWallet.show">
|
<div v-else-if="activeWallet.show">
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
<div class="row q-mb-lg">
|
||||||
<div class="col-12">
|
<div class="col">
|
||||||
<q-card>
|
<q-btn
|
||||||
<div class="q-pa-sm">
|
icon="arrow_back_ios"
|
||||||
<div class="row">
|
@click="backToUsersPage()"
|
||||||
<div class="q-pa-xs">
|
:label="$t('back')"
|
||||||
<q-btn
|
></q-btn>
|
||||||
icon="arrow_back_ios"
|
<q-btn
|
||||||
@click="backToUsersPage()"
|
@click="createWalletDialog.show = true"
|
||||||
:label="$t('back')"
|
:label="$t('create_new_wallet')"
|
||||||
></q-btn>
|
color="primary"
|
||||||
</div>
|
class="q-ml-md"
|
||||||
<div class="q-pa-xs">
|
></q-btn>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-card class="q-pa-md">
|
<q-card class="q-pa-md">
|
||||||
@@ -52,7 +35,6 @@
|
|||||||
<q-table :rows="wallets" :columns="walletTable.columns">
|
<q-table :rows="wallets" :columns="walletTable.columns">
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-th auto-width v-if="g.user.super_user"></q-th>
|
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th
|
<q-th
|
||||||
auto-width
|
auto-width
|
||||||
@@ -65,14 +47,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td auto-width v-if="g.user.super_user">
|
<q-td auto-width>
|
||||||
<lnbits-update-balance
|
<lnbits-update-balance
|
||||||
:wallet_id="props.row.id"
|
:wallet_id="props.row.id"
|
||||||
@credit-value="handleBalanceUpdate"
|
@credit-value="handleBalanceUpdate"
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
></lnbits-update-balance>
|
></lnbits-update-balance>
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
icon="menu"
|
icon="menu"
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
<!---->
|
%} {% block page %}
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
|
||||||
<!---->
|
|
||||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
<div class="row q-col-gutter-md justify-center">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -14,35 +11,13 @@
|
|||||||
{%include "users/_createWalletDialog.html" %}
|
{%include "users/_createWalletDialog.html" %}
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
<q-btn
|
||||||
<div class="col-12">
|
@click="showAccountPage()"
|
||||||
<q-card>
|
:label="$t('create_account')"
|
||||||
<div class="q-pa-sm">
|
color="primary"
|
||||||
<div class="row items-center justify-between q-gutter-xs">
|
class="q-mb-md"
|
||||||
<div class="col">
|
>
|
||||||
<q-btn
|
</q-btn>
|
||||||
@click="showAccountPage()"
|
|
||||||
:label="$t('create_account')"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
v-if="g.user.admin"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
icon="settings"
|
|
||||||
to="/admin#users"
|
|
||||||
>
|
|
||||||
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-card class="q-pa-md">
|
<q-card class="q-pa-md">
|
||||||
<q-table
|
<q-table
|
||||||
@@ -127,17 +102,7 @@
|
|||||||
:label="props.row.wallet_count"
|
:label="props.row.wallet_count"
|
||||||
@click="fetchWallets(props.row.id)"
|
@click="fetchWallets(props.row.id)"
|
||||||
>
|
>
|
||||||
</q-btn>
|
<q-tooltip>Show Wallets</q-tooltip>
|
||||||
<q-btn
|
|
||||||
v-if="(users.length == 1) && searchData.wallet_id"
|
|
||||||
round
|
|
||||||
icon="menu"
|
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
class="q-ml-sm"
|
|
||||||
@click="showWalletPayments(searchData.wallet_id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Show Payments</q-tooltip>
|
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
@@ -183,4 +148,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script src="{{ static_url_for('static', 'js/users.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+11
-130
@@ -1,38 +1,28 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
from shutil import make_archive
|
||||||
from shutil import make_archive, move
|
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from tempfile import NamedTemporaryFile
|
from typing import Optional
|
||||||
from typing import IO, Optional
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import filetype
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi import APIRouter, Depends, File, Header, HTTPException, UploadFile
|
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.core.models.misc import Image, SimpleStatus
|
|
||||||
from lnbits.core.models.notifications import NotificationType
|
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
enqueue_notification,
|
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
update_cached_settings,
|
update_cached_settings,
|
||||||
)
|
)
|
||||||
from lnbits.core.services.notifications import send_email_notification
|
|
||||||
from lnbits.core.services.settings import dict_to_settings
|
|
||||||
from lnbits.decorators import check_admin, check_super_user
|
from lnbits.decorators import check_admin, check_super_user
|
||||||
from lnbits.helpers import safe_upload_file_path
|
|
||||||
from lnbits.server import server_restart
|
from lnbits.server import server_restart
|
||||||
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||||
from lnbits.tasks import invoice_listeners
|
from lnbits.tasks import invoice_listeners
|
||||||
|
|
||||||
from .. import core_app_extra
|
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")
|
admin_router = APIRouter(tags=["Admin UI"], prefix="/admin")
|
||||||
file_upload = File(...)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get(
|
@admin_router.get(
|
||||||
@@ -57,18 +47,6 @@ async def api_monitor():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get(
|
|
||||||
"/api/v1/testemail",
|
|
||||||
name="TestEmail",
|
|
||||||
description="send a test email to the admin",
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def api_test_email():
|
|
||||||
return await send_email_notification(
|
|
||||||
"This is a LNbits test email.", "LNbits Test Email"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
||||||
async def api_get_settings(
|
async def api_get_settings(
|
||||||
user: User = Depends(check_admin),
|
user: User = Depends(check_admin),
|
||||||
@@ -82,7 +60,6 @@ async def api_get_settings(
|
|||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
|
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
|
||||||
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
|
||||||
await update_admin_settings(data)
|
await update_admin_settings(data)
|
||||||
admin_settings = await get_admin_settings(user.super_user)
|
admin_settings = await get_admin_settings(user.super_user)
|
||||||
assert admin_settings, "Updated admin settings not found."
|
assert admin_settings, "Updated admin settings not found."
|
||||||
@@ -91,15 +68,6 @@ async def api_update_settings(data: UpdateSettings, user: User = Depends(check_a
|
|||||||
return {"status": "Success"}
|
return {"status": "Success"}
|
||||||
|
|
||||||
|
|
||||||
@admin_router.patch(
|
|
||||||
"/api/v1/settings",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
)
|
|
||||||
async def api_update_settings_partial(data: dict, user: User = Depends(check_admin)):
|
|
||||||
updatable_settings = dict_to_settings({**settings.dict(), **data})
|
|
||||||
return await api_update_settings(updatable_settings, user)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get(
|
@admin_router.get(
|
||||||
"/api/v1/settings/default",
|
"/api/v1/settings/default",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
@@ -110,10 +78,13 @@ async def api_reset_settings(field_name: str):
|
|||||||
return {"default_value": getattr(default_settings, field_name)}
|
return {"default_value": getattr(default_settings, field_name)}
|
||||||
|
|
||||||
|
|
||||||
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
@admin_router.delete(
|
||||||
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
"/api/v1/settings",
|
||||||
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
status_code=HTTPStatus.OK,
|
||||||
await reset_core_settings()
|
dependencies=[Depends(check_super_user)],
|
||||||
|
)
|
||||||
|
async def api_delete_settings() -> None:
|
||||||
|
await delete_admin_settings()
|
||||||
server_restart.set()
|
server_restart.set()
|
||||||
|
|
||||||
|
|
||||||
@@ -165,93 +136,3 @@ async def api_download_backup() -> FileResponse:
|
|||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.post(
|
|
||||||
"/api/v1/images",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def upload_image(
|
|
||||||
file: UploadFile = file_upload,
|
|
||||||
content_length: int = Header(..., le=settings.lnbits_upload_size_bytes),
|
|
||||||
) -> Image:
|
|
||||||
if not file.filename:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="No filename provided."
|
|
||||||
)
|
|
||||||
|
|
||||||
# validate file types
|
|
||||||
file_info = filetype.guess(file.file)
|
|
||||||
if file_info is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE,
|
|
||||||
detail="Unable to determine file type",
|
|
||||||
)
|
|
||||||
detected_content_type = file_info.extension.lower()
|
|
||||||
if (
|
|
||||||
file.content_type not in settings.lnbits_upload_allowed_types
|
|
||||||
or detected_content_type not in settings.lnbits_upload_allowed_types
|
|
||||||
):
|
|
||||||
raise HTTPException(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported file type")
|
|
||||||
|
|
||||||
# validate file name
|
|
||||||
try:
|
|
||||||
file_path = safe_upload_file_path(file.filename)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail=f"The requested filename '{file.filename}' is forbidden.",
|
|
||||||
) from e
|
|
||||||
|
|
||||||
# validate file size
|
|
||||||
real_file_size = 0
|
|
||||||
temp: IO = NamedTemporaryFile(delete=False)
|
|
||||||
for chunk in file.file:
|
|
||||||
real_file_size += len(chunk)
|
|
||||||
if real_file_size > content_length:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
|
|
||||||
detail=f"File too large ({content_length / 1000} KB max)",
|
|
||||||
)
|
|
||||||
temp.write(chunk)
|
|
||||||
temp.close()
|
|
||||||
|
|
||||||
move(temp.name, file_path)
|
|
||||||
return Image(filename=file.filename)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get(
|
|
||||||
"/api/v1/images",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def list_uploaded_images() -> list[Image]:
|
|
||||||
image_folder = Path(settings.lnbits_data_folder, "images")
|
|
||||||
files = image_folder.glob("*")
|
|
||||||
images = []
|
|
||||||
for file in files:
|
|
||||||
if file.is_file():
|
|
||||||
images.append(Image(filename=file.name))
|
|
||||||
return images
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.delete(
|
|
||||||
"/api/v1/images/{filename}",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def delete_uploaded_image(filename: str) -> SimpleStatus:
|
|
||||||
try:
|
|
||||||
file_path = safe_upload_file_path(filename)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail=f"The requested filename '{filename}' is forbidden.",
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if not file_path.exists():
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Image not found.")
|
|
||||||
|
|
||||||
file_path.unlink()
|
|
||||||
return SimpleStatus(success=True, message=f"{filename} deleted")
|
|
||||||
|
|||||||
+15
-19
@@ -15,6 +15,7 @@ from fastapi import (
|
|||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
|
from lnbits.core.crud import get_user
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
BaseWallet,
|
BaseWallet,
|
||||||
ConversionData,
|
ConversionData,
|
||||||
@@ -29,7 +30,6 @@ from lnbits.decorators import (
|
|||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import check_callback_url
|
|
||||||
from lnbits.lnurl import decode as lnurl_decode
|
from lnbits.lnurl import decode as lnurl_decode
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.exchange_rates import (
|
from lnbits.utils.exchange_rates import (
|
||||||
@@ -50,18 +50,21 @@ api_router = APIRouter(tags=["Core"])
|
|||||||
async def health() -> dict:
|
async def health() -> dict:
|
||||||
return {
|
return {
|
||||||
"server_time": int(time()),
|
"server_time": int(time()),
|
||||||
"up_time": settings.lnbits_server_up_time,
|
"up_time": int(time() - settings.server_startup_time),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/api/v1/status", status_code=HTTPStatus.OK)
|
@api_router.get("/api/v1/status", status_code=HTTPStatus.OK)
|
||||||
async def health_check(user: User = Depends(check_user_exists)) -> dict:
|
async def health_check(wallet: WalletTypeInfo = Depends(require_invoice_key)) -> dict:
|
||||||
stat: dict[str, Any] = {
|
stat: dict[str, Any] = {
|
||||||
"server_time": int(time()),
|
"server_time": int(time()),
|
||||||
"up_time": settings.lnbits_server_up_time,
|
"up_time": int(time() - settings.server_startup_time),
|
||||||
"up_time_seconds": int(time() - settings.server_startup_time),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user = await get_user(wallet.wallet.user)
|
||||||
|
if not user:
|
||||||
|
return stat
|
||||||
|
|
||||||
stat["version"] = settings.version
|
stat["version"] = settings.version
|
||||||
if not user.admin:
|
if not user.admin:
|
||||||
return stat
|
return stat
|
||||||
@@ -129,21 +132,14 @@ async def api_lnurlscan(
|
|||||||
else:
|
else:
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
||||||
check_callback_url(url)
|
r = await client.get(url, timeout=5)
|
||||||
try:
|
r.raise_for_status()
|
||||||
r = await client.get(url, timeout=5)
|
if r.is_error:
|
||||||
r.raise_for_status()
|
|
||||||
except httpx.HTTPStatusError as exc:
|
|
||||||
if exc.response.status_code == 404:
|
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Not found") from exc
|
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
detail={
|
detail={"domain": domain, "message": "failed to get parameters"},
|
||||||
"domain": domain,
|
)
|
||||||
"message": "failed to get parameters",
|
|
||||||
},
|
|
||||||
) from exc
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(r.text)
|
data = json.loads(r.text)
|
||||||
except json.decoder.JSONDecodeError as exc:
|
except json.decoder.JSONDecodeError as exc:
|
||||||
@@ -231,7 +227,7 @@ async def api_perform_lnurlauth(
|
|||||||
|
|
||||||
@api_router.get(
|
@api_router.get(
|
||||||
"/api/v1/rate/history",
|
"/api/v1/rate/history",
|
||||||
dependencies=[Depends(check_user_exists)],
|
dependencies=[Depends(require_invoice_key)],
|
||||||
)
|
)
|
||||||
async def api_exchange_rate_history() -> list[dict]:
|
async def api_exchange_rate_history() -> list[dict]:
|
||||||
return settings.lnbits_exchange_rate_history
|
return settings.lnbits_exchange_rate_history
|
||||||
|
|||||||
+31
-171
@@ -11,26 +11,12 @@ from fastapi.responses import JSONResponse, RedirectResponse
|
|||||||
from fastapi_sso.sso.base import OpenID, SSOBase
|
from fastapi_sso.sso.base import OpenID, SSOBase
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud.users import (
|
|
||||||
get_user_access_control_lists,
|
|
||||||
update_user_access_control_list,
|
|
||||||
)
|
|
||||||
from lnbits.core.models.misc import SimpleItem
|
|
||||||
from lnbits.core.models.users import (
|
|
||||||
ApiTokenRequest,
|
|
||||||
ApiTokenResponse,
|
|
||||||
DeleteAccessControlList,
|
|
||||||
DeleteTokenRequest,
|
|
||||||
EndpointAccess,
|
|
||||||
UpdateAccessControlList,
|
|
||||||
)
|
|
||||||
from lnbits.core.services import create_user_account
|
from lnbits.core.services import create_user_account
|
||||||
from lnbits.decorators import access_token_payload, check_user_exists
|
from lnbits.decorators import access_token_payload, check_user_exists
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
decrypt_internal_message,
|
decrypt_internal_message,
|
||||||
encrypt_internal_message,
|
encrypt_internal_message,
|
||||||
get_api_routes,
|
|
||||||
is_valid_email_address,
|
is_valid_email_address,
|
||||||
is_valid_username,
|
is_valid_username,
|
||||||
)
|
)
|
||||||
@@ -58,7 +44,6 @@ from ..models import (
|
|||||||
UpdateUserPassword,
|
UpdateUserPassword,
|
||||||
UpdateUserPubkey,
|
UpdateUserPubkey,
|
||||||
User,
|
User,
|
||||||
UserAcls,
|
|
||||||
UserExtra,
|
UserExtra,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,7 +59,7 @@ async def get_auth_user(user: User = Depends(check_user_exists)) -> User:
|
|||||||
async def login(data: LoginUsernamePassword) -> JSONResponse:
|
async def login(data: LoginUsernamePassword) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||||
raise HTTPException(
|
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)
|
account = await get_account_by_username_or_email(data.username)
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
@@ -85,7 +70,9 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
|||||||
@auth_router.post("/nostr", description="Login via Nostr")
|
@auth_router.post("/nostr", description="Login via Nostr")
|
||||||
async def nostr_login(request: Request) -> JSONResponse:
|
async def nostr_login(request: Request) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
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)
|
event = _nostr_nip98_event(request)
|
||||||
account = await get_account_by_pubkey(event["pubkey"])
|
account = await get_account_by_pubkey(event["pubkey"])
|
||||||
if not account:
|
if not account:
|
||||||
@@ -102,137 +89,15 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
async def login_usr(data: LoginUsr) -> JSONResponse:
|
async def login_usr(data: LoginUsr) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.UNAUTHORIZED,
|
||||||
"Login by 'User ID' not allowed.",
|
"Login by 'User ID' not allowed.",
|
||||||
)
|
)
|
||||||
account = await get_account(data.usr)
|
account = await get_account(data.usr)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
|
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."
|
|
||||||
)
|
|
||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.get("/acl")
|
|
||||||
async def api_get_user_acls(
|
|
||||||
request: Request,
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
) -> UserAcls:
|
|
||||||
api_routes = get_api_routes(request.app.router.routes)
|
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
|
||||||
|
|
||||||
# Add missing/new endpoints to the ACLs
|
|
||||||
for acl in acls.access_control_list:
|
|
||||||
acl_api_routes = {**api_routes}
|
|
||||||
for route in api_routes.keys():
|
|
||||||
if acl.get_endpoint(route):
|
|
||||||
acl_api_routes.pop(route, None)
|
|
||||||
|
|
||||||
for path, name in acl_api_routes.items():
|
|
||||||
acl.endpoints.append(EndpointAccess(path=path, name=name))
|
|
||||||
acl.endpoints.sort(key=lambda e: e.name.lower())
|
|
||||||
|
|
||||||
return UserAcls(id=user.id, access_control_list=acls.access_control_list)
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/acl")
|
|
||||||
@auth_router.patch("/acl")
|
|
||||||
async def api_update_user_acl(
|
|
||||||
request: Request,
|
|
||||||
data: UpdateAccessControlList,
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
) -> UserAcls:
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
|
||||||
|
|
||||||
user_acls = await get_user_access_control_lists(user.id)
|
|
||||||
acl = user_acls.get_acl_by_id(data.id)
|
|
||||||
if acl:
|
|
||||||
user_acls.access_control_list.remove(acl)
|
|
||||||
else:
|
|
||||||
data.endpoints = []
|
|
||||||
data.id = uuid4().hex
|
|
||||||
|
|
||||||
api_routes = get_api_routes(request.app.router.routes)
|
|
||||||
for path, name in api_routes.items():
|
|
||||||
data.endpoints.append(EndpointAccess(path=path, name=name))
|
|
||||||
|
|
||||||
api_paths = get_api_routes(request.app.router.routes).keys()
|
|
||||||
data.endpoints = [e for e in data.endpoints if e.path in api_paths]
|
|
||||||
data.endpoints.sort(key=lambda e: e.name.lower())
|
|
||||||
|
|
||||||
user_acls.access_control_list.append(data)
|
|
||||||
user_acls.access_control_list.sort(key=lambda t: t.name.lower())
|
|
||||||
await update_user_access_control_list(user_acls)
|
|
||||||
|
|
||||||
return user_acls
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.delete("/acl")
|
|
||||||
async def api_delete_user_acl(
|
|
||||||
data: DeleteAccessControlList,
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
):
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
|
||||||
|
|
||||||
user_acls = await get_user_access_control_lists(user.id)
|
|
||||||
user_acls.delete_acl_by_id(data.id)
|
|
||||||
await update_user_access_control_list(user_acls)
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.post("/acl/token")
|
|
||||||
async def api_create_user_api_token(
|
|
||||||
data: ApiTokenRequest,
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
) -> ApiTokenResponse:
|
|
||||||
assert data.expiration_time_minutes > 0, "Expiration time must be in the future."
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
|
||||||
|
|
||||||
assert account.username, "Username must be configured."
|
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
|
||||||
acl = acls.get_acl_by_id(data.acl_id)
|
|
||||||
if not acl:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
|
||||||
|
|
||||||
api_token_id = uuid4().hex
|
|
||||||
api_token = _auth_api_token_response(
|
|
||||||
account.username, api_token_id, data.expiration_time_minutes
|
|
||||||
)
|
|
||||||
|
|
||||||
acl.token_id_list.append(SimpleItem(id=api_token_id, name=data.token_name))
|
|
||||||
await update_user_access_control_list(acls)
|
|
||||||
return ApiTokenResponse(id=api_token_id, api_token=api_token)
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.delete("/acl/token")
|
|
||||||
async def api_delete_user_api_token(
|
|
||||||
data: DeleteTokenRequest,
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
):
|
|
||||||
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
|
||||||
|
|
||||||
assert account.username, "Username must be configured."
|
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
|
||||||
acl = acls.get_acl_by_id(data.acl_id)
|
|
||||||
if not acl:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
|
||||||
acl.delete_token_by_id(data.id)
|
|
||||||
await update_user_access_control_list(acls)
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.get("/{provider}", description="SSO Provider")
|
@auth_router.get("/{provider}", description="SSO Provider")
|
||||||
async def login_with_sso_provider(
|
async def login_with_sso_provider(
|
||||||
request: Request, provider: str, user_id: Optional[str] = None
|
request: Request, provider: str, user_id: Optional[str] = None
|
||||||
@@ -240,7 +105,7 @@ async def login_with_sso_provider(
|
|||||||
provider_sso = _new_sso(provider)
|
provider_sso = _new_sso(provider)
|
||||||
if not provider_sso:
|
if not provider_sso:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.UNAUTHORIZED,
|
||||||
f"Login by '{provider}' not allowed.",
|
f"Login by '{provider}' not allowed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -255,7 +120,7 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons
|
|||||||
provider_sso = _new_sso(provider)
|
provider_sso = _new_sso(provider)
|
||||||
if not provider_sso:
|
if not provider_sso:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.UNAUTHORIZED,
|
||||||
f"Login by '{provider}' not allowed.",
|
f"Login by '{provider}' not allowed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -283,7 +148,7 @@ async def logout() -> JSONResponse:
|
|||||||
async def register(data: RegisterUser) -> JSONResponse:
|
async def register(data: RegisterUser) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.UNAUTHORIZED,
|
||||||
"Register by 'Username and Password' not allowed.",
|
"Register by 'Username and Password' not allowed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -373,7 +238,7 @@ async def update_password(
|
|||||||
async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||||
raise HTTPException(
|
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."
|
assert data.password == data.password_repeat, "Passwords do not match."
|
||||||
@@ -411,13 +276,23 @@ async def update(
|
|||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
||||||
if data.username and not is_valid_username(data.username):
|
if data.username and not is_valid_username(data.username):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
||||||
|
if data.email != user.email:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
"Email mismatch.",
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
data.username
|
data.username
|
||||||
and user.username != data.username
|
and user.username != data.username
|
||||||
and await get_account_by_username(data.username)
|
and await get_account_by_username(data.username)
|
||||||
):
|
):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
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)
|
account = await get_account(user.id)
|
||||||
if not account:
|
if not account:
|
||||||
@@ -425,6 +300,8 @@ async def update(
|
|||||||
|
|
||||||
if data.username:
|
if data.username:
|
||||||
account.username = data.username
|
account.username = data.username
|
||||||
|
if data.email:
|
||||||
|
account.email = data.email
|
||||||
if data.extra:
|
if data.extra:
|
||||||
account.extra = data.extra
|
account.extra = data.extra
|
||||||
|
|
||||||
@@ -435,7 +312,7 @@ async def update(
|
|||||||
@auth_router.put("/first_install")
|
@auth_router.put("/first_install")
|
||||||
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||||
if not settings.first_install:
|
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)
|
account = await get_account(settings.super_user)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
||||||
@@ -458,10 +335,10 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
|
|||||||
|
|
||||||
if verified_user_id:
|
if verified_user_id:
|
||||||
if account:
|
if account:
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Email already used.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Email already used.")
|
||||||
account = await get_account(verified_user_id)
|
account = await get_account(verified_user_id)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Cannot verify user email.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Cannot verify user email.")
|
||||||
redirect_path = "/account"
|
redirect_path = "/account"
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
@@ -485,37 +362,20 @@ def _auth_success_response(
|
|||||||
sub=username or "", usr=user_id, email=email, auth_time=int(time())
|
sub=username or "", usr=user_id, email=email, auth_time=int(time())
|
||||||
)
|
)
|
||||||
access_token = create_access_token(data=payload.dict())
|
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 = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
||||||
response.set_cookie(
|
response.set_cookie("cookie_access_token", access_token, httponly=True)
|
||||||
"cookie_access_token", access_token, httponly=True, max_age=max_age
|
response.set_cookie("is_lnbits_user_authorized", "true")
|
||||||
)
|
|
||||||
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
|
|
||||||
response.delete_cookie("is_access_token_expired")
|
response.delete_cookie("is_access_token_expired")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def _auth_api_token_response(
|
|
||||||
username: str, api_token_id: str, token_expire_minutes: int
|
|
||||||
):
|
|
||||||
payload = AccessTokenPayload(
|
|
||||||
sub=username, api_token_id=api_token_id, auth_time=int(time())
|
|
||||||
)
|
|
||||||
return create_access_token(
|
|
||||||
data=payload.dict(), token_expire_minutes=token_expire_minutes
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
||||||
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
|
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
|
||||||
access_token = create_access_token(data=payload.dict())
|
access_token = create_access_token(data=payload.dict())
|
||||||
max_age = settings.auth_token_expire_minutes * 60
|
|
||||||
response = RedirectResponse(path)
|
response = RedirectResponse(path)
|
||||||
response.set_cookie(
|
response.set_cookie("cookie_access_token", access_token, httponly=True)
|
||||||
"cookie_access_token", access_token, httponly=True, max_age=max_age
|
response.set_cookie("is_lnbits_user_authorized", "true")
|
||||||
)
|
|
||||||
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
|
|
||||||
response.delete_cookie("is_access_token_expired")
|
response.delete_cookie("is_access_token_expired")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -566,10 +426,10 @@ def _find_auth_provider_class(provider: str) -> Callable:
|
|||||||
def _nostr_nip98_event(request: Request) -> dict:
|
def _nostr_nip98_event(request: Request) -> dict:
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
if not auth_header:
|
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()
|
scheme, token = auth_header.split()
|
||||||
if scheme.lower() != "nostr":
|
if scheme.lower() != "nostr":
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid Authorization scheme.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid Authorization scheme.")
|
||||||
event = None
|
event = None
|
||||||
try:
|
try:
|
||||||
event_json = base64.b64decode(token.encode("ascii"))
|
event_json = base64.b64decode(token.encode("ascii"))
|
||||||
|
|||||||
@@ -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}'.",
|
|
||||||
)
|
|
||||||
@@ -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)
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Annotated, Optional, Union
|
from pathlib import Path
|
||||||
|
from typing import Annotated, List, Optional, Union
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -16,7 +17,7 @@ from lnbits.core.models.extensions import ExtensionMeta, InstallableExtension
|
|||||||
from lnbits.core.services import create_invoice, create_user_account
|
from lnbits.core.services import create_invoice, create_user_account
|
||||||
from lnbits.core.services.extensions import get_valid_extensions
|
from lnbits.core.services.extensions import get_valid_extensions
|
||||||
from lnbits.decorators import check_admin, check_user_exists
|
from lnbits.decorators import check_admin, check_user_exists
|
||||||
from lnbits.helpers import check_callback_url, template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ generic_router = APIRouter(
|
|||||||
|
|
||||||
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return RedirectResponse(settings.lnbits_qr_logo)
|
return FileResponse(Path("lnbits", "static", "favicon.ico"))
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/", response_class=HTMLResponse)
|
@generic_router.get("/", response_class=HTMLResponse)
|
||||||
@@ -49,9 +50,12 @@ async def home(request: Request, lightning: str = ""):
|
|||||||
@generic_router.get("/first_install", response_class=HTMLResponse)
|
@generic_router.get("/first_install", response_class=HTMLResponse)
|
||||||
async def first_install(request: Request):
|
async def first_install(request: Request):
|
||||||
if not settings.first_install:
|
if not settings.first_install:
|
||||||
raise HTTPException(
|
return template_renderer().TemplateResponse(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
request,
|
||||||
detail="Super user account has already been configured.",
|
"error.html",
|
||||||
|
{
|
||||||
|
"err": "Super user account has already been configured.",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
request,
|
request,
|
||||||
@@ -70,7 +74,7 @@ async def robots():
|
|||||||
|
|
||||||
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
|
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
|
||||||
async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
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]
|
installed_exts_ids = [e.id for e in installed_exts]
|
||||||
|
|
||||||
installable_exts = await InstallableExtension.get_installable_extensions()
|
installable_exts = await InstallableExtension.get_installable_extensions()
|
||||||
@@ -103,7 +107,7 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
|||||||
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
|
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
|
||||||
db_versions = await get_db_versions()
|
db_versions = await get_db_versions()
|
||||||
|
|
||||||
extension_data = [
|
extensions = [
|
||||||
{
|
{
|
||||||
"id": ext.id,
|
"id": ext.id,
|
||||||
"name": ext.name,
|
"name": ext.name,
|
||||||
@@ -148,8 +152,7 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
|||||||
"core/extensions.html",
|
"core/extensions.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"extension_data": extension_data,
|
"extensions": extensions,
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -176,25 +179,25 @@ async def wallet(
|
|||||||
wallet = user.wallets[0]
|
wallet = user.wallets[0]
|
||||||
|
|
||||||
if not wallet or wallet.deleted:
|
if not wallet or wallet.deleted:
|
||||||
raise HTTPException(
|
return template_renderer().TemplateResponse(
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
request, "error.html", {"err": "Wallet not found"}, HTTPStatus.NOT_FOUND
|
||||||
detail="Wallet not found",
|
|
||||||
)
|
)
|
||||||
context = {
|
|
||||||
"user": user.json(),
|
|
||||||
"wallet": wallet.json(),
|
|
||||||
"wallet_name": wallet.name,
|
|
||||||
"currencies": allowed_currencies(),
|
|
||||||
"service_fee": settings.lnbits_service_fee,
|
|
||||||
"service_fee_max": settings.lnbits_service_fee_max,
|
|
||||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
|
||||||
}
|
|
||||||
|
|
||||||
return template_renderer().TemplateResponse(
|
resp = template_renderer().TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"core/wallet.html",
|
"core/wallet.html",
|
||||||
{**context, "ajax": _is_ajax_request(request)},
|
{
|
||||||
|
"user": user.json(),
|
||||||
|
"wallet": wallet.json(),
|
||||||
|
"wallet_name": wallet.name,
|
||||||
|
"currencies": allowed_currencies(),
|
||||||
|
"service_fee": settings.lnbits_service_fee,
|
||||||
|
"service_fee_max": settings.lnbits_service_fee_max,
|
||||||
|
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
resp.set_cookie("lnbits_last_active_wallet", wallet.id)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get(
|
@generic_router.get(
|
||||||
@@ -211,26 +214,6 @@ async def account(
|
|||||||
"core/account.html",
|
"core/account.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@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),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -344,7 +327,6 @@ async def node(request: Request, user: User = Depends(check_admin)):
|
|||||||
"settings": settings.dict(),
|
"settings": settings.dict(),
|
||||||
"balance": balance,
|
"balance": balance,
|
||||||
"wallets": user.wallets[0].json(),
|
"wallets": user.wallets[0].json(),
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -383,7 +365,6 @@ async def admin_index(request: Request, user: User = Depends(check_admin)):
|
|||||||
"settings": settings.dict(),
|
"settings": settings.dict(),
|
||||||
"balance": balance,
|
"balance": balance,
|
||||||
"currencies": list(currencies.keys()),
|
"currencies": list(currencies.keys()),
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -400,7 +381,6 @@ async def users_index(request: Request, user: User = Depends(check_admin)):
|
|||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"settings": settings.dict(),
|
"settings": settings.dict(),
|
||||||
"currencies": list(currencies.keys()),
|
"currencies": list(currencies.keys()),
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -415,19 +395,6 @@ async def audit_index(request: Request, user: User = Depends(check_admin)):
|
|||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/payments", response_class=HTMLResponse)
|
|
||||||
async def payments_index(request: Request, user: User = Depends(check_user_exists)):
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
"payments/index.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user.json(),
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -444,22 +411,23 @@ async def hex_to_uuid4(hex_value: str):
|
|||||||
|
|
||||||
|
|
||||||
@generic_router.get("/lnurlwallet", response_class=RedirectResponse)
|
@generic_router.get("/lnurlwallet", response_class=RedirectResponse)
|
||||||
async def lnurlwallet(request: Request, lightning: str = ""):
|
async def lnurlwallet(request: Request):
|
||||||
"""
|
"""
|
||||||
If a user doesn't have a Lightning Network wallet and scans the LNURLw QR code with
|
If a user doesn't have a Lightning Network wallet and scans the LNURLw QR code with
|
||||||
their smartphone camera, or a QR scanner app, they can follow the link provided to
|
their smartphone camera, or a QR scanner app, they can follow the link provided to
|
||||||
claim their satoshis and get an instant LNbits wallet! lnbits/withdraw docs
|
claim their satoshis and get an instant LNbits wallet! lnbits/withdraw docs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not lightning:
|
lightning_param = request.query_params.get("lightning")
|
||||||
|
if not lightning_param:
|
||||||
return {"status": "ERROR", "reason": "lightning parameter not provided."}
|
return {"status": "ERROR", "reason": "lightning parameter not provided."}
|
||||||
if not settings.lnbits_allow_new_accounts:
|
if not settings.lnbits_allow_new_accounts:
|
||||||
return {"status": "ERROR", "reason": "New accounts are not allowed."}
|
return {"status": "ERROR", "reason": "New accounts are not allowed."}
|
||||||
|
|
||||||
lnurl = lnurl_decode(lightning)
|
lnurl = lnurl_decode(lightning_param)
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
check_callback_url(lnurl)
|
|
||||||
res1 = await client.get(lnurl, timeout=2)
|
res1 = await client.get(lnurl, timeout=2)
|
||||||
res1.raise_for_status()
|
res1.raise_for_status()
|
||||||
data1 = res1.json()
|
data1 = res1.json()
|
||||||
@@ -474,23 +442,19 @@ async def lnurlwallet(request: Request, lightning: str = ""):
|
|||||||
detail="Invalid lnurl. Expected maxWithdrawable",
|
detail="Invalid lnurl. Expected maxWithdrawable",
|
||||||
)
|
)
|
||||||
account = await create_user_account()
|
account = await create_user_account()
|
||||||
wallet = account.wallets[0]
|
wallet = await create_wallet(user_id=account.id)
|
||||||
payment = await create_invoice(
|
_, payment_request = await create_invoice(
|
||||||
wallet_id=wallet.id,
|
wallet_id=wallet.id,
|
||||||
amount=data1.get("maxWithdrawable") / 1000,
|
amount=data1.get("maxWithdrawable") / 1000,
|
||||||
memo=data1.get("defaultDescription", "lnurl wallet withdraw"),
|
memo=data1.get("defaultDescription", "lnurl wallet withdraw"),
|
||||||
)
|
)
|
||||||
url = data1.get("callback")
|
url = data1.get("callback")
|
||||||
params = {"k1": data1.get("k1"), "pr": payment.bolt11}
|
params = {"k1": data1.get("k1"), "pr": payment_request}
|
||||||
callback = url + ("&" if urlparse(url).query else "?") + urlencode(params)
|
callback = url + ("&" if urlparse(url).query else "?") + urlencode(params)
|
||||||
|
|
||||||
res2 = await client.get(callback, timeout=5)
|
res2 = await client.get(callback, timeout=2)
|
||||||
res2.raise_for_status()
|
res2.raise_for_status()
|
||||||
|
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/wallet?usr={account.id}&wal={wallet.id}",
|
f"/wallet?usr={account.id}&wal={wallet.id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _is_ajax_request(request: Request):
|
|
||||||
return request.headers.get("X-Requested-With", None) == "XMLHttpRequest"
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||||
@@ -91,18 +91,10 @@ async def api_get_info(
|
|||||||
@node_router.get("/channels")
|
@node_router.get("/channels")
|
||||||
async def api_get_channels(
|
async def api_get_channels(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
) -> Optional[list[NodeChannel]]:
|
) -> Optional[List[NodeChannel]]:
|
||||||
return await node.get_channels()
|
return await node.get_channels()
|
||||||
|
|
||||||
|
|
||||||
@node_router.get("/channels/{channel_id}")
|
|
||||||
async def api_get_channel(
|
|
||||||
channel_id: str,
|
|
||||||
node: Node = Depends(require_node),
|
|
||||||
) -> Optional[NodeChannel]:
|
|
||||||
return await node.get_channel(channel_id)
|
|
||||||
|
|
||||||
|
|
||||||
@super_node_router.post("/channels", response_model=ChannelPoint)
|
@super_node_router.post("/channels", response_model=ChannelPoint)
|
||||||
async def api_create_channel(
|
async def api_create_channel(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
@@ -121,7 +113,7 @@ async def api_delete_channel(
|
|||||||
output_index: Optional[int],
|
output_index: Optional[int],
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
) -> Optional[list[NodeChannel]]:
|
) -> Optional[List[NodeChannel]]:
|
||||||
return await node.close_channel(
|
return await node.close_channel(
|
||||||
short_id,
|
short_id,
|
||||||
(
|
(
|
||||||
@@ -133,17 +125,7 @@ async def api_delete_channel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@super_node_router.put("/channels/{channel_id}")
|
@node_router.get("/payments", response_model=Page[NodePayment])
|
||||||
async def api_set_channel_fees(
|
|
||||||
channel_id: str,
|
|
||||||
node: Node = Depends(require_node),
|
|
||||||
fee_ppm: int = Body(None),
|
|
||||||
fee_base_msat: int = Body(None),
|
|
||||||
):
|
|
||||||
await node.set_channel_fee(channel_id, fee_base_msat, fee_ppm)
|
|
||||||
|
|
||||||
|
|
||||||
@node_router.get("/payments")
|
|
||||||
async def api_get_payments(
|
async def api_get_payments(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
|
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
|
||||||
@@ -156,7 +138,7 @@ async def api_get_payments(
|
|||||||
return await node.get_payments(filters)
|
return await node.get_payments(filters)
|
||||||
|
|
||||||
|
|
||||||
@node_router.get("/invoices")
|
@node_router.get("/invoices", response_model=Page[NodeInvoice])
|
||||||
async def api_get_invoices(
|
async def api_get_invoices(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
|
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
|
||||||
@@ -169,8 +151,8 @@ async def api_get_invoices(
|
|||||||
return await node.get_invoices(filters)
|
return await node.get_invoices(filters)
|
||||||
|
|
||||||
|
|
||||||
@node_router.get("/peers")
|
@node_router.get("/peers", response_model=List[NodePeerInfo])
|
||||||
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()
|
return await node.get_peers()
|
||||||
|
|
||||||
|
|
||||||
@@ -212,10 +194,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)
|
r = await client.get(url=f"https://1ml.com/node/{node_id}/json", timeout=15)
|
||||||
try:
|
try:
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
return r.json()["noderank"]
|
||||||
if "noderank" not in data:
|
|
||||||
return None
|
|
||||||
return data["noderank"]
|
|
||||||
except httpx.HTTPStatusError as exc:
|
except httpx.HTTPStatusError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Node not found on 1ml.com"
|
status_code=HTTPStatus.NOT_FOUND, detail="Node not found on 1ml.com"
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import ssl
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -17,10 +16,6 @@ from fastapi.responses import JSONResponse
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.core.crud.payments import (
|
|
||||||
get_payment_count_stats,
|
|
||||||
get_wallets_stats,
|
|
||||||
)
|
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
CreateInvoice,
|
CreateInvoice,
|
||||||
CreateLnurl,
|
CreateLnurl,
|
||||||
@@ -28,27 +23,18 @@ from lnbits.core.models import (
|
|||||||
KeyType,
|
KeyType,
|
||||||
PayLnurlWData,
|
PayLnurlWData,
|
||||||
Payment,
|
Payment,
|
||||||
PaymentCountField,
|
|
||||||
PaymentCountStat,
|
|
||||||
PaymentDailyStats,
|
|
||||||
PaymentFilters,
|
PaymentFilters,
|
||||||
PaymentHistoryPoint,
|
PaymentHistoryPoint,
|
||||||
PaymentWalletStats,
|
Wallet,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.users import User
|
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
check_user_exists,
|
|
||||||
parse_filters,
|
parse_filters,
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import filter_dict_keys, generate_filter_params_openapi
|
||||||
check_callback_url,
|
|
||||||
filter_dict_keys,
|
|
||||||
generate_filter_params_openapi,
|
|
||||||
)
|
|
||||||
from lnbits.lnurl import decode as lnurl_decode
|
from lnbits.lnurl import decode as lnurl_decode
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||||
@@ -62,12 +48,9 @@ from ..crud import (
|
|||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
)
|
)
|
||||||
from ..services import (
|
from ..services import (
|
||||||
create_fiat_invoice,
|
create_invoice,
|
||||||
create_wallet_invoice,
|
|
||||||
fee_reserve_total,
|
fee_reserve_total,
|
||||||
get_payments_daily_stats,
|
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
update_pending_payment,
|
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,7 +62,7 @@ payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
|
|||||||
name="Payment List",
|
name="Payment List",
|
||||||
summary="get list of payments",
|
summary="get list of payments",
|
||||||
response_description="list of payments",
|
response_description="list of payments",
|
||||||
response_model=list[Payment],
|
response_model=List[Payment],
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments(
|
async def api_payments(
|
||||||
@@ -98,7 +81,7 @@ async def api_payments(
|
|||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
"/history",
|
"/history",
|
||||||
name="Get payments history",
|
name="Get payments history",
|
||||||
response_model=list[PaymentHistoryPoint],
|
response_model=List[PaymentHistoryPoint],
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments_history(
|
async def api_payments_history(
|
||||||
@@ -110,69 +93,6 @@ async def api_payments_history(
|
|||||||
return await get_payments_history(key_info.wallet.id, group, filters)
|
return await get_payments_history(key_info.wallet.id, group, filters)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
|
||||||
"/stats/count",
|
|
||||||
name="Get payments history for all users",
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
|
||||||
"/stats/wallets",
|
|
||||||
name="Get payments history for all users",
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
|
||||||
"/stats/daily",
|
|
||||||
name="Get payments history per day",
|
|
||||||
response_model=list[PaymentDailyStats],
|
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
|
||||||
)
|
|
||||||
async def api_payments_daily_stats(
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
"/paginated",
|
"/paginated",
|
||||||
name="Payment List",
|
name="Payment List",
|
||||||
@@ -185,41 +105,75 @@ async def api_payments_paginated(
|
|||||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||||
):
|
):
|
||||||
|
await update_pending_payments(key_info.wallet.id)
|
||||||
page = await get_payments_paginated(
|
page = await get_payments_paginated(
|
||||||
wallet_id=key_info.wallet.id,
|
wallet_id=key_info.wallet.id,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
)
|
)
|
||||||
for payment in page.data:
|
|
||||||
if payment.pending:
|
|
||||||
await update_pending_payment(payment)
|
|
||||||
|
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
async def _api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
||||||
"/all/paginated",
|
description_hash = b""
|
||||||
name="Payment List",
|
unhashed_description = b""
|
||||||
summary="get paginated list of payments",
|
memo = data.memo or settings.lnbits_site_title
|
||||||
response_description="list of payments",
|
if data.description_hash or data.unhashed_description:
|
||||||
response_model=Page[Payment],
|
if data.description_hash:
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
try:
|
||||||
)
|
description_hash = bytes.fromhex(data.description_hash)
|
||||||
async def api_all_payments_paginated(
|
except ValueError as exc:
|
||||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
raise HTTPException(
|
||||||
user: User = Depends(check_user_exists),
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
):
|
detail="'description_hash' must be a valid hex string",
|
||||||
if user.admin:
|
) from exc
|
||||||
# admin user can see payments from all wallets
|
if data.unhashed_description:
|
||||||
for_user_id = None
|
try:
|
||||||
else:
|
unhashed_description = bytes.fromhex(data.unhashed_description)
|
||||||
# regular user can only see payments from their wallets
|
except ValueError as exc:
|
||||||
for_user_id = user.id
|
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 = ""
|
||||||
|
|
||||||
return await get_payments_paginated(
|
payment = await create_invoice(
|
||||||
filters=filters,
|
wallet_id=wallet.id,
|
||||||
user_id=for_user_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:
|
||||||
|
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.post(
|
@payment_router.post(
|
||||||
"",
|
"",
|
||||||
@@ -243,7 +197,6 @@ async def api_payments_create(
|
|||||||
invoice_data: CreateInvoice,
|
invoice_data: CreateInvoice,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
wallet_id = wallet.wallet.id
|
|
||||||
if invoice_data.out is True and wallet.key_type == KeyType.admin:
|
if invoice_data.out is True and wallet.key_type == KeyType.admin:
|
||||||
if not invoice_data.bolt11:
|
if not invoice_data.bolt11:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -251,24 +204,21 @@ async def api_payments_create(
|
|||||||
detail="Missing BOLT11 invoice",
|
detail="Missing BOLT11 invoice",
|
||||||
)
|
)
|
||||||
payment = await pay_invoice(
|
payment = await pay_invoice(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet.wallet.id,
|
||||||
payment_request=invoice_data.bolt11,
|
payment_request=invoice_data.bolt11,
|
||||||
extra=invoice_data.extra,
|
extra=invoice_data.extra,
|
||||||
)
|
)
|
||||||
return payment
|
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(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
status_code=HTTPStatus.UNAUTHORIZED,
|
||||||
detail="Invoice (or Admin) key required.",
|
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")
|
@payment_router.get("/fee-reserve")
|
||||||
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||||
@@ -300,7 +250,6 @@ async def api_payments_pay_lnurl(
|
|||||||
amount_msat = ceil(amount_msat // 1000) * 1000
|
amount_msat = ceil(amount_msat // 1000) * 1000
|
||||||
else:
|
else:
|
||||||
amount_msat = data.amount
|
amount_msat = data.amount
|
||||||
check_callback_url(data.callback)
|
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
data.callback,
|
data.callback,
|
||||||
params={"amount": amount_msat, "comment": data.comment},
|
params={"amount": amount_msat, "comment": data.comment},
|
||||||
@@ -309,8 +258,7 @@ async def api_payments_pay_lnurl(
|
|||||||
if r.is_error:
|
if r.is_error:
|
||||||
raise httpx.ConnectError("LNURL callback connection error")
|
raise httpx.ConnectError("LNURL callback connection error")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except (httpx.HTTPError, ssl.SSLError) as exc:
|
except (httpx.ConnectError, httpx.RequestError) as exc:
|
||||||
logger.warning(exc)
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=f"Failed to connect to {domain}.",
|
detail=f"Failed to connect to {domain}.",
|
||||||
@@ -378,9 +326,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, "details": payment}
|
||||||
return {"paid": True, "preimage": payment.preimage}
|
return {"paid": True, "preimage": payment.preimage}
|
||||||
|
|
||||||
if payment.failed:
|
|
||||||
return {"paid": False, "status": "failed", "details": payment}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
status = await payment.check_status()
|
status = await payment.check_status()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -421,6 +366,7 @@ async def api_payment_pay_with_nfc(
|
|||||||
payment_request: str,
|
payment_request: str,
|
||||||
lnurl_data: PayLnurlWData,
|
lnurl_data: PayLnurlWData,
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
|
||||||
lnurl = lnurl_data.lnurl_w.lower()
|
lnurl = lnurl_data.lnurl_w.lower()
|
||||||
|
|
||||||
# Follow LUD-17 -> https://github.com/lnurl/luds/blob/luds/17.md
|
# Follow LUD-17 -> https://github.com/lnurl/luds/blob/luds/17.md
|
||||||
@@ -429,7 +375,6 @@ async def api_payment_pay_with_nfc(
|
|||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
||||||
try:
|
try:
|
||||||
check_callback_url(url)
|
|
||||||
lnurl_req = await client.get(url, timeout=10)
|
lnurl_req = await client.get(url, timeout=10)
|
||||||
if lnurl_req.is_error:
|
if lnurl_req.is_error:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import shortuuid
|
import shortuuid
|
||||||
@@ -31,11 +31,9 @@ from lnbits.core.models import (
|
|||||||
UserExtra,
|
UserExtra,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.notifications import NotificationType
|
|
||||||
from lnbits.core.models.users import Account
|
from lnbits.core.models.users import Account
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
create_user_account_no_ckeck,
|
create_user_account_no_ckeck,
|
||||||
enqueue_notification,
|
|
||||||
update_user_account,
|
update_user_account,
|
||||||
update_user_extensions,
|
update_user_extensions,
|
||||||
update_wallet_balance,
|
update_wallet_balance,
|
||||||
@@ -100,29 +98,20 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
|||||||
username=data.username,
|
username=data.username,
|
||||||
email=data.email,
|
email=data.email,
|
||||||
pubkey=data.pubkey,
|
pubkey=data.pubkey,
|
||||||
external_id=data.external_id,
|
|
||||||
extra=data.extra,
|
extra=data.extra,
|
||||||
)
|
)
|
||||||
account.validate_fields()
|
account.validate_fields()
|
||||||
account.hash_password(data.password)
|
account.hash_password(data.password)
|
||||||
user = await create_user_account_no_ckeck(account, default_exts=data.extensions)
|
user = await create_user_account_no_ckeck(account)
|
||||||
data.id = user.id
|
data.id = user.id
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@users_router.put("/user/{user_id}", name="Update user")
|
@users_router.put("/user/{user_id}", name="Update user")
|
||||||
async def api_update_user(
|
async def api_update_user(user_id: str, data: CreateUser) -> CreateUser:
|
||||||
user_id: str, data: CreateUser, user: User = Depends(check_admin)
|
|
||||||
) -> CreateUser:
|
|
||||||
if user_id != data.id:
|
if user_id != data.id:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
|
||||||
|
|
||||||
if user_id == settings.super_user and user.id != settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="Action only allowed for super user.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if data.password or data.password_repeat:
|
if data.password or data.password_repeat:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
|
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
|
||||||
@@ -133,7 +122,6 @@ async def api_update_user(
|
|||||||
username=data.username,
|
username=data.username,
|
||||||
email=data.email,
|
email=data.email,
|
||||||
pubkey=data.pubkey,
|
pubkey=data.pubkey,
|
||||||
external_id=data.external_id,
|
|
||||||
extra=data.extra or UserExtra(),
|
extra=data.extra or UserExtra(),
|
||||||
)
|
)
|
||||||
await update_user_account(account)
|
await update_user_account(account)
|
||||||
@@ -150,7 +138,7 @@ async def api_update_user(
|
|||||||
async def api_users_delete_user(
|
async def api_users_delete_user(
|
||||||
user_id: str, user: User = Depends(check_admin)
|
user_id: str, user: User = Depends(check_admin)
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
wallets = await get_wallets(user_id, deleted=False)
|
wallets = await get_wallets(user_id)
|
||||||
if len(wallets) > 0:
|
if len(wallets) > 0:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
@@ -216,7 +204,7 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
|||||||
|
|
||||||
|
|
||||||
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
@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)
|
return await get_wallets(user_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -259,50 +247,19 @@ async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleSta
|
|||||||
return SimpleStatus(success=True, message="Wallet is already active.")
|
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(
|
@users_router.delete(
|
||||||
"/user/{user_id}/wallet/{wallet}",
|
"/user/{user_id}/wallet/{wallet}",
|
||||||
name="Delete wallet by id",
|
name="Delete wallet by id",
|
||||||
summary="First time it is called it does a soft delete (only sets a flag)."
|
summary="First time it is called it does a soft delete (only sets a flag)."
|
||||||
"The second time it is called will delete the entry from the DB",
|
"The second time it is called will delete the entry from the DB",
|
||||||
)
|
)
|
||||||
async def api_users_delete_user_wallet(
|
async def api_users_delete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
|
||||||
user_id: str, wallet: str, user: User = Depends(check_admin)
|
|
||||||
) -> SimpleStatus:
|
|
||||||
wal = await get_wallet(wallet)
|
wal = await get_wallet(wallet)
|
||||||
if not wal:
|
if not wal:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
detail="Wallet does not exist.",
|
detail="Wallet does not exist.",
|
||||||
)
|
)
|
||||||
|
|
||||||
if user_id == settings.super_user and user.id != settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="Action only allowed for super user.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if wal.deleted:
|
if wal.deleted:
|
||||||
await force_delete_wallet(wallet)
|
await force_delete_wallet(wallet)
|
||||||
await delete_wallet(user_id=user_id, wallet_id=wallet)
|
await delete_wallet(user_id=user_id, wallet_id=wallet)
|
||||||
@@ -320,14 +277,4 @@ async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
|
|||||||
if not wallet:
|
if not wallet:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
|
||||||
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
|
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
|
||||||
enqueue_notification(
|
|
||||||
NotificationType.balance_update,
|
|
||||||
{
|
|
||||||
"amount": int(data.amount),
|
|
||||||
"wallet_id": wallet.id,
|
|
||||||
"wallet_name": wallet.name,
|
|
||||||
"balance": wallet.balance,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return SimpleStatus(success=True, message="Balance updated.")
|
return SimpleStatus(success=True, message="Balance updated.")
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
@@ -9,18 +8,16 @@ from fastapi import (
|
|||||||
HTTPException,
|
HTTPException,
|
||||||
)
|
)
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_wallets_paginated
|
from lnbits.core.models import (
|
||||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet
|
CreateWallet,
|
||||||
from lnbits.core.models.wallets import WalletsFilters
|
KeyType,
|
||||||
from lnbits.db import Filters, Page
|
Wallet,
|
||||||
|
)
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
check_user_exists,
|
|
||||||
parse_filters,
|
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import generate_filter_params_openapi
|
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_wallet,
|
create_wallet,
|
||||||
@@ -43,26 +40,6 @@ async def api_wallet(key_info: WalletTypeInfo = Depends(require_invoice_key)):
|
|||||||
return res
|
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}")
|
@wallet_router.put("/{new_name}")
|
||||||
async def api_update_wallet_name(
|
async def api_update_wallet_name(
|
||||||
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
@@ -79,52 +56,28 @@ async def api_update_wallet_name(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@wallet_router.put("/reset/{wallet_id}")
|
|
||||||
async def api_reset_wallet_keys(
|
|
||||||
wallet_id: str, user: User = Depends(check_user_exists)
|
|
||||||
) -> Wallet:
|
|
||||||
wallet = await get_wallet(wallet_id)
|
|
||||||
if not wallet or wallet.user != user.id:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
|
||||||
|
|
||||||
wallet.adminkey = uuid4().hex
|
|
||||||
wallet.inkey = uuid4().hex
|
|
||||||
await update_wallet(wallet)
|
|
||||||
return wallet
|
|
||||||
|
|
||||||
|
|
||||||
@wallet_router.patch("")
|
@wallet_router.patch("")
|
||||||
async def api_update_wallet(
|
async def api_update_wallet(
|
||||||
name: Optional[str] = Body(None),
|
name: Optional[str] = Body(None),
|
||||||
icon: Optional[str] = Body(None),
|
|
||||||
color: Optional[str] = Body(None),
|
|
||||||
currency: Optional[str] = Body(None),
|
currency: Optional[str] = Body(None),
|
||||||
pinned: Optional[bool] = Body(None),
|
|
||||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
key_info: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
wallet = await get_wallet(key_info.wallet.id)
|
wallet = await get_wallet(key_info.wallet.id)
|
||||||
if not wallet:
|
if not wallet:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||||
wallet.name = name or wallet.name
|
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
|
wallet.currency = currency if currency is not None else wallet.currency
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
@wallet_router.delete("/{wallet_id}")
|
@wallet_router.delete("")
|
||||||
async def api_delete_wallet(
|
async def api_delete_wallet(
|
||||||
wallet_id: str, user: User = Depends(check_user_exists)
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> None:
|
) -> None:
|
||||||
wallet = await get_wallet(wallet_id)
|
|
||||||
if not wallet or wallet.user != user.id:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
|
||||||
|
|
||||||
await delete_wallet(
|
await delete_wallet(
|
||||||
user_id=wallet.user,
|
user_id=wallet.wallet.user,
|
||||||
wallet_id=wallet.id,
|
wallet_id=wallet.wallet.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ from lnbits.core.models import (
|
|||||||
CreateWebPushSubscription,
|
CreateWebPushSubscription,
|
||||||
WebPushSubscription,
|
WebPushSubscription,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.users import User
|
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
check_user_exists,
|
WalletTypeInfo,
|
||||||
|
require_admin_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
@@ -33,20 +33,20 @@ webpush_router = APIRouter(prefix="/api/v1/webpush", tags=["Webpush"])
|
|||||||
async def api_create_webpush_subscription(
|
async def api_create_webpush_subscription(
|
||||||
request: Request,
|
request: Request,
|
||||||
data: CreateWebPushSubscription,
|
data: CreateWebPushSubscription,
|
||||||
user: User = Depends(check_user_exists),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> WebPushSubscription:
|
) -> WebPushSubscription:
|
||||||
try:
|
try:
|
||||||
subscription = json.loads(data.subscription)
|
subscription = json.loads(data.subscription)
|
||||||
endpoint = subscription["endpoint"]
|
endpoint = subscription["endpoint"]
|
||||||
host = urlparse(str(request.url)).netloc
|
host = urlparse(str(request.url)).netloc
|
||||||
|
|
||||||
subscription = await get_webpush_subscription(endpoint, user.id)
|
subscription = await get_webpush_subscription(endpoint, wallet.wallet.user)
|
||||||
if subscription:
|
if subscription:
|
||||||
return subscription
|
return subscription
|
||||||
else:
|
else:
|
||||||
return await create_webpush_subscription(
|
return await create_webpush_subscription(
|
||||||
endpoint,
|
endpoint,
|
||||||
user.id,
|
wallet.wallet.user,
|
||||||
data.subscription,
|
data.subscription,
|
||||||
host,
|
host,
|
||||||
)
|
)
|
||||||
@@ -61,13 +61,13 @@ async def api_create_webpush_subscription(
|
|||||||
@webpush_router.delete("", status_code=HTTPStatus.OK)
|
@webpush_router.delete("", status_code=HTTPStatus.OK)
|
||||||
async def api_delete_webpush_subscription(
|
async def api_delete_webpush_subscription(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: User = Depends(check_user_exists),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
endpoint = unquote(
|
endpoint = unquote(
|
||||||
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
|
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
|
||||||
)
|
)
|
||||||
count = await delete_webpush_subscription(endpoint, user.id)
|
count = await delete_webpush_subscription(endpoint, wallet.wallet.user)
|
||||||
return {"count": count}
|
return {"count": count}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(exc)
|
logger.debug(exc)
|
||||||
|
|||||||
+46
-77
@@ -8,7 +8,7 @@ import time
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Generic, Literal, TypeVar, get_origin
|
from typing import Any, Generic, Literal, Optional, TypeVar, Union, get_origin
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, ValidationError, root_validator
|
from pydantic import BaseModel, ValidationError, root_validator
|
||||||
@@ -22,13 +22,6 @@ POSTGRES = "POSTGRES"
|
|||||||
COCKROACH = "COCKROACH"
|
COCKROACH = "COCKROACH"
|
||||||
SQLITE = "SQLITE"
|
SQLITE = "SQLITE"
|
||||||
|
|
||||||
DateTrunc = Literal["hour", "day", "month"]
|
|
||||||
sqlite_formats = {
|
|
||||||
"hour": "%Y-%m-%d %H:00:00",
|
|
||||||
"day": "%Y-%m-%d 00:00:00",
|
|
||||||
"month": "%Y-%m-01 00:00:00",
|
|
||||||
}
|
|
||||||
|
|
||||||
if settings.lnbits_database_url:
|
if settings.lnbits_database_url:
|
||||||
database_uri = settings.lnbits_database_url
|
database_uri = settings.lnbits_database_url
|
||||||
if database_uri.startswith("cockroachdb://"):
|
if database_uri.startswith("cockroachdb://"):
|
||||||
@@ -65,8 +58,8 @@ def get_placeholder(model: Any, field: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class Compat:
|
class Compat:
|
||||||
type: str | None = "<inherited>"
|
type: Optional[str] = "<inherited>"
|
||||||
schema: str | None = "<inherited>"
|
schema: Optional[str] = "<inherited>"
|
||||||
|
|
||||||
def interval_seconds(self, seconds: int) -> str:
|
def interval_seconds(self, seconds: int) -> str:
|
||||||
if self.type in {POSTGRES, COCKROACH}:
|
if self.type in {POSTGRES, COCKROACH}:
|
||||||
@@ -82,18 +75,6 @@ class Compat:
|
|||||||
return time.mktime(date.timetuple())
|
return time.mktime(date.timetuple())
|
||||||
return "<nothing>"
|
return "<nothing>"
|
||||||
|
|
||||||
def datetime_grouping(self, group: DateTrunc):
|
|
||||||
if self.type in {POSTGRES, COCKROACH}:
|
|
||||||
return f"date_trunc('{group}', time)"
|
|
||||||
elif self.type == SQLITE:
|
|
||||||
return (
|
|
||||||
"CAST (strftime('%s',datetime(strftime("
|
|
||||||
f"'{sqlite_formats[group]}'"
|
|
||||||
", time, 'unixepoch'))) AS INT)"
|
|
||||||
)
|
|
||||||
|
|
||||||
return "<bad grouping>"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timestamp_now(self) -> str:
|
def timestamp_now(self) -> str:
|
||||||
if self.type in {POSTGRES, COCKROACH}:
|
if self.type in {POSTGRES, COCKROACH}:
|
||||||
@@ -167,8 +148,8 @@ class Connection(Compat):
|
|||||||
async def fetchall(
|
async def fetchall(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: dict | None = None,
|
values: Optional[dict] = None,
|
||||||
model: type[TModel] | None = None,
|
model: Optional[type[TModel]] = None,
|
||||||
) -> list[TModel]:
|
) -> list[TModel]:
|
||||||
params = self.rewrite_values(values) if values else {}
|
params = self.rewrite_values(values) if values else {}
|
||||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||||
@@ -183,8 +164,8 @@ class Connection(Compat):
|
|||||||
async def fetchone(
|
async def fetchone(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: dict | None = None,
|
values: Optional[dict] = None,
|
||||||
model: type[TModel] | None = None,
|
model: Optional[type[TModel]] = None,
|
||||||
) -> TModel:
|
) -> TModel:
|
||||||
params = self.rewrite_values(values) if values else {}
|
params = self.rewrite_values(values) if values else {}
|
||||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||||
@@ -211,11 +192,11 @@ class Connection(Compat):
|
|||||||
async def fetch_page(
|
async def fetch_page(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
where: list[str] | None = None,
|
where: Optional[list[str]] = None,
|
||||||
values: dict | None = None,
|
values: Optional[dict] = None,
|
||||||
filters: Filters | None = None,
|
filters: Optional[Filters] = None,
|
||||||
model: type[TModel] | None = None,
|
model: Optional[type[TModel]] = None,
|
||||||
group_by: list[str] | None = None,
|
group_by: Optional[list[str]] = None,
|
||||||
) -> Page[TModel]:
|
) -> Page[TModel]:
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
@@ -268,7 +249,7 @@ class Connection(Compat):
|
|||||||
total=count,
|
total=count,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def execute(self, query: str, values: dict | None = None):
|
async def execute(self, query: str, values: Optional[dict] = None):
|
||||||
params = self.rewrite_values(values) if values else {}
|
params = self.rewrite_values(values) if values else {}
|
||||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||||
await self.conn.commit()
|
await self.conn.commit()
|
||||||
@@ -350,8 +331,8 @@ class Database(Compat):
|
|||||||
async def fetchall(
|
async def fetchall(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: dict | None = None,
|
values: Optional[dict] = None,
|
||||||
model: type[TModel] | None = None,
|
model: Optional[type[TModel]] = None,
|
||||||
) -> list[TModel]:
|
) -> list[TModel]:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetchall(query, values, model)
|
return await conn.fetchall(query, values, model)
|
||||||
@@ -359,8 +340,8 @@ class Database(Compat):
|
|||||||
async def fetchone(
|
async def fetchone(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: dict | None = None,
|
values: Optional[dict] = None,
|
||||||
model: type[TModel] | None = None,
|
model: Optional[type[TModel]] = None,
|
||||||
) -> TModel:
|
) -> TModel:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetchone(query, values, model)
|
return await conn.fetchone(query, values, model)
|
||||||
@@ -378,16 +359,16 @@ class Database(Compat):
|
|||||||
async def fetch_page(
|
async def fetch_page(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
where: list[str] | None = None,
|
where: Optional[list[str]] = None,
|
||||||
values: dict | None = None,
|
values: Optional[dict] = None,
|
||||||
filters: Filters | None = None,
|
filters: Optional[Filters] = None,
|
||||||
model: type[TModel] | None = None,
|
model: Optional[type[TModel]] = None,
|
||||||
group_by: list[str] | None = None,
|
group_by: Optional[list[str]] = None,
|
||||||
) -> Page[TModel]:
|
) -> Page[TModel]:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
||||||
|
|
||||||
async def execute(self, query: str, values: dict | None = None):
|
async def execute(self, query: str, values: Optional[dict] = None):
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.execute(query, values)
|
return await conn.execute(query, values)
|
||||||
|
|
||||||
@@ -445,7 +426,7 @@ class Operator(Enum):
|
|||||||
|
|
||||||
class FilterModel(BaseModel):
|
class FilterModel(BaseModel):
|
||||||
__search_fields__: list[str] = []
|
__search_fields__: list[str] = []
|
||||||
__sort_fields__: list[str] | None = None
|
__sort_fields__: Optional[list[str]] = None
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
@@ -461,8 +442,8 @@ class Page(BaseModel, Generic[T]):
|
|||||||
class Filter(BaseModel, Generic[TFilterModel]):
|
class Filter(BaseModel, Generic[TFilterModel]):
|
||||||
field: str
|
field: str
|
||||||
op: Operator = Operator.EQ
|
op: Operator = Operator.EQ
|
||||||
model: type[TFilterModel] | None
|
model: Optional[type[TFilterModel]]
|
||||||
values: dict | None = None
|
values: Optional[dict] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_query(
|
def parse_query(
|
||||||
@@ -517,15 +498,15 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
filters: list[Filter[TFilterModel]] = []
|
filters: list[Filter[TFilterModel]] = []
|
||||||
search: str | None = None
|
search: Optional[str] = None
|
||||||
|
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
limit: int | None = None
|
limit: Optional[int] = None
|
||||||
|
|
||||||
sortby: str | None = None
|
sortby: Optional[str] = None
|
||||||
direction: Literal["asc", "desc"] | None = None
|
direction: Optional[Literal["asc", "desc"]] = None
|
||||||
|
|
||||||
model: type[TFilterModel] | None = None
|
model: Optional[type[TFilterModel]] = None
|
||||||
|
|
||||||
@root_validator(pre=True)
|
@root_validator(pre=True)
|
||||||
def validate_sortby(cls, values):
|
def validate_sortby(cls, values):
|
||||||
@@ -547,21 +528,18 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
stmt += f"OFFSET {self.offset}"
|
stmt += f"OFFSET {self.offset}"
|
||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
def where(self, where_stmts: list[str] | None = None) -> str:
|
def where(self, where_stmts: Optional[list[str]] = None) -> str:
|
||||||
if not where_stmts:
|
if not where_stmts:
|
||||||
where_stmts = []
|
where_stmts = []
|
||||||
if self.filters:
|
if self.filters:
|
||||||
for page_filter in self.filters:
|
for page_filter in self.filters:
|
||||||
where_stmts.append(page_filter.statement)
|
where_stmts.append(page_filter.statement)
|
||||||
if self.search and self.model and self.model.__search_fields__:
|
if self.search and self.model:
|
||||||
# Use `COALESCE` to handle `NULL` values and `||`
|
fields = self.model.__search_fields__
|
||||||
# for cross-database compatible string concatenation
|
if DB_TYPE == POSTGRES:
|
||||||
_fields = self.model.__search_fields__
|
where_stmts.append(f"lower(concat({', '.join(fields)})) LIKE :search")
|
||||||
search_expr = " || ".join(
|
elif DB_TYPE == SQLITE:
|
||||||
f"COALESCE(CAST({field} AS TEXT), '')" for field in _fields
|
where_stmts.append(f"lower({'||'.join(fields)}) LIKE :search")
|
||||||
)
|
|
||||||
where_stmts.append(f"lower({search_expr}) LIKE :search")
|
|
||||||
|
|
||||||
if where_stmts:
|
if where_stmts:
|
||||||
return "WHERE " + " AND ".join(where_stmts)
|
return "WHERE " + " AND ".join(where_stmts)
|
||||||
return ""
|
return ""
|
||||||
@@ -571,7 +549,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def values(self, values: dict | None = None) -> dict:
|
def values(self, values: Optional[dict] = None) -> dict:
|
||||||
if not values:
|
if not values:
|
||||||
values = {}
|
values = {}
|
||||||
if self.filters:
|
if self.filters:
|
||||||
@@ -580,7 +558,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
for key, value in page_filter.values.items():
|
for key, value in page_filter.values.items():
|
||||||
values[key] = value
|
values[key] = value
|
||||||
if self.search and self.model:
|
if self.search and self.model:
|
||||||
values["search"] = f"%{self.search.lower()}%"
|
values["search"] = f"%{self.search}%"
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
@@ -645,12 +623,12 @@ def model_to_dict(model: BaseModel) -> dict:
|
|||||||
return _dict
|
return _dict
|
||||||
|
|
||||||
|
|
||||||
def dict_to_submodel(model: type[TModel], value: dict | str) -> TModel | None:
|
def dict_to_submodel(model: type[TModel], value: Union[dict, str]) -> Optional[TModel]:
|
||||||
"""convert a dictionary or JSON string to a Pydantic model"""
|
"""convert a dictionary or JSON string to a Pydantic model"""
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
if value == "null" or value == "":
|
if value == "null":
|
||||||
return None
|
return None
|
||||||
_subdict = _safe_load_json(value)
|
_subdict = json.loads(value)
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, dict):
|
||||||
_subdict = value
|
_subdict = value
|
||||||
|
|
||||||
@@ -674,7 +652,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
|||||||
type_ = model.__fields__[key].type_
|
type_ = model.__fields__[key].type_
|
||||||
outertype_ = model.__fields__[key].outer_type_
|
outertype_ = model.__fields__[key].outer_type_
|
||||||
if get_origin(outertype_) is list:
|
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[key] = [
|
||||||
dict_to_submodel(type_, v) if issubclass(type_, BaseModel) else v
|
dict_to_submodel(type_, v) if issubclass(type_, BaseModel) else v
|
||||||
for v in _items
|
for v in _items
|
||||||
@@ -695,7 +673,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
|||||||
# TODO: remove this when all sub models are migrated to Pydantic
|
# TODO: remove this when all sub models are migrated to Pydantic
|
||||||
# NOTE: this is for type dict on BaseModel, (used in Payment class)
|
# NOTE: this is for type dict on BaseModel, (used in Payment class)
|
||||||
if type_ is dict and value:
|
if type_ is dict and value:
|
||||||
_dict[key] = _safe_load_json(value)
|
_dict[key] = json.loads(value)
|
||||||
continue
|
continue
|
||||||
_dict[key] = value
|
_dict[key] = value
|
||||||
continue
|
continue
|
||||||
@@ -703,12 +681,3 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
|||||||
if isinstance(_model, BaseModel):
|
if isinstance(_model, BaseModel):
|
||||||
_model.__init__(**_dict) # type: ignore
|
_model.__init__(**_dict) # type: ignore
|
||||||
return _model
|
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 {}
|
|
||||||
|
|||||||
+33
-96
@@ -1,11 +1,11 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Annotated, Literal, Optional, Union
|
from typing import Annotated, Literal, Optional, Type, Union
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from fastapi import Cookie, Depends, Query, Request, Security
|
from fastapi import Cookie, Depends, Query, Request, Security
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.openapi.models import APIKey, APIKeyIn, SecuritySchemeType
|
from fastapi.openapi.models import APIKey, APIKeyIn, SecuritySchemeType
|
||||||
from fastapi.security import APIKeyHeader, APIKeyQuery, HTTPBearer, OAuth2PasswordBearer
|
from fastapi.security import APIKeyHeader, APIKeyQuery, OAuth2PasswordBearer
|
||||||
from fastapi.security.base import SecurityBase
|
from fastapi.security.base import SecurityBase
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic.types import UUID4
|
from pydantic.types import UUID4
|
||||||
@@ -18,7 +18,6 @@ from lnbits.core.crud import (
|
|||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import get_user_access_control_lists
|
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccessTokenPayload,
|
AccessTokenPayload,
|
||||||
Account,
|
Account,
|
||||||
@@ -28,18 +27,10 @@ from lnbits.core.models import (
|
|||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
)
|
)
|
||||||
from lnbits.db import Connection, Filter, Filters, TFilterModel
|
from lnbits.db import Connection, Filter, Filters, TFilterModel
|
||||||
from lnbits.helpers import normalize_path, path_segments
|
|
||||||
from lnbits.settings import AuthMethods, settings
|
from lnbits.settings import AuthMethods, settings
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth", auto_error=False)
|
||||||
tokenUrl="api/v1/auth",
|
|
||||||
auto_error=False,
|
|
||||||
description="OAuth2 access token for authentication with username and password.",
|
|
||||||
)
|
|
||||||
http_bearer = HTTPBearer(
|
|
||||||
auto_error=False,
|
|
||||||
description="Bearer Token for custom ACL based access control",
|
|
||||||
)
|
|
||||||
api_key_header = APIKeyHeader(
|
api_key_header = APIKeyHeader(
|
||||||
name="X-API-KEY",
|
name="X-API-KEY",
|
||||||
auto_error=False,
|
auto_error=False,
|
||||||
@@ -102,7 +93,7 @@ class KeyChecker(SecurityBase):
|
|||||||
request.scope["user_id"] = wallet.user
|
request.scope["user_id"] = wallet.user
|
||||||
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
status_code=HTTPStatus.UNAUTHORIZED,
|
||||||
detail="Invalid adminkey.",
|
detail="Invalid adminkey.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -139,9 +130,8 @@ async def require_invoice_key(
|
|||||||
async def check_access_token(
|
async def check_access_token(
|
||||||
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
|
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
|
||||||
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
|
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
|
||||||
bearer_access_token: Annotated[Union[str, None], Depends(http_bearer)] = None,
|
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
return header_access_token or cookie_access_token or bearer_access_token
|
return header_access_token or cookie_access_token
|
||||||
|
|
||||||
|
|
||||||
async def check_user_exists(
|
async def check_user_exists(
|
||||||
@@ -150,13 +140,9 @@ async def check_user_exists(
|
|||||||
usr: Optional[UUID4] = None,
|
usr: Optional[UUID4] = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
if access_token:
|
if access_token:
|
||||||
account = await _get_account_from_token(access_token, r["path"], r["method"])
|
account = await _get_account_from_token(access_token)
|
||||||
elif usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
elif usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||||
account = await get_account(usr.hex)
|
account = await get_account(usr.hex)
|
||||||
if account and account.is_admin:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.FORBIDDEN, "User id only access for admins is forbidden."
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing user ID or access token.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing user ID or access token.")
|
||||||
|
|
||||||
@@ -165,7 +151,7 @@ async def check_user_exists(
|
|||||||
|
|
||||||
r.scope["user_id"] = account.id
|
r.scope["user_id"] = account.id
|
||||||
if not settings.is_user_allowed(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)
|
user = await get_user_from_account(account)
|
||||||
if not user:
|
if not user:
|
||||||
@@ -175,14 +161,13 @@ async def check_user_exists(
|
|||||||
|
|
||||||
|
|
||||||
async def optional_user_id(
|
async def optional_user_id(
|
||||||
r: Request,
|
|
||||||
access_token: Annotated[Optional[str], Depends(check_access_token)],
|
access_token: Annotated[Optional[str], Depends(check_access_token)],
|
||||||
usr: Optional[UUID4] = None,
|
usr: Optional[UUID4] = None,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||||
return usr.hex
|
return usr.hex
|
||||||
if access_token:
|
if access_token:
|
||||||
account = await _get_account_from_token(access_token, r["path"], r["method"])
|
account = await _get_account_from_token(access_token)
|
||||||
return account.id if account else None
|
return account.id if account else None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -201,11 +186,7 @@ async def access_token_payload(
|
|||||||
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
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:
|
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
|
||||||
raise HTTPException(
|
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(
|
|
||||||
HTTPStatus.FORBIDDEN, "Admin users must have credentials configured."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
@@ -214,16 +195,12 @@ 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:
|
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||||
if user.id != settings.super_user:
|
if user.id != settings.super_user:
|
||||||
raise HTTPException(
|
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(
|
|
||||||
HTTPStatus.FORBIDDEN, "Super user must have credentials configured."
|
|
||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def parse_filters(model: type[TFilterModel]):
|
def parse_filters(model: Type[TFilterModel]):
|
||||||
"""
|
"""
|
||||||
Parses the query params as filters.
|
Parses the query params as filters.
|
||||||
:param model: model used for validation of filter values
|
:param model: model used for validation of filter values
|
||||||
@@ -270,18 +247,19 @@ async def check_user_extension_access(
|
|||||||
success=False, message=f"User not authorized for extension '{ext_id}'."
|
success=False, message=f"User not authorized for extension '{ext_id}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.is_installed_extension_id(ext_id):
|
if settings.is_extension_id(ext_id):
|
||||||
ext_ids = await get_user_active_extensions_ids(user_id, conn=conn)
|
ext_ids = await get_user_active_extensions_ids(user_id, conn=conn)
|
||||||
if ext_id not in ext_ids:
|
if ext_id not in ext_ids:
|
||||||
return SimpleStatus(
|
return SimpleStatus(
|
||||||
success=False, message=f"Extension '{ext_id}' not enabled."
|
success=False, message=f"User extension '{ext_id}' not enabled."
|
||||||
)
|
)
|
||||||
|
|
||||||
return SimpleStatus(success=True, message="OK")
|
return SimpleStatus(success=True, message="OK")
|
||||||
|
|
||||||
|
|
||||||
async def _check_user_extension_access(user_id: str, path: str):
|
async def _check_user_extension_access(user_id: str, current_path: str):
|
||||||
ext_id = path_segments(path)[0]
|
path = current_path.split("/")
|
||||||
|
ext_id = path[3] if path[1] == "upgrades" else path[1]
|
||||||
status = await check_user_extension_access(user_id, ext_id)
|
status = await check_user_extension_access(user_id, ext_id)
|
||||||
if not status.success:
|
if not status.success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -290,14 +268,16 @@ async def _check_user_extension_access(user_id: str, path: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _get_account_from_token(
|
async def _get_account_from_token(access_token) -> Optional[Account]:
|
||||||
access_token: str, path: str, method: str
|
|
||||||
) -> Optional[Account]:
|
|
||||||
try:
|
try:
|
||||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
||||||
return await _get_account_from_jwt_payload(
|
user = await _get_user_from_jwt_payload(payload)
|
||||||
AccessTokenPayload(**payload), path, method
|
if not user:
|
||||||
)
|
raise HTTPException(
|
||||||
|
HTTPStatus.UNAUTHORIZED, "Data missing for access token."
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
except jwt.ExpiredSignatureError as exc:
|
except jwt.ExpiredSignatureError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -308,54 +288,11 @@ async def _get_account_from_token(
|
|||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid access token.") from exc
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid access token.") from exc
|
||||||
|
|
||||||
|
|
||||||
async def _get_account_from_jwt_payload(
|
async def _get_user_from_jwt_payload(payload) -> Optional[Account]:
|
||||||
payload: AccessTokenPayload, path: str, method: str
|
if "sub" in payload and payload.get("sub"):
|
||||||
) -> Optional[Account]:
|
return await get_account_by_username(str(payload.get("sub")))
|
||||||
account = None
|
if "usr" in payload and payload.get("usr"):
|
||||||
if payload.sub:
|
return await get_account(str(payload.get("usr")))
|
||||||
account = await get_account_by_username(payload.sub)
|
if "email" in payload and payload.get("email"):
|
||||||
elif payload.usr:
|
return await get_account_by_email(str(payload.get("email")))
|
||||||
account = await get_account(payload.usr)
|
return None
|
||||||
elif payload.email:
|
|
||||||
account = await get_account_by_email(payload.email)
|
|
||||||
|
|
||||||
if not account:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if payload.api_token_id:
|
|
||||||
await _check_account_api_access(account.id, payload.api_token_id, path, method)
|
|
||||||
|
|
||||||
return account
|
|
||||||
|
|
||||||
|
|
||||||
async def _check_account_api_access(
|
|
||||||
user_id: str, token_id: str, path: str, method: str
|
|
||||||
):
|
|
||||||
segments = path.split("/")
|
|
||||||
if len(segments) < 3:
|
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Not an API endpoint.")
|
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user_id)
|
|
||||||
acl = acls.get_acl_by_token_id(token_id)
|
|
||||||
if not acl:
|
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Invalid token id.")
|
|
||||||
|
|
||||||
path = "/" + "/".join(path_segments(path)[:3])
|
|
||||||
endpoint = acl.get_endpoint(path)
|
|
||||||
if not endpoint:
|
|
||||||
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]
|
|
||||||
|
|||||||
+5
-62
@@ -7,11 +7,8 @@ from fastapi import FastAPI, HTTPException, Request
|
|||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi.exceptions import RequestValidationError
|
||||||
from fastapi.responses import JSONResponse, RedirectResponse, Response
|
from fastapi.responses import JSONResponse, RedirectResponse, Response
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from shortuuid import uuid
|
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from .helpers import template_renderer
|
||||||
|
|
||||||
from .helpers import path_segments, template_renderer
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentError(Exception):
|
class PaymentError(Exception):
|
||||||
@@ -26,18 +23,12 @@ class InvoiceError(Exception):
|
|||||||
self.status = status
|
self.status = status
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
||||||
# Only the browser sends "text/html" request
|
# Only the browser sends "text/html" request
|
||||||
# not fail proof, but everything else get's a JSON response
|
# not fail proof, but everything else get's a JSON response
|
||||||
|
|
||||||
if not request.headers:
|
if not request.headers:
|
||||||
return None
|
return None
|
||||||
|
if "text/html" not in request.headers.get("accept", ""):
|
||||||
if not _is_browser_request(request):
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -58,14 +49,7 @@ def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
request,
|
request, "error.html", {"err": f"Error: {exc!s}"}, status_code
|
||||||
"error.html",
|
|
||||||
{
|
|
||||||
"err": f"Error: {exc!s}",
|
|
||||||
"status_code": int(status_code),
|
|
||||||
"message": str(exc).split(":")[-1].strip(),
|
|
||||||
},
|
|
||||||
status_code,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -76,11 +60,10 @@ def register_exception_handlers(app: FastAPI):
|
|||||||
async def exception_handler(request: Request, exc: Exception):
|
async def exception_handler(request: Request, exc: Exception):
|
||||||
etype, _, tb = sys.exc_info()
|
etype, _, tb = sys.exc_info()
|
||||||
traceback.print_exception(etype, exc, tb)
|
traceback.print_exception(etype, exc, tb)
|
||||||
exception_id = uuid()
|
logger.error(f"Exception: {exc!s}")
|
||||||
logger.error(f"Exception ID: {exception_id}\n{exc!s}")
|
|
||||||
return render_html_error(request, exc) or JSONResponse(
|
return render_html_error(request, exc) or JSONResponse(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
content={"detail": f"Unexpected error! ID: {exception_id}"},
|
content={"detail": str(exc)},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.exception_handler(AssertionError)
|
@app.exception_handler(AssertionError)
|
||||||
@@ -136,43 +119,3 @@ def register_exception_handlers(app: FastAPI):
|
|||||||
status_code=520,
|
status_code=520,
|
||||||
content={"detail": exc.message, "status": exc.status},
|
content={"detail": exc.message, "status": exc.status},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.exception_handler(404)
|
|
||||||
async def error_handler_404(request: Request, exc: HTTPException):
|
|
||||||
logger.error(f"404: {request.url.path} {exc.status_code}: {exc.detail}")
|
|
||||||
|
|
||||||
if not _is_browser_request(request):
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=exc.status_code,
|
|
||||||
content={"detail": exc.detail},
|
|
||||||
)
|
|
||||||
|
|
||||||
path = path_segments(request.url.path)[0]
|
|
||||||
status_code = HTTPStatus.NOT_FOUND
|
|
||||||
message: str = "Page not found."
|
|
||||||
|
|
||||||
if settings.is_ready_to_install_extension_id(path):
|
|
||||||
status_code = HTTPStatus.FORBIDDEN
|
|
||||||
message = f"Extension '{path}' not installed. Ask the admin to install it."
|
|
||||||
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
"error.html",
|
|
||||||
{"status_code": int(status_code), "message": message},
|
|
||||||
int(status_code),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_browser_request(request: Request) -> bool:
|
|
||||||
# Check a few common browser agents, also not fail proof
|
|
||||||
if "api/v1" in request.url.path:
|
|
||||||
return False
|
|
||||||
|
|
||||||
browser_agents = ["Mozilla", "Chrome", "Safari"]
|
|
||||||
if any(agent in request.headers.get("user-agent", "") for agent in browser_agents):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if "text/html" in request.headers.get("accept", ""):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|||||||
@@ -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",
|
|
||||||
]
|
|
||||||
@@ -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 ""
|
|
||||||
@@ -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)]
|
|
||||||
)
|
|
||||||
+54
-157
@@ -3,20 +3,18 @@ import json
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional, Type
|
||||||
from urllib import request
|
from urllib import request
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
import jwt
|
import jwt
|
||||||
import shortuuid
|
import shortuuid
|
||||||
from fastapi.routing import APIRoute
|
|
||||||
from loguru import logger
|
|
||||||
from packaging import version
|
from packaging import version
|
||||||
from pydantic.schema import field_schema
|
from pydantic.schema import field_schema
|
||||||
|
|
||||||
from lnbits.jinja2_templating import Jinja2Templates
|
from lnbits.jinja2_templating import Jinja2Templates
|
||||||
from lnbits.nodes import get_node_class
|
from lnbits.nodes import get_node_class
|
||||||
|
from lnbits.requestvars import g
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import AESCipher
|
||||||
|
|
||||||
@@ -41,7 +39,7 @@ def urlsafe_short_hash() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> 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 = "?"
|
url_params = "?"
|
||||||
for key, value in params.items():
|
for key, value in params.items():
|
||||||
url_params += f"{key}={value}&"
|
url_params += f"{key}={value}&"
|
||||||
@@ -64,52 +62,43 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
|||||||
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
||||||
t.env.globals["static_url_for"] = static_url_for
|
t.env.globals["static_url_for"] = static_url_for
|
||||||
|
|
||||||
window_settings = {
|
if settings.lnbits_ad_space_enabled:
|
||||||
"AD_SPACE": settings.lnbits_ad_space.split(","),
|
t.env.globals["AD_SPACE"] = settings.lnbits_ad_space.split(",")
|
||||||
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
t.env.globals["AD_SPACE_TITLE"] = settings.lnbits_ad_space_title
|
||||||
"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"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
t.env.globals["VOIDWALLET"] = settings.lnbits_backend_wallet_class == "VoidWallet"
|
||||||
for key, value in window_settings.items():
|
t.env.globals["HIDE_API"] = settings.lnbits_hide_api
|
||||||
t.env.globals[key] = value
|
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
|
||||||
|
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
|
||||||
|
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_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_all_extensions_ids)
|
||||||
|
|
||||||
|
if settings.lnbits_custom_logo:
|
||||||
|
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
|
||||||
|
|
||||||
if settings.bundle_assets:
|
if settings.bundle_assets:
|
||||||
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
||||||
@@ -123,6 +112,8 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
|||||||
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
||||||
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
||||||
|
|
||||||
|
t.env.globals["WEBPUSH_PUBKEY"] = settings.lnbits_webpush_pubkey
|
||||||
|
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
@@ -153,7 +144,7 @@ def get_current_extension_name() -> str:
|
|||||||
return ext_name
|
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
|
Generate openapi documentation for Filters. This is intended to be used along
|
||||||
parse_filters (see example)
|
parse_filters (see example)
|
||||||
@@ -197,14 +188,6 @@ def is_valid_username(username: str) -> bool:
|
|||||||
return re.fullmatch(username_regex, username) is not None
|
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:
|
def is_valid_pubkey(pubkey: str) -> bool:
|
||||||
if len(pubkey) != 64:
|
if len(pubkey) != 64:
|
||||||
return False
|
return False
|
||||||
@@ -215,42 +198,27 @@ def is_valid_pubkey(pubkey: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def create_access_token(data: dict, token_expire_minutes: Optional[int] = None) -> str:
|
def create_access_token(data: dict):
|
||||||
minutes = token_expire_minutes or settings.auth_token_expire_minutes
|
expire = datetime.now(timezone.utc) + timedelta(
|
||||||
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
|
minutes=settings.auth_token_expire_minutes
|
||||||
to_encode = {k: v for k, v in data.items() if v is not None}
|
)
|
||||||
to_encode.update({"exp": expire}) # todo:check expiration
|
to_encode = data.copy()
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
||||||
|
|
||||||
|
|
||||||
def encrypt_internal_message(
|
def encrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
|
||||||
m: Optional[str] = None, urlsafe: bool = False
|
"""Encrypt message with the internal secret key"""
|
||||||
) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Encrypt message with the internal secret key
|
|
||||||
|
|
||||||
Args:
|
|
||||||
m: Message to encrypt
|
|
||||||
urlsafe: Whether to use URL-safe base64 encoding
|
|
||||||
"""
|
|
||||||
if not m:
|
if not m:
|
||||||
return None
|
return None
|
||||||
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode(), urlsafe=urlsafe)
|
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode())
|
||||||
|
|
||||||
|
|
||||||
def decrypt_internal_message(
|
def decrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
|
||||||
m: Optional[str] = None, urlsafe: bool = False
|
"""Decrypt message with the internal secret key"""
|
||||||
) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Decrypt message with the internal secret key
|
|
||||||
|
|
||||||
Args:
|
|
||||||
m: Message to decrypt
|
|
||||||
urlsafe: Whether the message uses URL-safe base64 encoding
|
|
||||||
"""
|
|
||||||
if not m:
|
if not m:
|
||||||
return None
|
return None
|
||||||
return AESCipher(key=settings.auth_secret_key).decrypt(m, urlsafe=urlsafe)
|
return AESCipher(key=settings.auth_secret_key).decrypt(m)
|
||||||
|
|
||||||
|
|
||||||
def filter_dict_keys(data: dict, filter_keys: Optional[list[str]]) -> dict:
|
def filter_dict_keys(data: dict, filter_keys: Optional[list[str]]) -> dict:
|
||||||
@@ -288,24 +256,6 @@ def is_lnbits_version_ok(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def check_callback_url(url: str):
|
|
||||||
if not settings.lnbits_callback_url_rules:
|
|
||||||
# no rules, all urls are allowed
|
|
||||||
return
|
|
||||||
u = urlparse(url)
|
|
||||||
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."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def download_url(url, save_path):
|
def download_url(url, save_path):
|
||||||
with request.urlopen(url, timeout=60) as dl_file:
|
with request.urlopen(url, timeout=60) as dl_file:
|
||||||
with open(save_path, "wb") as out_file:
|
with open(save_path, "wb") as out_file:
|
||||||
@@ -320,56 +270,3 @@ def file_hash(filename):
|
|||||||
while n := f.readinto(mv):
|
while n := f.readinto(mv):
|
||||||
h.update(mv[:n])
|
h.update(mv[:n])
|
||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def get_api_routes(routes: list) -> dict[str, str]:
|
|
||||||
data = {}
|
|
||||||
for route in routes:
|
|
||||||
if not isinstance(route, APIRoute):
|
|
||||||
continue
|
|
||||||
segments = route.path.split("/")
|
|
||||||
if len(segments) < 3:
|
|
||||||
continue
|
|
||||||
if "/".join(segments[1:3]) == "api/v1":
|
|
||||||
data["/".join(segments[0:4])] = segments[3].capitalize()
|
|
||||||
elif "/".join(segments[2:4]) == "api/v1":
|
|
||||||
data["/".join(segments[0:4])] = segments[1].capitalize()
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def path_segments(path: str) -> list[str]:
|
|
||||||
path = path.strip("/")
|
|
||||||
segments = path.split("/")
|
|
||||||
if len(segments) < 2:
|
|
||||||
return segments
|
|
||||||
if segments[0] == "upgrades":
|
|
||||||
return segments[2:]
|
|
||||||
return segments[0:]
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_path(path: Optional[str]) -> str:
|
|
||||||
path = path or ""
|
|
||||||
return "/" + "/".join(path_segments(path))
|
|
||||||
|
|
||||||
|
|
||||||
def safe_upload_file_path(filename: str, directory: str = "images") -> Path:
|
|
||||||
image_folder = Path(settings.lnbits_data_folder, directory)
|
|
||||||
file_path = image_folder / filename
|
|
||||||
# Prevent dir traversal attack
|
|
||||||
if image_folder.resolve() not in file_path.resolve().parents:
|
|
||||||
raise ValueError("Unsafe filename.")
|
|
||||||
# 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
@@ -58,9 +58,9 @@ class LnurlErrorResponseHandler(APIRoute):
|
|||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"LnurlErrorResponse",
|
|
||||||
"LnurlErrorResponseHandler",
|
|
||||||
"decode",
|
"decode",
|
||||||
"encode",
|
"encode",
|
||||||
"handle",
|
"handle",
|
||||||
|
"LnurlErrorResponse",
|
||||||
|
"LnurlErrorResponseHandler",
|
||||||
]
|
]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user