Compare commits

..
Author SHA1 Message Date
arcbtc 86a1313258 make cards highlight with neon 2024-12-20 02:02:57 +00:00
arcbtc 0fa8fca70e Merge remote-tracking branch 'origin/backgroundimage' into all_ui_elements 2024-12-20 01:56:18 +00:00
arcbtc b33518f075 Merge remote-tracking branch 'origin/calls_to_action' into all_ui_elements 2024-12-20 01:55:43 +00:00
arcbtc 3dce95968e fix 2024-12-20 01:55:00 +00:00
arcbtc 7c92788cd8 fix 2024-12-20 01:54:06 +00:00
arcbtc 2e31d1fd91 Merge branch 'better_fiat_tracking' into all_ui_elements 2024-12-20 01:50:11 +00:00
arcbtc 6c353f366d Merge branch 'card_flip' into all_ui_elements 2024-12-20 01:43:08 +00:00
Arc 902f3c7c6f format 2024-12-20 01:26:47 +00:00
arcbtc 7fbffa20d7 added to card, bg idea was terrible 2024-12-20 01:25:53 +00:00
Arc 0185f1e465 bundle/format 2024-12-19 23:52:27 +00:00
arcbtc 42b3616b03 Cards working nicely on mobile 2024-12-19 23:43:39 +00:00
arcbtc a446cd6c5b different way to do wallet cards 2024-12-19 22:47:37 +00:00
arcbtc 3c0c1ca930 small bug 2024-12-19 20:25:59 +00:00
arcbtc 1a4c706ed4 fixed merge bug 2024-12-19 20:10:15 +00:00
arcbtc 8969c0235d Merge remote-tracking branch 'origin/dev' into better_fiat_tracking 2024-12-19 20:08:37 +00:00
Arc 34ee5b273a format 2024-12-19 19:30:30 +00:00
arcbtc c7c1af9465 working 2024-12-19 19:29:32 +00:00
ArcandGitHub f7ed194e02 Merge branch 'dev' into backgroundimage 2024-12-19 16:24:44 +00:00
ArcandGitHub fd4b6af1de Merge branch 'dev' into calls_to_action 2024-12-19 16:24:29 +00:00
Arc 342bab5714 format 2024-12-19 16:06:28 +00:00
arcbtc 3d9a882066 More calls to actions 2024-12-19 16:05:37 +00:00
Arc 85b2f30a1a format 2024-12-19 13:38:25 +00:00
arcbtc 07eb0ecdde bug 2024-12-19 13:37:11 +00:00
arcbtc ea0e1e032e Merge branch 'better_fiat_tracking' of https://github.com/lnbits/lnbits into better_fiat_tracking 2024-12-19 13:36:30 +00:00
arcbtc b23e8ea781 bug 2024-12-19 13:35:53 +00:00
Arc fa61016ed1 bundle 2024-12-19 13:31:06 +00:00
arcbtc 6f44b755e4 default in create_wallet 2024-12-19 13:22:06 +00:00
Arc 5997fd74fa format and bundle 2024-12-18 23:28:42 +00:00
arcbtc fb5659de2a working, also fixed the button 2024-12-18 23:19:03 +00:00
arcbtc e13fd39e75 layout bug 2024-12-18 22:15:17 +00:00
arcbtc acd3b598b7 bug fix 2024-12-18 21:55:54 +00:00
arcbtc 1a6671b4b3 bug 2024-12-18 21:38:48 +00:00
arcbtc 7fc723c2dd added js 2024-12-18 21:33:02 +00:00
arcbtc 468a786f78 step3 2024-12-18 21:00:41 +00:00
arcbtc f74a7cbe1f step2 2024-12-18 20:56:28 +00:00
arcbtc ce9b63c54e step1 wallet card adjust 2024-12-18 20:53:55 +00:00
arcbtc 3dd1c6992b Trigger to default fiat conversion on
Like all the other wallets
2024-12-18 20:40:32 +00:00
Arc 1c49430ece fundle 2024-12-18 14:55:47 +00:00
arcbtc b14b3bbaad trans 2024-12-18 14:45:20 +00:00
arcbtc 020f6b6b1b account.html 2024-12-18 14:37:35 +00:00
arcbtc 0314453100 account.js 2024-12-18 14:35:40 +00:00
arcbtc 37dca56ccc base.js 2024-12-18 14:34:14 +00:00
183 changed files with 6232 additions and 18164 deletions
+12 -15
View File
@@ -1,17 +1,5 @@
#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 ###########
######################################
@@ -26,7 +14,7 @@ LNBITS_ADMIN_UI=true
# Change theme
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."
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
@@ -145,8 +133,17 @@ BREEZ_GREENLIGHT_SEED=SEED
BREEZ_GREENLIGHT_INVITE_CODE=CODE
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_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
# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
@@ -186,7 +183,7 @@ LNBITS_ADMIN_USERS=""
# SUPER_USER=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="ngrok, nostrclient"
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
# Extensions enabled by default when a user is created
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
+1 -1
View File
@@ -7,7 +7,7 @@ inputs:
default: "3.10"
poetry-version:
description: "Poetry Version"
default: "1.8.5"
default: "1.7.0"
node-version:
description: "Node Version"
default: "20.x"
-81
View File
@@ -1,81 +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
export LNBITS_ADMIN_UI=true
# 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

-72
View File
@@ -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 }}
-2
View File
@@ -63,8 +63,6 @@ jobs:
LNBITS_ENDPOINT: http://localhost:5001
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
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
PYTHONUNBUFFERED: 1
DEBUG: true
+1 -1
View File
@@ -3,7 +3,7 @@ name: release-rc
on:
push:
tags:
- "*-rc[0-9]+"
- "*-rc[0-9]"
permissions:
contents: write
+1 -3
View File
@@ -39,8 +39,6 @@ lnbits/static/bundle-components.js
lnbits/static/bundle.css
lnbits/static/bundle.min.js.old
lnbits/static/bundle.min.css.old
lnbits/static/bundle-components.min.js.old
lnbits/upgrades
docker
# Nix
@@ -53,7 +51,7 @@ lnbits-backup.zip
# Ignore extensions (post installable extension PR)
/lnbits/extensions
/lnbits/upgrades/
/upgrades/
# builded python package
dist
+4 -6
View File
@@ -4,7 +4,7 @@ RUN apt-get clean
RUN apt-get update
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
WORKDIR /app
@@ -19,8 +19,7 @@ ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
ARG POETRY_INSTALL_ARGS="--only main"
RUN poetry install ${POETRY_INSTALL_ARGS}
RUN poetry install --only main
FROM python:3.12-slim-bookworm
@@ -33,7 +32,7 @@ RUN apt-get update && apt-get -y upgrade && \
apt-get -y install postgresql-client-14 postgresql-client-common && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
ENV POETRY_NO_INTERACTION=1 \
@@ -47,8 +46,7 @@ WORKDIR /app
COPY . .
COPY --from=builder /app/.venv .venv
ARG POETRY_INSTALL_ARGS="--only main"
RUN poetry install ${POETRY_INSTALL_ARGS}
RUN poetry install --only main
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
+18 -60
View File
@@ -6,44 +6,30 @@ nav_order: 2
# 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.
## Option 1: AppImage (LInux)
### 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-latest.AppImage --host 0.0.0.0 --port 5000
```
LNbits will create a folder for db and extension files in the folder the AppImage runs from.
## Option 2: Poetry (recommended for developers)
## Option 1 (recommended): Poetry
It is recommended to use the latest version of Poetry. Make sure you have Python version `3.12` installed.
### 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
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
```
### Install Poetry
```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 - && export PATH="$HOME/.local/bin:$PATH"
curl -sSL https://install.python-poetry.org | python3 -
# 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
@@ -78,24 +64,11 @@ poetry run lnbits-cli --help
```sh
cd lnbits
# Stop LNbits with `ctrl + x` or with service manager
# sudo systemctl stop lnbits
# Update LNbits
git pull --rebase
# Check your poetry version with
poetry env list
# If version is less 3.12, update it by running
poetry env use python3.12
poetry env remove python3.9
poetry env list
# Run install and start LNbits with
# Stop LNbits with `ctrl + x`
git pull
# Keep your poetry install up to date, this can be done with `poetry self update`
poetry install --only main
poetry run lnbits
# use LNbits admin UI Extensions page function "Update All" do get extensions onto proper level
# Start LNbits with `poetry run lnbits`
```
## Option 2: Install script (on Debian/Ubuntu)
@@ -108,7 +81,7 @@ chmod +x lnbits.sh &&
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
@@ -150,7 +123,7 @@ SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/ln
## Option 4: Docker
Use latest version from Docker Hub.
use latest version from docker hub
```sh
docker pull lnbits/lnbits
@@ -159,7 +132,7 @@ mkdir data
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
```
Build the image yourself.
build the image yourself
```sh
git clone https://github.com/lnbits/lnbits.git
@@ -170,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
```
You can optionally override the arguments that are passed to `poetry install` during the build process by setting the Docker build argument named `POETRY_INSTALL_ARGS`. For example, to enable the Breez funding source, build the Docker image with the command:
```sh
docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .
```
## Option 5: Fly.io
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
@@ -259,7 +226,7 @@ Problems installing? These commands have helped us install LNbits.
sudo apt install pkg-config libffi-dev libpq-dev
# 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 you used poetry
@@ -583,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.
### FreeBSD notes
Currently there is an issue with secp256k1 0.14.0 on FreeBSD. Thanks to @GitKalle
1. Install package `py311-secp256k1` with `pkg install py311-secp256k1`
2. Change version in `pyproject.toml` from 0.14.0 to 0.13.2
3. Rewrite `poetry.lock` file with command `poetry lock`
4. Follow install instruction with Poetry
+1 -1
View File
@@ -51,7 +51,7 @@ You can also use an AES-encrypted macaroon (more info) instead by using
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
To encrypt your macaroon, run `poetry run lnbits-cli encrypt macaroon`.
To encrypt your macaroon, run `poetry run python lnbits/wallets/macaroon/macaroon.py`.
### LNbits
Generated
+3 -3
View File
@@ -41,11 +41,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1735563628,
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
"lastModified": 1723938990,
"narHash": "sha256-9tUadhnZQbWIiYVXH8ncfGXGvkNq3Hag4RCBEMUk7MI=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
"rev": "c42fcfbdfeae23e68fc520f9182dde9f38ad1890",
"type": "github"
},
"original": {
-10
View File
@@ -34,16 +34,6 @@
protobuf = prev.protobuf.override { preferWheel = true; };
ruff = prev.ruff.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
];
});
});
};
});
+3
View File
@@ -42,6 +42,9 @@ elif [ ! -d lnbits/wallets ]; then
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
fi
# Set path for running after install
export PATH="/home/$USER/.local/bin:$PATH"
# Install the dependencies using Poetry
poetry env use python3.9
poetry install --only main
+16 -39
View File
@@ -24,17 +24,14 @@ from lnbits.core.crud import (
)
from lnbits.core.crud.extensions import create_installed_extension
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.notifications import enqueue_notification
from lnbits.core.tasks import (
from lnbits.core.tasks import ( # watchdog_task
audit_queue,
collect_exchange_rates_data,
killswitch_task,
purge_audit_data,
run_by_the_minute_tasks,
wait_for_audit_data,
wait_for_paid_invoices,
wait_notification_messages,
)
from lnbits.exceptions import register_exception_handlers
from lnbits.helpers import version_parse
@@ -81,7 +78,6 @@ async def startup(app: FastAPI):
# setup admin settings
await check_admin_settings()
core_app_extra.register_new_ratelimiter()
await check_webpush_settings()
# check extensions after restart
@@ -106,24 +102,9 @@ async def startup(app: FastAPI):
# initialize tasks
register_async_tasks()
enqueue_notification(
NotificationType.server_start_stop,
{
"message": "LNbits server started.",
"up_time": settings.lnbits_server_up_time,
},
)
async def shutdown():
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
# shutdown event
@@ -163,13 +144,9 @@ def create_app() -> FastAPI:
core_app_extra.register_new_ratelimiter = register_new_ratelimiter(app)
# register static files
app.mount("/static", StaticFiles(directory=Path("lnbits", "static")), name="static")
Path(settings.lnbits_data_folder, "images").mkdir(parents=True, exist_ok=True)
app.mount(
"/library",
StaticFiles(directory=Path(settings.lnbits_data_folder, "images")),
name="library",
)
static_path = Path("lnbits", "static")
static = StaticFiles(directory=static_path)
app.mount("/static", static, name="static")
g().base_url = f"http://{settings.host}:{settings.port}"
@@ -288,14 +265,14 @@ async def build_all_installed_extensions_list(
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
"""
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():
try:
if not ext_dir.is_dir():
continue
ext_id = ext_dir.name
if ext_id in settings.lnbits_installed_extensions_ids:
if ext_id in settings.lnbits_all_extensions_ids:
continue
ext_info = InstallableExtension.from_ext_dir(ext_id)
if not ext_info:
@@ -310,7 +287,7 @@ async def build_all_installed_extensions_list(
logger.warning(e)
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
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
@@ -371,11 +348,6 @@ async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
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:
return
default_ext_path = os.path.join("lnbits", "extensions")
@@ -394,6 +366,11 @@ def register_custom_extensions_path():
Path(extensions_dir).mkdir(parents=True, exist_ok=True)
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:
# Returns a function that registers new routes for an extension.
@@ -467,8 +444,6 @@ async def check_and_register_extensions(app: FastAPI):
def register_async_tasks():
create_permanent_task(wait_for_audit_data)
create_permanent_task(wait_notification_messages)
create_permanent_task(check_pending_payments)
create_permanent_task(invoice_listener)
create_permanent_task(internal_invoice_listener)
@@ -479,7 +454,9 @@ def register_async_tasks():
register_invoice_listener(invoice_queue, "core")
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(collect_exchange_rates_data)
+15 -105
View File
@@ -3,10 +3,8 @@ import importlib
import sys
import time
from functools import wraps
from getpass import getpass
from pathlib import Path
from typing import Optional
from uuid import uuid4
import click
import httpx
@@ -29,21 +27,19 @@ from lnbits.core.crud import (
update_payment,
)
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 (
CreateExtension,
ExtensionRelease,
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 (
api_install_extension,
api_uninstall_extension,
)
from lnbits.settings import settings
from lnbits.utils.crypto import AESCipher
from lnbits.wallets.base import Wallet
from lnbits.wallets.macaroon import load_macaroon
def coro(f):
@@ -68,13 +64,6 @@ def db():
"""
@lnbits_cli.group()
def users():
"""
Users related commands
"""
@lnbits_cli.group()
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]:
"""Get the superuser"""
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
@@ -134,7 +109,7 @@ async def delete_settings():
"""Deletes the settings"""
async with core_db.connect() as conn:
await conn.execute("DELETE from system_settings")
await conn.execute("DELETE from settings")
@db.command("migrate")
@@ -205,6 +180,17 @@ async def database_revert_payment(checking_id: str):
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")
@click.option("-d", "--days", help="Maximum age of payments in days.")
@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)]))
@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")
@coro
async def extensions_list():
@@ -379,7 +339,7 @@ async def extensions_update(
if not await _can_run_operation(url):
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)
sys.path.append(str(upgrades_dir))
@@ -496,56 +456,6 @@ async def extensions_uninstall(
return False, str(ex)
@encrypt.command("macaroon")
def encrypt_macaroon():
"""Encrypts a macaroon (LND wallets)"""
_macaroon = getpass("Enter macaroon: ")
try:
macaroon = load_macaroon(_macaroon)
except Exception as ex:
click.echo(f"Error loading macaroon: {ex}")
return
key = getpass("Enter encryption key: ")
aes = AESCipher(key.encode())
try:
encrypted_macaroon = aes.encrypt(bytes.fromhex(macaroon))
except Exception as ex:
click.echo(f"Error encrypting macaroon: {ex}")
return
click.echo("Encrypted macaroon: ")
click.echo(encrypted_macaroon)
@encrypt.command("aes")
@click.option("-p", "--payload", required=True, help="Payload to encrypt.")
def encrypt_aes(payload: str):
"""AES encrypts a payload"""
key = getpass("Enter encryption key: ")
aes = AESCipher(key.encode())
try:
encrypted = aes.encrypt(payload.encode())
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.")
def decrypt_aes(payload: str):
"""AES decrypts a payload"""
key = getpass("Enter encryption key: ")
aes = AESCipher(key.encode())
try:
decrypted = aes.decrypt(payload)
except Exception as ex:
click.echo(f"Error decrypting payload: {ex}")
return
click.echo("Decrypted payload: ")
click.echo(decrypted)
def main():
"""main function"""
lnbits_cli()
-4
View File
@@ -43,7 +43,6 @@ from .settings import (
delete_admin_settings,
get_admin_settings,
get_super_settings,
reset_core_settings,
update_admin_settings,
update_super_user,
)
@@ -59,7 +58,6 @@ from .users import (
get_account_by_username_or_email,
get_accounts,
get_user,
get_user_access_control_lists,
get_user_from_account,
update_account,
)
@@ -130,7 +128,6 @@ __all__ = [
"get_super_settings",
"update_admin_settings",
"update_super_user",
"reset_core_settings",
# tinyurl
"create_tinyurl",
"delete_tinyurl",
@@ -148,7 +145,6 @@ __all__ = [
"get_accounts",
"get_user",
"get_user_from_account",
"get_user_access_control_lists",
"update_account",
# wallets
"create_wallet",
+22 -152
View File
@@ -1,23 +1,25 @@
from time import time
from typing import Any, Optional, Tuple
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.models import PaymentState
from lnbits.db import Connection, DateTrunc, Filters, Page
from lnbits.db import DB_TYPE, SQLITE, Connection, Filters, Page
from ..models import (
CreatePayment,
Payment,
PaymentCountField,
PaymentCountStat,
PaymentDailyStats,
PaymentFilters,
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():
pass
@@ -85,7 +87,7 @@ async def get_latest_payments_by_extension(
WHERE status = '{PaymentState.SUCCESS}'
AND extra LIKE :ext_name
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}%"},
Payment,
@@ -95,10 +97,8 @@ async def get_latest_payments_by_extension(
async def get_payments_paginated(
*,
wallet_id: Optional[str] = None,
user_id: Optional[str] = None,
complete: bool = False,
pending: bool = False,
failed: bool = False,
outgoing: bool = False,
incoming: bool = False,
since: Optional[int] = None,
@@ -107,11 +107,11 @@ async def get_payments_paginated(
conn: Optional[Connection] = None,
) -> Page[Payment]:
"""
Filters payments to be returned by:
- complete | pending | failed | outgoing | incoming.
Filters payments to be returned by complete | pending | outgoing | incoming.
"""
values: dict[str, Any] = {
values: dict = {
"wallet_id": wallet_id,
"time": since,
}
clause: list[str] = []
@@ -120,15 +120,7 @@ async def get_payments_paginated(
clause.append(f"time > {db.timestamp_placeholder('time')}")
if wallet_id:
values["wallet_id"] = wallet_id
clause.append("wallet_id = :wallet_id")
elif user_id:
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]
clause.append(f""" wallet_id IN ({", ".join(wallet_ids_str)}) """)
if complete and pending:
clause.append(
@@ -145,8 +137,6 @@ async def get_payments_paginated(
)
elif pending:
clause.append(f"status = '{PaymentState.PENDING}'")
elif failed:
clause.append(f"status = '{PaymentState.FAILED}'")
if outgoing and incoming:
pass
@@ -210,21 +200,6 @@ async def get_payments(
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(
conn: Optional[Connection] = None,
) -> None:
@@ -259,7 +234,6 @@ async def create_payment(
# note: this can be removed if the db uniqueness constraints are set appropriately
previous_payment = await get_standalone_payment(checking_id, conn=conn)
assert previous_payment is None, "Payment already exists"
extra = data.extra or {}
payment = Payment(
checking_id=checking_id,
@@ -273,8 +247,7 @@ async def create_payment(
expiry=data.expiry,
webhook=data.webhook,
fee=data.fee,
tag=extra.get("tag", None),
extra=extra,
extra=data.extra or {},
)
await (conn or db).insert("apipayments", payment)
@@ -311,7 +284,12 @@ async def get_payments_history(
if not 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 = {
"wallet_id": wallet_id,
@@ -363,114 +341,6 @@ async def get_payments_history(
return results
async def get_payment_count_stats(
field: PaymentCountField,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
) -> list[PaymentCountStat]:
if not filters:
filters = Filters()
clause = filters.where()
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,
conn: Optional[Connection] = None,
) -> Tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
if not filters:
filters = Filters()
in_clause = filters.where(
["(apipayments.status = 'success' AND apipayments.amount > 0)"]
)
out_clause = filters.where(
["(apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)"]
)
date_trunc = db.datetime_grouping("day")
query = """
SELECT {date_trunc} date,
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,
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
)
)
""",
]
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(
checking_id: str, wallet_id: str, conn: Optional[Connection] = None
) -> None:
@@ -516,7 +386,7 @@ async def is_internal_status_success(
return payment.status == PaymentState.SUCCESS.value
async def mark_webhook_sent(payment_hash: str, status: str) -> None:
async def mark_webhook_sent(payment_hash: str, status: int) -> None:
await db.execute(
"""
UPDATE apipayments SET webhook_status = :status
+1 -17
View File
@@ -61,23 +61,7 @@ async def update_super_user(super_user: str) -> SuperSettings:
async def delete_admin_settings(tag: Optional[str] = "core") -> None:
await db.execute(
"DELETE FROM system_settings WHERE tag = :tag",
{"tag": tag},
)
async def reset_core_settings() -> None:
await db.execute(
"""
DELETE FROM system_settings WHERE tag = 'core'
AND id NOT IN (
'super_user',
'lnbits_webpush_pubkey',
'lnbits_webpush_privkey'
)
""",
)
await db.execute("DELETE FROM settings WHERE tag = :tag", {"tag": tag})
async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSettings:
-24
View File
@@ -6,7 +6,6 @@ from uuid import uuid4
from lnbits.core.crud.extensions import get_user_active_extensions_ids
from lnbits.core.crud.wallets import get_wallets
from lnbits.core.db import db
from lnbits.core.models import UserAcls
from lnbits.db import Connection, Filters, Page
from ..models import (
@@ -94,8 +93,6 @@ async def get_accounts(
async def get_account(
user_id: str, conn: Optional[Connection] = None
) -> Optional[Account]:
if len(user_id) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE id = :id",
{"id": user_id},
@@ -125,8 +122,6 @@ async def delete_accounts_no_wallets(
async def get_account_by_username(
username: str, conn: Optional[Connection] = None
) -> Optional[Account]:
if len(username) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE username = :username",
{"username": username},
@@ -147,8 +142,6 @@ async def get_account_by_pubkey(
async def get_account_by_email(
email: str, conn: Optional[Connection] = None
) -> Optional[Account]:
if len(email) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE email = :email",
{"email": email},
@@ -192,20 +185,3 @@ async def get_user_from_account(
super_user=account.is_super_user,
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)
+3 -23
View File
@@ -23,7 +23,7 @@ async def create_wallet(
user=user_id,
adminkey=uuid4().hex,
inkey=uuid4().hex,
currency=settings.lnbits_default_accounting_currency or "USD",
currency="USD",
)
await (conn or db).insert("wallets", wallet)
return wallet
@@ -135,26 +135,6 @@ async def get_wallets(
)
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(
key: str,
conn: Optional[Connection] = None,
@@ -173,6 +153,6 @@ async def get_wallet_for_key(
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()
return row.get("balance", 0) or 0
return row.get("balance", 0)
+21 -48
View File
@@ -602,46 +602,34 @@ async def m026_update_payment_table(db: Connection):
async def m027_update_apipayments_data(db: Connection):
result = None
try:
result = await db.execute("SELECT * FROM apipayments LIMIT 100")
result = await db.execute("SELECT * FROM apipayments")
except Exception as exc:
logger.warning("Could not select, trying again after cache cleared.")
logger.debug(exc)
await db.execute("COMMIT")
offset = 0
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("SELECT * FROM apipayments")
result = await db.execute(
f"SELECT * FROM apipayments ORDER BY time LIMIT {limit} OFFSET {offset}"
payments = result.mappings().all()
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):
@@ -696,18 +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")
-12
View File
@@ -14,15 +14,10 @@ from .payments import (
DecodePayment,
PayInvoice,
Payment,
PaymentCountField,
PaymentCountStat,
PaymentDailyStats,
PaymentExtra,
PaymentFilters,
PaymentHistoryPoint,
PaymentsStatusCount,
PaymentState,
PaymentWalletStats,
)
from .tinyurl import TinyURL
from .users import (
@@ -41,7 +36,6 @@ from .users import (
UpdateUserPassword,
UpdateUserPubkey,
User,
UserAcls,
UserExtra,
)
from .wallets import BaseWallet, CreateWallet, KeyType, Wallet, WalletTypeInfo
@@ -68,11 +62,6 @@ __all__ = [
"DecodePayment",
"PayInvoice",
"Payment",
"PaymentCountField",
"PaymentCountStat",
"PaymentDailyStats",
"PaymentsStatusCount",
"PaymentWalletStats",
"PaymentExtra",
"PaymentFilters",
"PaymentHistoryPoint",
@@ -84,7 +73,6 @@ __all__ = [
"Account",
"AccountFilters",
"AccountOverview",
"UserAcls",
"CreateUser",
"RegisterUser",
"LoginUsernamePassword",
+16 -15
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Optional
from pydantic import BaseModel, Field
@@ -9,16 +10,16 @@ from lnbits.settings import settings
class AuditEntry(BaseModel):
component: str | None = None
ip_address: str | None = None
user_id: str | None = None
path: str | None = None
request_type: str | None = None
request_method: str | None = None
request_details: str | None = None
response_code: str | None = None
component: Optional[str] = None
ip_address: Optional[str] = None
user_id: Optional[str] = None
path: Optional[str] = None
request_type: Optional[str] = None
request_method: Optional[str] = None
request_details: Optional[str] = None
response_code: Optional[str] = None
duration: float
delete_at: datetime | None = None
delete_at: Optional[datetime] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
def __init__(self, **data):
@@ -41,12 +42,12 @@ class AuditFilters(FilterModel):
"duration",
]
ip_address: str | None = None
user_id: str | None = None
path: str | None = None
request_method: str | None = None
response_code: str | None = None
component: str | None = None
ip_address: Optional[str] = None
user_id: Optional[str] = None
path: Optional[str] = None
request_method: Optional[str] = None
response_code: Optional[str] = None
component: Optional[str] = None
class AuditCountStat(BaseModel):
+78 -66
View File
@@ -7,7 +7,7 @@ import os
import shutil
import zipfile
from pathlib import Path
from typing import Any
from typing import Any, Optional
import httpx
from loguru import logger
@@ -29,17 +29,17 @@ class ExplicitRelease(BaseModel):
archive: str
hash: str
dependencies: list[str] = []
repo: str | None
icon: str | None
short_description: str | None
min_lnbits_version: str | None
max_lnbits_version: str | None
html_url: str | None # todo: release_url
warning: str | None
info_notification: str | None
critical_notification: str | None
details_link: str | None
pay_link: str | None
repo: Optional[str]
icon: Optional[str]
short_description: Optional[str]
min_lnbits_version: Optional[str]
max_lnbits_version: Optional[str]
html_url: Optional[str] # todo: release_url
warning: Optional[str]
info_notification: Optional[str]
critical_notification: Optional[str]
details_link: Optional[str]
pay_link: Optional[str]
def is_version_compatible(self):
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
@@ -77,9 +77,9 @@ class ExtensionConfig(BaseModel):
name: str
short_description: str
tile: str = ""
warning: str | None = ""
min_lnbits_version: str | None
max_lnbits_version: str | None
warning: Optional[str] = ""
min_lnbits_version: Optional[str]
max_lnbits_version: Optional[str]
def is_version_compatible(self) -> bool:
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
@@ -87,7 +87,7 @@ class ExtensionConfig(BaseModel):
@classmethod
async def fetch_github_release_config(
cls, org: str, repo: str, tag_name: str
) -> ExtensionConfig | None:
) -> Optional[ExtensionConfig]:
config_url = (
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
)
@@ -97,28 +97,28 @@ class ExtensionConfig(BaseModel):
class ReleasePaymentInfo(BaseModel):
amount: int | None = None
pay_link: str | None = None
payment_hash: str | None = None
payment_request: str | None = None
amount: Optional[int] = None
pay_link: Optional[str] = None
payment_hash: Optional[str] = None
payment_request: Optional[str] = None
class PayToEnableInfo(BaseModel):
amount: int = 0
amount: int
required: bool = False
wallet: str | None = None
wallet: Optional[str] = None
class UserExtensionInfo(BaseModel):
paid_to_enable: bool | None = False
payment_hash_to_enable: str | None = None
paid_to_enable: Optional[bool] = False
payment_hash_to_enable: Optional[str] = None
class UserExtension(BaseModel):
user: str
extension: str
active: bool
extra: UserExtensionInfo | None = None
extra: Optional[UserExtensionInfo] = None
@property
def is_paid(self) -> bool:
@@ -140,14 +140,16 @@ class UserExtension(BaseModel):
class Extension(BaseModel):
code: str
is_valid: bool
name: str | None = None
short_description: str | None = None
tile: str | None = None
upgrade_hash: str | None = ""
name: Optional[str] = None
short_description: Optional[str] = None
tile: Optional[str] = None
upgrade_hash: Optional[str] = ""
@property
def module_name(self) -> str:
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}"
if settings.has_default_extension_path:
@@ -176,21 +178,21 @@ class ExtensionRelease(BaseModel):
archive: str
source_repo: str
is_github_release: bool = False
hash: str | None = None
min_lnbits_version: str | None = None
max_lnbits_version: str | None = None
is_version_compatible: bool | None = True
html_url: str | None = None
description: str | None = None
warning: str | None = None
repo: str | None = None
icon: str | None = None
details_link: str | None = None
hash: Optional[str] = None
min_lnbits_version: Optional[str] = None
max_lnbits_version: Optional[str] = None
is_version_compatible: Optional[bool] = True
html_url: Optional[str] = None
description: Optional[str] = None
warning: Optional[str] = None
repo: Optional[str] = None
icon: Optional[str] = None
details_link: Optional[str] = None
pay_link: str | None = None
cost_sats: int | None = None
paid_sats: int | None = 0
payment_hash: str | None = None
pay_link: Optional[str] = None
cost_sats: Optional[int] = None
paid_sats: Optional[int] = 0
payment_hash: Optional[str] = None
@property
def archive_url(self) -> str:
@@ -208,8 +210,8 @@ class ExtensionRelease(BaseModel):
self.cost_sats = payment_info.amount if payment_info else None
async def fetch_release_payment_info(
self, amount: int | None = None
) -> ReleasePaymentInfo | None:
self, amount: Optional[int] = None
) -> Optional[ReleasePaymentInfo]:
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
assert url, "Missing URL for payment info."
try:
@@ -281,7 +283,7 @@ class ExtensionRelease(BaseModel):
return [GitHubRepoRelease.parse_obj(r) for r in releases]
@classmethod
async def fetch_release_details(cls, details_link: str) -> dict | None:
async def fetch_release_details(cls, details_link: str) -> Optional[dict]:
try:
async with httpx.AsyncClient() as client:
@@ -300,12 +302,12 @@ class ExtensionRelease(BaseModel):
class ExtensionMeta(BaseModel):
installed_release: ExtensionRelease | None = None
latest_release: ExtensionRelease | None = None
pay_to_enable: PayToEnableInfo | None = None
installed_release: Optional[ExtensionRelease] = None
latest_release: Optional[ExtensionRelease] = None
pay_to_enable: Optional[PayToEnableInfo] = None
payments: list[ReleasePaymentInfo] = []
dependencies: list[str] = []
archive: str | None = None
archive: Optional[str] = None
featured: bool = False
@@ -313,11 +315,11 @@ class InstallableExtension(BaseModel):
id: str
name: str
version: str
active: bool | None = False
short_description: str | None = None
icon: str | None = None
active: Optional[bool] = False
short_description: Optional[str] = None
icon: Optional[str] = None
stars: int = 0
meta: ExtensionMeta | None = None
meta: Optional[ExtensionMeta] = None
@property
def hash(self) -> str:
@@ -341,11 +343,15 @@ class InstallableExtension(BaseModel):
@property
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
def module_name(self) -> str:
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}"
if settings.has_default_extension_path:
@@ -405,7 +411,9 @@ class InstallableExtension(BaseModel):
def extract_archive(self):
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)
shutil.rmtree(tmp_dir, True)
@@ -452,7 +460,7 @@ class InstallableExtension(BaseModel):
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:
return
if not self.meta or not self.meta.latest_release:
@@ -465,7 +473,9 @@ class InstallableExtension(BaseModel):
):
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:
return None
return next(
@@ -505,7 +515,7 @@ class InstallableExtension(BaseModel):
@classmethod
async def from_github_release(
cls, github_release: GitHubRelease
) -> InstallableExtension | None:
) -> Optional[InstallableExtension]:
try:
repo, latest_release, config = await cls.fetch_github_repo_info(
github_release.organisation, github_release.repository
@@ -544,7 +554,7 @@ class InstallableExtension(BaseModel):
)
@classmethod
def from_ext_dir(cls, ext_id: str) -> InstallableExtension | None:
def from_ext_dir(cls, ext_id: str) -> Optional[InstallableExtension]:
try:
conf_path = Path(
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
@@ -584,6 +594,7 @@ class InstallableExtension(BaseModel):
cls,
) -> list[InstallableExtension]:
extension_list: list[InstallableExtension] = []
extension_id_list: list[str] = []
for url in settings.lnbits_extensions_manifests:
try:
@@ -604,6 +615,7 @@ class InstallableExtension(BaseModel):
meta.featured = ext.id in manifest.featured
ext.meta = meta
extension_list += [ext]
extension_id_list += [ext.id]
for e in manifest.extensions:
release = ExtensionRelease.from_explicit_release(url, e)
@@ -619,10 +631,10 @@ class InstallableExtension(BaseModel):
meta.featured = ext.id in manifest.featured
ext.meta = meta
extension_list += [ext]
extension_id_list += [e.id]
except Exception as e:
logger.warning(f"Manifest {url} failed with '{e!s}'")
settings.lnbits_all_extensions_ids = {e.id for e in extension_list}
return extension_list
@classmethod
@@ -655,7 +667,7 @@ class InstallableExtension(BaseModel):
@classmethod
async def get_extension_release(
cls, ext_id: str, source_repo: str, archive: str, version: str
) -> ExtensionRelease | None:
) -> Optional[ExtensionRelease]:
all_releases: list[ExtensionRelease] = (
await InstallableExtension.get_extension_releases(ext_id)
)
@@ -706,8 +718,8 @@ class CreateExtension(BaseModel):
archive: str
source_repo: str
version: str
cost_sats: int | None = 0
payment_hash: str | None = None
cost_sats: Optional[int] = 0
payment_hash: Optional[str] = None
class ExtensionDetailsRequest(BaseModel):
@@ -716,7 +728,7 @@ class ExtensionDetailsRequest(BaseModel):
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}
if 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()
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:
return ""
_, _, *rest = path.split("/")
+4 -25
View File
@@ -1,9 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
import bcrypt
from pydantic import BaseModel
@@ -11,15 +9,6 @@ def _do_nothing(*_):
pass
@dataclass
class SolveBugBcryptWarning:
__version__: str = getattr(bcrypt, "__version__") # noqa
# fix annoying warning in the logs
setattr(bcrypt, "__about__", SolveBugBcryptWarning()) # noqa
class CoreAppExtra:
register_new_ext_routes: Callable = _do_nothing
register_new_ratelimiter: Callable
@@ -36,12 +25,12 @@ class Callback(BaseModel):
class BalanceDelta(BaseModel):
lnbits_balance_sats: int
node_balance_sats: int
lnbits_balance_msats: int
node_balance_msats: int
@property
def delta_sats(self) -> int:
return int(self.lnbits_balance_sats - self.node_balance_sats)
def delta_msats(self):
return self.node_balance_msats - self.lnbits_balance_msats
class SimpleStatus(BaseModel):
@@ -49,16 +38,6 @@ class SimpleStatus(BaseModel):
message: str
class SimpleItem(BaseModel):
id: str
name: str
class DbVersion(BaseModel):
db: str
version: int
class Image(BaseModel):
filename: str
directory: str = "library"
-65
View File
@@ -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}).
""",
}
+39 -84
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from typing import Literal
from typing import Optional
from fastapi import Query
from pydantic import BaseModel, Field, validator
@@ -28,16 +28,16 @@ class PaymentState(str, Enum):
class PaymentExtra(BaseModel):
comment: str | None = None
success_action: str | None = None
lnurl_response: str | None = None
comment: Optional[str] = None
success_action: Optional[str] = None
lnurl_response: Optional[str] = None
class PayInvoice(BaseModel):
payment_request: str
description: str | None = None
max_sat: int | None = None
extra: dict | None = {}
description: Optional[str] = None
max_sat: Optional[int] = None
extra: Optional[dict] = {}
class CreatePayment(BaseModel):
@@ -46,10 +46,10 @@ class CreatePayment(BaseModel):
bolt11: str
amount_msat: int
memo: str
extra: dict | None = {}
preimage: str | None = None
expiry: datetime | None = None
webhook: str | None = None
extra: Optional[dict] = {}
preimage: Optional[str] = None
expiry: Optional[datetime] = None
webhook: Optional[str] = None
fee: int = 0
@@ -61,13 +61,13 @@ class Payment(BaseModel):
fee: int
bolt11: str
status: str = PaymentState.PENDING
memo: str | None = None
expiry: datetime | None = None
webhook: str | None = None
webhook_status: str | None = None
preimage: str | None = None
tag: str | None = None
extension: str | None = None
memo: Optional[str] = None
expiry: Optional[datetime] = None
webhook: Optional[str] = None
webhook_status: Optional[int] = None
preimage: Optional[str] = None
tag: Optional[str] = None
extension: Optional[str] = None
time: 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))
@@ -125,60 +125,22 @@ class Payment(BaseModel):
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 | None
tag: str | None
checking_id: str | None
status: str
checking_id: str
amount: int
fee: int
memo: str | None
memo: Optional[str]
time: datetime
preimage: str | None
payment_hash: str | None
wallet_id: str | None
class PaymentDataPoint(BaseModel):
date: datetime
count: 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
bolt11: str
preimage: str
payment_hash: str
expiry: Optional[datetime]
extra: dict = {}
wallet_id: str
webhook: Optional[str]
webhook_status: Optional[int]
class PaymentHistoryPoint(BaseModel):
@@ -190,7 +152,7 @@ class PaymentHistoryPoint(BaseModel):
class DecodePayment(BaseModel):
data: str
filter_fields: list[str] | None = []
filter_fields: Optional[list[str]] = []
class CreateInvoice(BaseModel):
@@ -198,14 +160,14 @@ class CreateInvoice(BaseModel):
internal: bool = False
out: bool = True
amount: float = Query(None, ge=0)
memo: str | None = Query(None, max_length=640)
description_hash: str | None = None
unhashed_description: str | None = None
expiry: int | None = None
extra: dict | None = None
webhook: str | None = None
bolt11: str | None = None
lnurl_callback: str | None = None
memo: Optional[str] = None
description_hash: Optional[str] = None
unhashed_description: Optional[str] = None
expiry: Optional[int] = None
extra: Optional[dict] = None
webhook: Optional[str] = None
bolt11: Optional[str] = None
lnurl_callback: Optional[str] = None
@validator("unit")
@classmethod
@@ -213,10 +175,3 @@ class CreateInvoice(BaseModel):
if v != "sat" and v not in allowed_currencies():
raise ValueError("The provided unit is not supported")
return v
class PaymentsStatusCount(BaseModel):
incoming: int = 0
outgoing: int = 0
failed: int = 0
pending: int = 0
+39 -128
View File
@@ -1,13 +1,13 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Optional
from uuid import UUID
from fastapi import Query
from passlib.context import CryptContext
from pydantic import BaseModel, Field
from lnbits.core.models.misc import SimpleItem
from lnbits.db import FilterModel
from lnbits.helpers import is_valid_email_address, is_valid_pubkey, is_valid_username
from lnbits.settings import settings
@@ -16,86 +16,25 @@ from .wallets import Wallet
class UserExtra(BaseModel):
email_verified: bool | None = False
first_name: str | None = None
last_name: str | None = None
display_name: str | None = None
picture: str | None = None
email_verified: Optional[bool] = False
first_name: Optional[str] = None
last_name: Optional[str] = None
display_name: Optional[str] = None
picture: Optional[str] = None
# Auth provider, possible values:
# - "env": the user was created automatically by the system
# - "lnbits": the user was created via register form (username/pass or user_id only)
# - "google | github | ...": the user was created using an SSO provider
provider: str | None = "lnbits" # auth provider
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
provider: Optional[str] = "lnbits" # auth provider
class Account(BaseModel):
id: str
username: str | None = None
password_hash: str | None = None
pubkey: str | None = None
email: str | None = None
username: Optional[str] = None
password_hash: Optional[str] = None
pubkey: Optional[str] = None
email: Optional[str] = None
extra: UserExtra = UserExtra()
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@@ -133,10 +72,10 @@ class Account(BaseModel):
class AccountOverview(Account):
transaction_count: int | None = 0
wallet_count: int | None = 0
balance_msat: int | None = 0
last_payment: datetime | None = None
transaction_count: Optional[int] = 0
wallet_count: Optional[int] = 0
balance_msat: Optional[int] = 0
last_payment: Optional[datetime] = None
class AccountFilters(FilterModel):
@@ -150,20 +89,20 @@ class AccountFilters(FilterModel):
"last_payment",
]
email: str | None = None
user: str | None = None
username: str | None = None
pubkey: str | None = None
wallet_id: str | None = None
email: Optional[str] = None
user: Optional[str] = None
username: Optional[str] = None
pubkey: Optional[str] = None
wallet_id: Optional[str] = None
class User(BaseModel):
id: str
created_at: datetime
updated_at: datetime
email: str | None = None
username: str | None = None
pubkey: str | None = None
email: Optional[str] = None
username: Optional[str] = None
pubkey: Optional[str] = None
extensions: list[str] = []
wallets: list[Wallet] = []
admin: bool = False
@@ -175,7 +114,7 @@ class User(BaseModel):
def wallet_ids(self) -> list[str]:
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]
return w[0] if w else None
@@ -191,33 +130,33 @@ class User(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)
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
class CreateUser(BaseModel):
id: str | None = Query(default=None)
email: str | None = Query(default=None)
username: str | None = Query(default=None, min_length=2, max_length=20)
password: str | None = Query(default=None, min_length=8, max_length=50)
password_repeat: str | None = Query(default=None, min_length=8, max_length=50)
id: Optional[str] = Query(default=None)
email: Optional[str] = Query(default=None)
username: Optional[str] = Query(default=None, min_length=2, max_length=20)
password: Optional[str] = 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)
extensions: list[str] | None = None
extra: UserExtra | None = None
extensions: Optional[list[str]] = None
extra: Optional[UserExtra] = None
class UpdateUser(BaseModel):
user_id: str
email: str | None = Query(default=None)
username: str | None = Query(default=..., min_length=2, max_length=20)
extra: UserExtra | None = None
email: Optional[str] = Query(default=None)
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
extra: Optional[UserExtra] = None
class UpdateUserPassword(BaseModel):
user_id: str
password_old: str | None = None
password_old: Optional[str] = None
password: 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)
@@ -251,39 +190,11 @@ class LoginUsernamePassword(BaseModel):
class AccessTokenPayload(BaseModel):
sub: str
usr: str | None = None
email: str | None = None
auth_time: int | None = 0
api_token_id: str | None = None
usr: Optional[str] = None
email: Optional[str] = None
auth_time: Optional[int] = 0
class UpdateBalance(BaseModel):
id: str
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
+3 -8
View File
@@ -5,6 +5,7 @@ import hmac
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from typing import Optional
from ecdsa import SECP256k1, SigningKey
from pydantic import BaseModel, Field
@@ -22,11 +23,6 @@ class BaseWallet(BaseModel):
balance_msat: int
class WalletExtra(BaseModel):
icon: str = "flash_on"
color: str = "primary"
class Wallet(BaseModel):
id: str
user: str
@@ -36,9 +32,8 @@ class Wallet(BaseModel):
deleted: bool = False
created_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)
extra: WalletExtra = WalletExtra()
@property
def balance(self) -> int:
@@ -66,7 +61,7 @@ class Wallet(BaseModel):
class CreateWallet(BaseModel):
name: str | None = None
name: Optional[str] = None
class KeyType(Enum):
+4 -4
View File
@@ -3,7 +3,6 @@ from .funding_source import (
switch_to_voidwallet,
)
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
from .notifications import enqueue_notification, send_payment_notification
from .payments import (
calculate_fiat_amounts,
check_transaction_status,
@@ -12,6 +11,7 @@ from .payments import (
fee_reserve,
fee_reserve_total,
pay_invoice,
send_payment_notification,
service_fee,
update_pending_payments,
update_wallet_balance,
@@ -24,6 +24,7 @@ from .users import (
check_admin_settings,
create_user_account,
create_user_account_no_ckeck,
init_admin_settings,
update_user_account,
update_user_extensions,
)
@@ -36,9 +37,6 @@ __all__ = [
# lnurl
"redeem_lnurl_withdraw",
"perform_lnurlauth",
# notifications
"enqueue_notification",
"send_payment_notification",
# payments
"calculate_fiat_amounts",
"check_transaction_status",
@@ -47,6 +45,7 @@ __all__ = [
"fee_reserve",
"fee_reserve_total",
"pay_invoice",
"send_payment_notification",
"service_fee",
"update_pending_payments",
"update_wallet_balance",
@@ -57,6 +56,7 @@ __all__ = [
"check_admin_settings",
"create_user_account",
"create_user_account_no_ckeck",
"init_admin_settings",
"update_user_account",
"update_user_extensions",
# websockets
+2 -62
View File
@@ -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.wallets import get_funding_source, set_funding_source
@@ -22,62 +18,6 @@ async def get_balance_delta() -> BalanceDelta:
status = await funding_source.status()
lnbits_balance = await get_total_balance()
return BalanceDelta(
lnbits_balance_sats=int(lnbits_balance) // 1000,
node_balance_sats=status.balance_msat // 1000,
lnbits_balance_msats=lnbits_balance,
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
+1 -4
View File
@@ -13,7 +13,7 @@ from lnbits.decorators import (
WalletTypeInfo,
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 decode as decode_lnurl
from lnbits.settings import settings
@@ -37,7 +37,6 @@ async def redeem_lnurl_withdraw(
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
lnurl = decode_lnurl(lnurl_request)
check_callback_url(str(lnurl))
r = await client.get(str(lnurl))
res = r.json()
@@ -73,7 +72,6 @@ async def redeem_lnurl_withdraw(
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
check_callback_url(res["callback"])
await client.get(res["callback"], params=params)
except Exception:
pass
@@ -137,7 +135,6 @@ async def perform_lnurlauth(
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
assert key.verifying_key, "LNURLauth verifying_key does not exist"
check_callback_url(callback)
r = await client.get(
callback,
params={
-73
View File
@@ -1,73 +0,0 @@
import asyncio
from typing import Tuple
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
-332
View File
@@ -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, Tuple
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}"
)
+71 -173
View File
@@ -1,6 +1,5 @@
import asyncio
import json
import time
from datetime import datetime, timedelta, timezone
from typing import Optional
from bolt11 import Bolt11, MilliSatoshi, Tags
@@ -8,14 +7,11 @@ from bolt11 import decode as bolt11_decode
from bolt11 import encode as bolt11_encode
from loguru import logger
from lnbits.core.crud.payments import get_daily_stats
from lnbits.core.db import db
from lnbits.core.models import PaymentDailyStats, PaymentFilters
from lnbits.db import Connection, Filters
from lnbits.db import Connection
from lnbits.decorators import check_user_extension_access
from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.settings import settings
from lnbits.tasks import create_task
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
from lnbits.wallets import fake_wallet, get_funding_source
@@ -42,10 +38,7 @@ from ..models import (
PaymentState,
Wallet,
)
from .notifications import send_payment_notification
payment_lock = asyncio.Lock()
wallets_payments_lock: dict[str, asyncio.Lock] = {}
from .websockets import websocket_manager
async def pay_invoice(
@@ -58,16 +51,14 @@ async def pay_invoice(
tag: str = "",
conn: Optional[Connection] = None,
) -> Payment:
if settings.lnbits_only_allow_incoming_payments:
raise PaymentError("Only incoming payments allowed.", status="failed")
invoice = _validate_payment_request(payment_request, max_sat)
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
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")
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
@@ -82,12 +73,10 @@ async def pay_invoice(
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)
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
await _credit_service_fee_wallet(payment, new_conn)
return payment
return payment
async def create_invoice(
@@ -111,7 +100,7 @@ async def create_invoice(
if not user_wallet:
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
funding_source = fake_wallet if internal else get_funding_source()
@@ -120,12 +109,6 @@ async def create_invoice(
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(
user_wallet.balance_msat / 1000 + amount_sat
):
@@ -135,29 +118,29 @@ async def create_invoice(
status="failed",
)
payment_response = await funding_source.create_invoice(
(
ok,
checking_id,
payment_request,
error_message,
) = await funding_source.create_invoice(
amount=amount_sat,
memo=invoice_memo,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=expiry or settings.lightning_invoice_expiry,
)
if (
not payment_response.ok
or not payment_response.payment_request
or not payment_response.checking_id
):
if not ok or not payment_request or not checking_id:
raise InvoiceError(
message=payment_response.error_message or "unexpected backend error.",
status="pending",
error_message or "unexpected backend error.", status="pending"
)
invoice = bolt11_decode(payment_response.payment_request)
invoice = bolt11_decode(payment_request)
create_payment_model = CreatePayment(
wallet_id=wallet_id,
bolt11=payment_response.payment_request,
bolt11=payment_request,
payment_hash=invoice.payment_hash,
preimage=payment_response.preimage,
amount_msat=amount_sat * 1000,
expiry=invoice.expiry_date,
memo=memo,
@@ -166,7 +149,7 @@ async def create_invoice(
)
payment = await create_payment(
checking_id=payment_response.checking_id,
checking_id=checking_id,
data=create_payment_model,
conn=conn,
)
@@ -181,20 +164,13 @@ async def update_pending_payments(wallet_id: str):
exclude_uncheckable=True,
)
for payment in pending_payments:
await update_pending_payment(payment)
async def update_pending_payment(payment: Payment) -> bool:
status = await payment.check_status()
if status.failed:
payment.status = PaymentState.FAILED
await update_payment(payment)
return True
if status.success:
payment.status = PaymentState.SUCCESS
await update_payment(payment)
return True
return False
status = await payment.check_status()
if status.failed:
payment.status = PaymentState.FAILED
await update_payment(payment)
elif status.success:
payment.status = PaymentState.SUCCESS
await update_payment(payment)
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
@@ -202,7 +178,6 @@ def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
def fee_reserve(amount_msat: int, internal: bool = False) -> int:
amount_msat = abs(amount_msat)
return settings.fee_reserve(amount_msat, internal)
@@ -286,6 +261,27 @@ async def update_wallet_balance(
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(
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
):
@@ -358,22 +354,17 @@ async def calculate_fiat_amounts(
fiat_amounts["fiat_currency"] = currency
fiat_amounts["fiat_amount"] = round(amount, ndigits=3)
fiat_amounts["fiat_rate"] = amount_sat / amount
fiat_amounts["btc_rate"] = (amount / amount_sat) * 100_000_000
else:
amount_sat = int(amount)
if wallet_currency:
try:
if wallet_currency == currency:
fiat_amount = amount
else:
fiat_amount = await satoshis_amount_as_fiat(amount_sat, wallet_currency)
fiat_amounts["wallet_fiat_currency"] = wallet_currency
fiat_amounts["wallet_fiat_amount"] = round(fiat_amount, ndigits=3)
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}")
if wallet_currency == currency:
fiat_amount = amount
else:
fiat_amount = await satoshis_amount_as_fiat(amount_sat, wallet_currency)
fiat_amounts["wallet_fiat_currency"] = wallet_currency
fiat_amounts["wallet_fiat_amount"] = round(fiat_amount, ndigits=3)
fiat_amounts["wallet_fiat_rate"] = amount_sat / fiat_amount
logger.debug(
f"Calculated fiat amounts {wallet.id=} {amount=} {currency=}: {fiat_amounts=}"
@@ -397,74 +388,11 @@ async def check_transaction_status(
return await payment.check_status()
async def get_payments_daily_stats(
filters: Filters[PaymentFilters],
) -> list[PaymentDailyStats]:
data_in, data_out = await get_daily_stats(filters)
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_invoice(wallet, create_payment_model, conn):
payment = await _pay_internal_invoice(wallet, create_payment_model, conn)
if not payment:
payment = await _pay_external_invoice(wallet, create_payment_model, conn)
return payment
async def _pay_internal_invoice(
@@ -499,15 +427,12 @@ async def _pay_internal_invoice(
):
raise PaymentError("Invalid invoice. Bolt11 changed.", status="failed")
fee_reserve_total_msat = fee_reserve_total(amount_msat, internal=True)
fee_reserve_total_msat = fee_reserve_total(abs(amount_msat), internal=True)
create_payment_model.fee = abs(fee_reserve_total_msat)
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
raise PaymentError("Insufficient balance.", status="failed")
# release the preimage
create_payment_model.preimage = internal_invoice.preimage
internal_id = f"internal_{create_payment_model.payment_hash}"
logger.debug(f"creating temporary internal payment with id {internal_id}")
payment = await create_payment(
@@ -545,8 +470,6 @@ async def _pay_external_invoice(
fee_reserve_total_msat = fee_reserve_total(amount_msat, internal=False)
if wallet.balance_msat < abs(amount_msat):
raise PaymentError("Insufficient balance.", status="failed")
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
raise PaymentError(
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
@@ -568,19 +491,13 @@ async def _pay_external_invoice(
fee_reserve_msat = fee_reserve(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)
funding_source = get_funding_source()
logger.debug(f"fundingsource: sending payment {checking_id}")
payment_response: PaymentResponse = await funding_source.pay_invoice(
create_payment_model.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
logger.debug(f"backend: pay_invoice finished {checking_id}, {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:"
@@ -616,22 +533,9 @@ async def _pay_external_invoice(
"didn't receive checking_id from backend, payment may be stuck in"
f" database: {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()
payment_response: PaymentResponse = await funding_source.pay_invoice(
bolt11, fee_reserve_msat
)
logger.debug(f"backend: pay_invoice finished {checking_id}, {payment_response}")
return payment_response
async def _verify_external_payment(
payment: Payment, conn: Optional[Connection] = None
) -> Payment:
@@ -691,14 +595,8 @@ def _validate_payment_request(
if not invoice.amount_msat or not invoice.amount_msat > 0:
raise PaymentError("Amountless invoices not supported.", status="failed")
max_sat = max_sat or settings.lnbits_max_outgoing_payment_amount_sats
max_sat = min(max_sat, settings.lnbits_max_outgoing_payment_amount_sats)
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",
)
if max_sat and invoice.amount_msat > max_sat * 1000:
raise PaymentError("Amount in invoice is too high.", status="failed")
return invoice
+1 -6
View File
@@ -6,7 +6,6 @@ from py_vapid.utils import b64urlencode
from lnbits.db import dict_to_model
from lnbits.settings import (
EditableSettings,
UpdateSettings,
readonly_variables,
settings,
)
@@ -38,12 +37,8 @@ async def check_webpush_settings():
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):
editable_settings = dict_to_settings(sets_dict)
editable_settings = dict_to_model(sets_dict, EditableSettings)
for key in sets_dict.keys():
if key in readonly_variables:
continue
+13 -10
View File
@@ -8,6 +8,7 @@ from lnbits.core.models.extensions import UserExtension
from lnbits.settings import (
EditableSettings,
SuperSettings,
send_admin_user_to_saas,
settings,
)
@@ -46,9 +47,7 @@ async def create_user_account(
async def create_user_account_no_ckeck(
account: Optional[Account] = None,
wallet_name: Optional[str] = None,
default_exts: Optional[list[str]] = None,
account: Optional[Account] = None, wallet_name: Optional[str] = None
) -> User:
if account:
@@ -71,13 +70,9 @@ async def create_user_account_no_ckeck(
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
)
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
for ext_id in user_extensions:
try:
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}")
for ext_id in settings.lnbits_user_default_extensions:
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
await update_user_extension(user_ext)
user = await get_user_from_account(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:
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)
if account and account.extra and account.extra.provider == "env":
settings.first_install = True
+111 -90
View File
@@ -1,92 +1,79 @@
import asyncio
import traceback
from typing import Callable, Coroutine
import httpx
from loguru import logger
from lnbits.core.crud import (
create_audit_entry,
get_wallet,
get_webpush_subscriptions_for_user,
mark_webhook_sent,
)
from lnbits.core.crud.audit import delete_expired_audit_entries
from lnbits.core.crud.payments import get_payments_status_count
from lnbits.core.crud.users import get_accounts
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,
from lnbits.core.models import AuditEntry, Payment
from lnbits.core.services import (
get_balance_delta,
)
from lnbits.core.services.notifications import (
enqueue_notification,
process_next_notification,
send_payment_notification,
switch_to_voidwallet,
)
from lnbits.db import Filters
from lnbits.settings import settings
from lnbits.tasks import create_unique_task
from lnbits.settings import get_funding_source, settings
from lnbits.tasks import send_push_notification
from lnbits.utils.exchange_rates import btc_rates
audit_queue: asyncio.Queue = asyncio.Queue()
async def run_by_the_minute_tasks():
minute_counter = 0
async def killswitch_task():
"""
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:
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:
# runs by default every minute, the delta should not change that often
await check_balance_delta_changed()
except Exception as ex:
logger.error(ex)
if minute_counter % settings.lnbits_watchdog_interval_minutes == 0:
try:
await check_server_balance_against_node()
except Exception as ex:
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)
balance = await get_balance_delta()
delta = balance.delta_msats
logger.debug(f"Running watchdog task. current delta: {delta}")
if delta + settings.lnbits_watchdog_delta <= 0:
logger.error(f"Switching to VoidWallet. current delta: {delta}")
await switch_to_voidwallet()
except Exception as e:
logger.error("Error in watchdog task", e)
await asyncio.sleep(settings.lnbits_watchdog_interval * 60)
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
@@ -100,6 +87,61 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
wallet = await get_wallet(payment.wallet_id)
if wallet:
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():
@@ -116,16 +158,6 @@ async def wait_for_audit_data():
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():
"""
Remove audit entries which have passed their retention period.
@@ -162,14 +194,3 @@ async def collect_exchange_rates_data():
else:
sleep_time = 60
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())
+29 -45
View File
@@ -15,8 +15,11 @@
/>
</q-item-section>
<q-item-section>
<q-item-label v-text="$t('enable_audit')"></q-item-label>
<q-item-label caption v-text="$t('audit_desc')"></q-item-label>
<q-item-label>Enable audit</q-item-label>
<q-item-label caption
>Record HTTP requests according with the specified
filters</q-item-label
>
</q-item-section>
</q-item>
</div>
@@ -45,16 +48,14 @@
/>
</q-item-section>
<q-item-section>
<q-item-label v-text="$t('audit_record_req')"></q-item-label>
<q-item-label caption>
<span v-text="$t('audit_record_warning')"></span>
<br />
<q-item-label>Record Request Body</q-item-label>
<q-item-label caption
>Warning:
<ul>
<li><span v-text="$t('audit_record_req_warning_1')"></span></li>
<li><span v-text="$t('audit_record_req_warning_2')"></span></li>
<li>confidential data (like passwords) will be logged</li>
<li>the request body can have large size.</li>
</ul>
<br />
<span v-text="$t('audit_record_use')"></span>
Use it with caution.
</q-item-label>
</q-item-section>
</q-item>
@@ -71,12 +72,8 @@
/>
</q-item-section>
<q-item-section>
<q-item-label>
<span v-text="$t('audit_ip')"></span>
</q-item-label>
<q-item-label caption>
<span v-text="$t('audit_ip_desc')"></span>
</q-item-label>
<q-item-label>Record IP Address</q-item-label>
<q-item-label caption>Save the client IP address.</q-item-label>
</q-item-section>
</q-item>
</div>
@@ -92,12 +89,8 @@
/>
</q-item-section>
<q-item-section>
<q-item-label>
<span v-text="$t('audit_path_params')"></span>
</q-item-label>
<q-item-label caption>
<span v-text="$t('recommended')"></span>
</q-item-label>
<q-item-label>Record Path Parameters</q-item-label>
<q-item-label caption>Recommended. </q-item-label>
</q-item-section>
</q-item>
</div>
@@ -113,12 +106,8 @@
/>
</q-item-section>
<q-item-section>
<q-item-label>
<span v-text="$t('audit_query_params')"></span>
</q-item-label>
<q-item-label caption>
<span v-text="$t('recommended')"></span>
</q-item-label>
<q-item-label>Record Query Parameters</q-item-label>
<q-item-label caption>Recommended.</q-item-label>
</q-item-section>
</q-item>
</div>
@@ -126,26 +115,25 @@
<q-separator class="q-mb-xl q-mt-sm"></q-separator>
<div class="row q-mb-lg">
<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
filled
v-model="formData.lnbits_audit_http_methods"
multiple
:hint="$t('audit_http_methods_hint')"
:label="$t('audit_http_methods_label')"
hint="List of HTTP methods to be included. Empty lists means all."
label="HTTP Methods"
:options="['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']"
></q-select>
</div>
<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
class="q-mb-md"
filled
v-model="formAddIncludeResponseCode"
@keydown.enter="addIncludeResponseCode"
type="text"
:label="$t('audit_resp_codes_label')"
:hint="$t('audit_resp_codes_hint')"
label="HTTP Response code (regex)"
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-input>
@@ -166,15 +154,14 @@
<div class="row">
<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
class="q-mb-md"
filled
v-model="formAddIncludePath"
@keydown.enter="addIncludePath"
type="text"
:label="$t('audit_paths_label')"
:hint="$t('audit_paths_hint')"
label="HTTP Path (regex)"
hint="List of paths to be included (regex match). Empty list means all."
>
<q-btn @click="addIncludePath" dense flat icon="add"></q-btn>
</q-input>
@@ -193,17 +180,14 @@
<br />
</div>
<div class="col-md-6 col-sm-12">
<p>
<span v-text="$t('audit_paths_exclude')"></span>
</p>
<p>Exclude Paths</p>
<q-input
class="q-mb-md"
filled
v-model="formAddExcludePath"
@keydown.enter="addExcludePath"
type="text"
:label="$t('audit_paths_exclude_label')"
:hint="$t('audit_paths_exclude_hint')"
label="HTTP Path (regex)"
hint="List of paths to be excluded (regex match). Empty list means none."
>
<q-btn @click="addExcludePath" dense flat icon="add"></q-btn>
</q-input>
@@ -1,7 +1,5 @@
<q-tab-panel name="exchange_providers">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('exchange_providers')"></span>
</h6>
<h6 class="q-my-none q-mb-sm">Exchange Providers</h6>
<div class="row">
<div class="col-md-8 col-sm-12">
@@ -1,22 +1,17 @@
<q-tab-panel name="extensions">
<q-card-section class="q-pa-none">
<div>
<h6 class="q-my-none">
<span v-text="$t('extensions')"></span>
</h6>
<h6 class="q-my-none">Extensions</h6>
<div class="row q-col-gutter-md">
<div class="col-12 q-mb-md">
<p>
<span v-text="$t('extension_sources')"></span>
</p>
<div class="col-12">
<p>Extension Sources</p>
<q-input
class="q-mb-md"
filled
v-model="formAddExtensionsManifest"
@keydown.enter="addExtensionsManifest"
type="text"
:label="$t('ext_sources_label')"
:hint="$t('ext_sources_hint')"
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
hint="Repositories from where the extensions can be downloaded"
>
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
</q-input>
@@ -35,44 +30,34 @@
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>
<span v-text="$t('admin_extensions')"></span>
</p>
<p>Admin Extensions</p>
<q-select
filled
v-model="formData.lnbits_admin_extensions"
multiple
:label="$t('admin_extensions_label')"
:hint="$t('admin_extensions_hint')"
hint="Extensions only user with admin privileges can use"
label="Admin extensions"
:options="g.extensions"
></q-select>
</div>
<div class="col-12 col-md-6">
<p>
<span v-text="$t('user_default_extensions')"></span>
</p>
<p>User Default Extensions</p>
<q-select
filled
v-model="formData.lnbits_user_default_extensions"
multiple
:label="$t('user_default_extensions_label')"
:hint="$t('user_default_extensions_hint')"
hint="Extensions that will be enabled by default for the users."
label="User extensions"
:options="g.extensions"
></q-select>
</div>
<div class="col-12 col-md-6">
<p>
<span v-text="$t('miscellaneous')"></span>
</p>
<p>Miscellaneous</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>
<span v-text="$t('misc_disable_extensions')"></span>
</q-item-label>
<q-item-label caption>
<span v-text="$t('misc_disable_extensions_label')"></span>
</q-item-label>
<q-item-label>Disable Extensions</q-item-label>
<q-item-label caption>Disables all extensions</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
@@ -86,12 +71,10 @@
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>
<span v-text="$t('misc_hide_api')"></span>
</q-item-label>
<q-item-label caption>
<span v-text="$t('misc_hide_api_label')"></span>
</q-item-label>
<q-item-label>Hide API</q-item-label>
<q-item-label caption
>Hides wallet api, extensions can choose to honor</q-item-label
>
</q-item-section>
<q-item-section avatar>
<q-toggle
+40 -140
View File
@@ -1,104 +1,88 @@
<q-tab-panel name="funding">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">
<span v-text="$t('wallets_management')"></span>
</h6>
<h6 class="q-my-none">Wallets Management</h6>
<br />
<div>
<div class="row">
<div class="col">
<p>
<span v-text="$t('funding_source_info')"></span>
</p>
<p>Funding Source Info</p>
<ul>
<li
v-text="$t('funding_source', {wallet_class: settings.lnbits_backend_wallet_class})"
v-text="'Funding Source: '+ settings.lnbits_backend_wallet_class"
></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
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
v-text="$t('funding_reserve_percent', {
percent: auditData.lnbits_balance_sats > 0
? (auditData.node_balance_sats / auditData.lnbits_balance_sats * 100).toFixed(2)
: 100
})"
v-text="'Reserve Percent: ' + (auditData.node_balance_msats /
auditData.lnbits_balance_msats * 100).toFixed(2) + ' %'"
></li>
</ul>
<br />
</div>
<div class="col">
{% if LNBITS_NODE_UI_AVAILABLE %}
<p><span v-text="$t('node_management')"></span></p>
<p>Node Management</p>
<q-toggle
:label="$t('toggle_node_ui')"
label="Node UI"
v-model="formData.lnbits_node_ui"
></q-toggle>
<q-toggle
v-if="formData.lnbits_node_ui"
:label="$t('toggle_public_node_ui')"
label="Public node UI"
v-model="formData.lnbits_public_node_ui"
></q-toggle>
<br />
<q-toggle
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"
></q-toggle>
{% else %}
<p><span v-text="$t('node_management_not_supported')"></span></p>
<p>Node Management not supported by active funding source</p>
{% endif %}
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p><span v-text="$t('fee_reserve')"></span></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>
<div class="col-12 col-md-4">
<p>Invoice Expiry</p>
<q-input
filled
v-model.number="formData.lightning_invoice_expiry"
type="number"
:label="$t('invoice_expiry_label')"
label="Invoice expiry (seconds)"
mask="#######"
>
</q-input>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('payment_wait_time')"></span></p>
<q-input
type="number"
filled
name="lnbits_funding_source_pay_invoice_wait_seconds"
v-model="formData.lnbits_funding_source_pay_invoice_wait_seconds"
:label="$t('payment_wait_time')"
:hint="$t('payment_wait_time_desc')"
step="1"
min="0"
></q-input>
<div class="col-12 col-md-8">
<p>Fee reserve</p>
<div class="row q-col-gutter-md">
<div class="col-6">
<q-input
type="number"
filled
v-model="formData.lnbits_reserve_fee_min"
label="Reserve fee in msats"
>
</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 v-if="isSuperUser">
@@ -107,90 +91,6 @@
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/>
</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>
</q-card-section>
</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-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('notifications_configure')"></span>
<q-btn
round
flat
@click="hideInputsToggle()"
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
></q-btn>
</h6>
<q-separator class="q-mt-md q-mb-sm"></q-separator>
<div class="row q-col-gutter-md">
<div class="col-sm-12 col-md-6">
<strong v-text="$t('notifications_nostr_config')"></strong>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_enable_nostr')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_enable_nostr_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_nostr_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_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 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_notifications')"></q-item-label>
<q-item-label
caption
v-text="$t('enable_notifications_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notifications"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
<p
v-if="!formData.lnbits_notifications"
v-text="$t('notifications_disabled')"
></p>
<div v-if="formData.lnbits_notifications">
{% include "admin/_tab_security_notifications.html" %}
</div>
<div class="col">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_to_emails')"
></q-item-label>
<q-item-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>
<br />
<div>
<p v-text="$t('notification_source')"></p>
<q-input
filled
v-model="formData.lnbits_status_manifest"
type="text"
:label="$t('notification_source_label')"
/>
</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>
<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>
</q-card-section>
+42 -50
View File
@@ -1,28 +1,26 @@
<q-tab-panel name="security">
<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="col-md-6">
<p><span v-text="$t('base_url')"></span></p>
<p>Base URL</p>
<q-input
filled
v-model.number="formData.lnbits_baseurl"
:label="$t('base_url_label')"
label="Static/Base url for the server"
></q-input>
<br />
</div>
</div>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('authentication')"></span>
</h6>
<h6 class="q-my-none q-mb-sm">Authentication</h6>
<div class="row q-col-gutter-sm">
<div class="col-12 col-sm-6">
<q-input
filled
v-model="formData.auth_token_expire_minutes"
type="number"
:label="$t('auth_token_expiry_label')"
:hint="$t('auth_token_expiry_hint')"
label="Token expire minutes"
hint="Time in minutes until the token expires"
>
</q-input>
</div>
@@ -31,8 +29,8 @@
filled
v-model="formData.auth_allowed_methods"
multiple
:hint="$t('auth_allowed_methods_hint')"
:label="$t('auth_allowed_methods_label')"
hint="Allowed authorization methods"
label="Select authorization methods"
:options="formData.auth_all_methods"
></q-select>
</div>
@@ -52,8 +50,8 @@
v-model="nostrAcceptedUrl"
@keydown.enter="addNostrUrl"
type="text"
:label="$t('auth_nostr_label')"
:hint="$t('auth_nostr_hint')"
label="Nostr Request URL"
hint="Absolute URL that the clients will use to login."
>
<q-btn @click="addNostrUrl" dense flat icon="add"></q-btn>
</q-input>
@@ -83,8 +81,8 @@
<q-input
filled
v-model="formData.google_client_id"
:label="$t('auth_google_ci_label')"
:hint="$t('auth_google_ci_hint')"
label="Google Client ID"
hint="Make sure thant the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token"
>
</q-input>
</div>
@@ -93,7 +91,7 @@
filled
v-model="formData.google_client_secret"
type="password"
:label="$t('auth_google_cs_label')"
label="Google Client Secret"
>
</q-input>
</div>
@@ -110,8 +108,8 @@
<q-input
filled
v-model="formData.github_client_id"
:label="$t('auth_gh_client_id_label')"
:hint="$t('auth_gh_client_id_hint')"
label="GitHub Client ID"
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/github/token"
>
</q-input>
</div>
@@ -120,7 +118,7 @@
filled
v-model="formData.github_client_secret"
type="password"
:label="$t('auth_gh_client_secret_label')"
label="GitHub Client Secret"
>
</q-input>
</div>
@@ -137,7 +135,7 @@
<q-input
filled
v-model="formData.keycloak_discovery_url"
:label="$t('auth_keycloak_label')"
label="Keycloak Discovey URL"
>
</q-input>
</div>
@@ -145,8 +143,8 @@
<q-input
filled
v-model="formData.keycloak_client_id"
:label="$t('auth_keycloak_ci_label')"
:hint="$t('auth_keycloak_ci_hint')"
label="Keycloak Client ID"
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/keycloak/token"
>
</q-input>
</div>
@@ -155,7 +153,7 @@
filled
v-model="formData.keycloak_client_secret"
type="password"
:label="$t('auth_keycloak_cs_label')"
label="Keycloak Client Secret"
>
</q-input>
</div>
@@ -283,37 +281,31 @@
</div>
<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="col-12">
<div class="col-4">
<q-input
filled
v-model="formCallbackUrlRule"
@keydown.enter="addCallbackUrlRule"
type="text"
:label="$t('enter_callback_url_rule')"
:hint="$t('callback_url_rule_hint')"
>
<q-btn
@click="addCallbackUrlRule"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="rule in formData.lnbits_callback_url_rules"
:key="rule"
removable
@remove="removeCallbackUrlRule(rule)"
color="primary"
text-color="white"
:label="rule"
class="ellipsis"
></q-chip>
</div>
<br />
type="number"
v-model.number="formData.lnbits_wallet_limit_max_balance"
:label="$t('wallet_max_ballance')"
></q-input>
</div>
<div class="col-4">
<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-4">
<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>
</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>
+26 -91
View File
@@ -1,144 +1,79 @@
<q-tab-panel name="server">
<q-card-section class="q-pa-none">
<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="col-12 col-md-6">
<p><span v-text="$t('allowed_currencies')"></span></p>
<p>Allowed currencies</p>
<q-select
filled
v-model="formData.lnbits_allowed_currencies"
multiple
:hint="$t('allowed_currencies_hint')"
:label="$t('allowed_currencies')"
hint="Limit the number of available fiat currencies"
label="Allowed currencies"
:options="{{ currencies | safe }}"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('default_account_currency')"></span></p>
<p>Default Accounting Currency</p>
<q-select
filled
v-model="formData.lnbits_default_accounting_currency"
clearable
:hint="$t('default_account_currency_hint')"
:label="$t('currency')"
hint="Default currency for accounting"
label="Currency"
:options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}"
></q-select>
<br />
</div>
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none"><span v-text="$t('payments')"></span></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>
<br />
<h6 class="q-my-none">Service Fee</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('service_fee')"></span></p>
<p>Service Fee</p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee"
:label="$t('service_fee_label')"
label="Service fee (%)"
step="0.1"
:hint="$t('service_fee_hint')"
hint="Fee charged per tx (%)"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('service_fee_max')"></span></p>
<p>Service fee max</p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee_max"
:label="$t('service_fee_max_label')"
:hint="$t('service_fee_max_hint')"
label="Service fee max (sats)"
hint="Max service fee to charge in (sats)"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('fee_wallet')"></span></p>
<p>Fee Wallet</p>
<q-input
filled
v-model="formData.lnbits_service_fee_wallet"
:label="$t('fee_wallet_label')"
:hint="$t('fee_wallet_hint')"
label="Fee wallet (wallet ID)"
hint="Wallet ID to send funds to"
></q-input>
<br />
</div>
<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-section>
<q-item-label v-text="$t('disable_fee')"></q-item-label>
<q-item-label
caption
v-text="$t('disable_fee_internal_desc')"
></q-item-label>
<q-item-label>Disable Fee</q-item-label>
<q-item-label caption
>Disable Service Fee for Internal Lightning
Payments</q-item-label
>
</q-item-section>
<q-item-section avatar>
<q-toggle
+39 -89
View File
@@ -1,72 +1,82 @@
<q-tab-panel name="site_customisation">
<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 />
<div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-5">
<p><span v-text="$t('ui_site_title')"></span></p>
<p>Site Title</p>
<q-input
filled
type="text"
v-model="formData.lnbits_site_title"
:label="$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')"
label="Site title"
></q-input>
<br />
</div>
<div class="col-12 col-md-5">
<p><span v-text="$t('ui_site_tagline')"></span></p>
<p>Site Tagline</p>
<q-input
filled
type="text"
v-model="formData.lnbits_site_tagline"
:label="$t('ui_site_tagline')"
label="Site tagline"
></q-input>
<br />
</div>
<div class="col-12 col-md-2 q-mt-xl">
<q-toggle
:tip="$t('ui_toggle_elements_tip')"
tip="Remove homepage elements like 'runs on' etc"
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>
</div>
</div>
<div>
<p><span v-text="$t('ui_site_description')"></span></p>
<p>Site Description</p>
<q-input
v-model="formData.lnbits_site_description"
filled
type="textarea"
:hint="$t('ui_site_description_hint')"
hint="Use plain text, Markdown, or raw HTML"
/>
</div>
<br />
<div class="row q-col-gutter-md">
<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
filled
type="text"
v-model="formData.lnbits_default_wallet_name"
:label="$t('lnbits_wallet')"
label="LNbits wallet"
></q-input>
</div>
<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
filled
type="text"
v-model="formData.lnbits_qr_logo"
label="https://example.com/image.svg"
:hint="$t('ui_qr_code_logo_hint')"
hint="URL to logo image in QR code"
></q-input>
</div>
</div>
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_badge')"></span></p>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<p>Custom Badge</p>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8">
<q-input
@@ -74,7 +84,7 @@
type="text"
tip="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>
</div>
<div class="col-12 col-md-4">
@@ -82,127 +92,67 @@
filled
v-model="formData.lnbits_custom_badge_color"
:options="colors"
:label="$t('ui_custom_badge_color_label')"
label="Custom badge color"
></q-select>
</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>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('themes')"></span></p>
<p>Themes</p>
<q-select
filled
v-model="formData.lnbits_theme_options"
multiple
:hint="$t('themes_hint')"
hint="Choose themes available for users"
:options="lnbits_theme_options"
:label="$t('themes')"
label="Themes"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('custom_logo')"></span></p>
<p>Custom Logo</p>
<q-input
filled
type="text"
v-model="formData.lnbits_custom_logo"
label="https://example.com/image.png"
:hint="$t('custom_logo_hint')"
hint="URL to logo image"
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('ad_space_title')"></span></p>
<p>Ad Space Title</p>
<q-input
filled
type="text"
v-model="formData.lnbits_ad_space_title"
:label="$t('ad_space_title_label')"
label="Supported by"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('ad_slots')"></span></p>
<p>Advertisement Slots</p>
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_ad_space"
type="text"
:label="$t('ad_slots_label')"
:hint="$t('ad_slots_hint')"
label="url;img_light_url;img_dark_url, url..."
hint="Ad url and image filepaths in CSV format, extensions can choose to honor"
>
</q-input>
<q-toggle
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 />
</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>
</q-card-section>
</q-tab-panel>
+11 -14
View File
@@ -1,19 +1,17 @@
<q-tab-panel name="users">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('user_management')"></span>
</h6>
<h6 class="q-my-none q-mb-sm">User Management</h6>
<div class="row">
<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
filled
v-model="formAddAdmin"
@keydown.enter="addAdminUser"
type="text"
:label="$t('admin_users_label')"
:hint="$t('admin_users_hint')"
label="User ID"
hint="Users with admin privileges"
>
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
</q-input>
@@ -33,14 +31,14 @@
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('allowed_users')"></span></p>
<p>Allowed Users</p>
<q-input
filled
v-model="formAddUser"
@keydown.enter="addAllowedUser"
type="text"
:label="$t('allowed_users_label')"
:hint="$t('allowed_users_hint')"
label="User ID"
hint="Only these users can use LNbits"
>
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
</q-input>
@@ -60,11 +58,10 @@
<br />
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('allow_creation_user')"></q-item-label>
<q-item-label
caption
v-text="$t('allow_creation_user_desc')"
></q-item-label>
<q-item-label>Allow creation of new users</q-item-label>
<q-item-label caption
>Allow creation of new users on the index page</q-item-label
>
</q-item-section>
<q-item-section avatar>
<q-toggle
+57 -80
View File
@@ -1,75 +1,60 @@
{% if not ajax %} {% extends "base.html" %} {% endif %} {% from "macros.jinja"
import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
{% endblock %} {% block page %}
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% 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">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<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>
<q-badge
v-if="checkChanges"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-badge
v-if="checkChanges"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
v-if="isSuperUser"
:label="$t('restart')"
color="primary"
@click="restartServer"
class="q-ml-md"
>
<q-tooltip v-if="needsRestart">
<span v-text="$t('restart_tooltip')"></span>
</q-tooltip>
<q-btn
v-if="isSuperUser"
:label="$t('restart')"
color="primary"
@click="restartServer"
class="q-ml-md"
>
<q-tooltip v-if="needsRestart">
<span v-text="$t('restart_tooltip')"></span>
</q-tooltip>
<q-badge
v-if="needsRestart"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-badge
v-if="needsRestart"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
<q-btn
:label="$t('download_backup')"
flat
@click="downloadBackup"
></q-btn>
<q-btn
flat
v-if="isSuperUser"
:label="$t('reset_defaults')"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip>
<span v-text="$t('reset_defaults_tooltip')"></span>
</q-tooltip>
</q-btn>
</div>
<div></div>
</div>
</div>
</q-card>
<q-btn
flat
v-if="isSuperUser"
:label="$t('reset_defaults')"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip>
<span v-text="$t('reset_defaults_tooltip')"></span>
</q-tooltip>
</q-btn>
</div>
</div>
@@ -149,14 +134,6 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('audit')"></span></q-tooltip
></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
style="word-break: break-all"
name="site_customisation"
@@ -185,8 +162,7 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
"admin/_tab_extensions.html" %} {% include
"admin/_tab_notifications.html" %} {% include
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
%}{% include "admin/_tab_audit.html"%}{% include
"admin/_tab_library.html"%}
%}{% include "admin/_tab_audit.html"%}
</q-tab-panels>
</q-form>
</template>
@@ -197,9 +173,7 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<div class="q-mb-md">
<strong v-text="$t('create_ticker_converter')"></strong>
</div>
<strong>Create Currecny Ticker Converter</strong>
<div class="row">
<div class="col-12 q-mb-md">
<q-select
@@ -237,4 +211,7 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
</div>
</q-card>
</q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
{% endblock %}
+19 -41
View File
@@ -1,63 +1,36 @@
{% 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">
<!-- 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>
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<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">
<q-card class="q-pt-sm">
<strong v-text="$t('components')"></strong>
<strong>Components</strong>
<div style="width: 250px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="componentUseChart"></canvas>
<canvas ref="componentUseChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<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">
<canvas v-if="chartsReady" ref="longDurationChart"></canvas>
<canvas ref="longDurationChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<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">
<canvas v-if="chartsReady" ref="requestMethodChart"></canvas>
<canvas ref="requestMethodChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<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">
<canvas v-if="chartsReady" ref="responseCodeChart"></canvas>
<canvas ref="responseCodeChart"></canvas>
</div>
</q-card>
</div>
@@ -110,6 +83,11 @@
<span v-else v-text="col.label"></span>
</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>
</template>
<template v-slot:body="props">
@@ -124,9 +102,7 @@
class="cursor-pointer q-mr-xs"
@click="showDetailsDialog(props.row)"
>
<q-tooltip
><span v-text="$t('request_details')"></span
></q-tooltip>
<q-tooltip>Request Details</q-tooltip>
</q-btn>
<span v-text="formatDate(props.row.created_at)"></span>
@@ -170,7 +146,7 @@
<q-dialog v-model="auditDetailsDialog.show" position="top">
<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
filled
dense
@@ -198,4 +174,6 @@
</q-card>
</q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/audit.js') }}"></script>
{% endblock %}
+8 -57
View File
@@ -85,12 +85,7 @@
<q-card-section>
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": "<i
v-text="inkeyHidden ? '****************' : wallet.inkey"
></i
>"}</code
><br />
<code>{"X-Api-Key": "<i v-text="wallet.inkey"></i>"}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
@@ -101,8 +96,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i
>"</code
<i v-text="wallet.inkey"></i>"</code
>
</q-card-section>
</q-card>
@@ -118,12 +112,7 @@
<q-card-section>
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": "<i
v-text="inkeyHidden ? '****************' : wallet.inkey"
></i
>"}</code
><br />
<code>{"X-Api-Key": "<i v-text="wallet.inkey"></i>"}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;,
@@ -141,9 +130,8 @@
<code
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
'{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;}' -H
"X-Api-Key:
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
"Content-type: application/json"</code
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
application/json"</code
>
</q-card-section>
</q-card>
@@ -230,12 +218,7 @@
/api/v1/payments/&lt;payment_hash&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": "<i
v-text="inkeyHidden ? '****************' : wallet.inkey"
></i
>"}</code
>
<code>{"X-Api-Key": "{{ wallet.inkey }}"}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
@@ -244,42 +227,10 @@
<code
>curl -X GET
<span v-text="baseUrl"></span>api/v1/payments/&lt;payment_hash&gt; -H
"X-Api-Key:
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
"Content-type: application/json"</code
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
application/json"</code
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<code
><span class="text-pink">WS</span>
/api/v1/ws/&lt;invoice_key&gt;</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": &lt;int&gt;, "payment": &lt;object&gt;}</code>
</q-card-section>
</q-card>
</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>
+44 -401
View File
@@ -1,51 +1,39 @@
{% if not ajax %} {% extends "base.html" %} {% endif %}
{% extends "base.html" %}
<!---->
{% 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>
<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>
{% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/account.js') }}"></script>
{% endblock %} {% block page %}
<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-section>
<div class="row">
<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-panel name="user">
<div v-if="credentialsData.show">
<q-separator></q-separator>
<q-card-section>
<div class="row">
<div class="col">
<h4 class="q-my-none">
<span v-text="$t('password')"></span>
<span v-text="$t('password_config')"></span>
</h4>
</div>
<div class="col">
@@ -102,7 +90,7 @@
unelevated
color="primary"
class="float-right"
:label="$t('update_password')"
:label="$t('change_password')"
>
</q-btn>
</q-card-section>
@@ -110,7 +98,7 @@
<q-card-section>
<div class="col q-mb-sm">
<h4 class="q-my-none">
Nostr <span v-text="$t('pubkey')"></span>
<span v-text="$t('pubkey')"></span>
</h4>
</div>
<q-input
@@ -141,10 +129,11 @@
</q-card-section>
</div>
<div v-else>
<q-card-section v-if="user.extra.picture">
<q-card-section>
<div class="row">
<div class="col">
<q-img
v-if="user.extra.picture"
style="max-width: 100px"
:src="user.extra.picture"
class="float-right"
@@ -152,6 +141,7 @@
</div>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<q-input
@@ -287,7 +277,7 @@
</q-btn>
<q-btn
@click="showUpdateCredentials()"
:label="$t('change_password')"
:label="$t('update_credentials')"
filled
color="primary"
class="float-right"
@@ -302,7 +292,8 @@
flat
round
icon="settings"
to="/admin#site_customisation"
tag="a"
href="/admin#site_customisation"
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
></q-btn>
<div class="row q-mb-md">
@@ -344,7 +335,7 @@
v-if="g.allowedThemes.includes('classic')"
dense
flat
@click="themeChoiceFunc('classic')"
@click="changeColor('classic')"
icon="circle"
color="deep-purple"
size="md"
@@ -354,9 +345,9 @@
v-if="g.allowedThemes.includes('bitcoin')"
dense
flat
@click="themeChoiceFunc('bitcoin')"
@click="changeColor('bitcoin')"
icon="circle"
color="deep-orange"
color="orange"
size="md"
><q-tooltip>bitcoin</q-tooltip>
</q-btn>
@@ -364,7 +355,7 @@
v-if="g.allowedThemes.includes('mint')"
dense
flat
@click="themeChoiceFunc('mint')"
@click="changeColor('mint')"
icon="circle"
color="green"
size="md"
@@ -373,7 +364,7 @@
v-if="g.allowedThemes.includes('autumn')"
dense
flat
@click="themeChoiceFunc('autumn')"
@click="changeColor('autumn')"
icon="circle"
color="brown"
size="md"
@@ -383,7 +374,7 @@
v-if="g.allowedThemes.includes('monochrome')"
dense
flat
@click="themeChoiceFunc('monochrome')"
@click="changeColor('monochrome')"
icon="circle"
color="grey"
size="md"
@@ -393,7 +384,7 @@
v-if="g.allowedThemes.includes('salvador')"
dense
flat
@click="themeChoiceFunc('salvador')"
@click="changeColor('salvador')"
icon="circle"
color="blue-10"
size="md"
@@ -403,7 +394,7 @@
v-if="g.allowedThemes.includes('freedom')"
dense
flat
@click="themeChoiceFunc('freedom')"
@click="changeColor('freedom')"
icon="circle"
color="pink-13"
size="md"
@@ -413,7 +404,7 @@
v-if="g.allowedThemes.includes('cyber')"
dense
flat
@click="themeChoiceFunc('cyber')"
@click="changeColor('cyber')"
icon="circle"
color="light-green-9"
size="md"
@@ -423,7 +414,7 @@
v-if="g.allowedThemes.includes('flamingo')"
dense
flat
@click="themeChoiceFunc('flamingo')"
@click="changeColor('flamingo')"
icon="circle"
color="pink-3"
size="md"
@@ -437,9 +428,9 @@
</div>
<div class="col-8">
<q-input
v-model="bgimageSelection"
v-model="backgroundImage"
:label="$t('background_image')"
@update:model-value="bgimageChoiceFunc"
@update:model-value="backgroundImageFunc"
>
<q-tooltip
><span v-text="$t('background_image')"></span
@@ -493,9 +484,9 @@
</div>
<div class="col-8">
<q-select
v-model="borderSelection"
v-model="borderChoice"
:options="borderOptions"
label="Borders"
label="Reactions"
@update:model-value="applyBorder"
>
<q-tooltip
@@ -505,9 +496,7 @@
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('notifications')"></span>
</div>
<div class="col-4">Notifications</div>
<div class="col-8">
<lnbits-notifications-btn
v-if="g.user"
@@ -521,7 +510,7 @@
</div>
<div class="col-8">
<q-select
v-model="reactionSelection"
v-model="reactionChoice"
:options="reactionOptions"
label="Reactions"
@update:model-value="reactionChoiceFunc"
@@ -533,230 +522,6 @@
</div>
</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>
&nbsp;<code>Authorization</code>
&nbsp;
</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 &nbsp;</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-panels>
</div>
</div>
@@ -771,126 +536,4 @@
</q-card>
</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 %}
+687 -25
View File
@@ -1,14 +1,10 @@
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user, wallet, extensions, extension_data)
}}{% endblock %} {% block page %}
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {{ window_vars(user, extensions) }}{% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div>
<div class="q-pa-xs">
<div class="q-gutter-y-md">
<q-tabs
:model-value="tab"
@@ -58,7 +54,8 @@
flat
round
icon="settings"
to="/admin#extensions"
tag="a"
href="/admin#extensions"
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
></q-btn>
</q-tabs>
@@ -69,7 +66,6 @@
</div>
<div class="row q-col-gutter-md">
<div
v-if="filteredExtensions"
class="col-12 col-sm-6 col-md-6 col-lg-4"
v-for="extension in filteredExtensions"
:key="extension.id + extension.hash"
@@ -118,7 +114,6 @@
></q-tooltip>
</q-badge>
<div
v-if="extension.name"
class="text-h5"
style="cursor: pointer"
@click="showExtensionDetails(extension.id, extension.details_link)"
@@ -191,7 +186,7 @@
<div class="col-10">
<div v-if="!extension.inProgress">
<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
color="primary"
type="a"
@@ -199,20 +194,20 @@
:label="$t('open')"
></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
color="grey-5"
@click="disableExtension(extension)"
:label="$t('disable')"
></q-btn>
<q-badge
v-if="extension.isAdminOnly && !g.user.admin"
v-if="extension.isAdminOnly && !user.admin"
v-text="$t('admin_only')"
>
</q-badge>
<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
color="primary"
@click="enableExtensionForUser(extension)"
@@ -235,7 +230,7 @@
</q-btn>
</div>
<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>
@@ -337,7 +332,7 @@
</a>
</div>
<div v-else>
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
<q-spinner color="primary" size="2.55em"></q-spinner>
</div>
</q-card-section>
@@ -436,10 +431,7 @@
:header-class="getReleaseIconColor(release)"
>
<div v-if="release.inProgress">
<q-spinner-bars
color="primary"
size="2.55em"
></q-spinner-bars>
<q-spinner color="primary" size="2.55em"></q-spinner>
</div>
<div v-else-if="release.error">
<q-icon
@@ -450,7 +442,7 @@
></q-icon>
<span v-text="$t('release_details_error')"></span>
</div>
<q-card class="no-border" v-else>
<q-card v-else>
<q-card-section v-if="release.is_version_compatible">
<span
v-if="release.requiresPayment && !release.paid_sats"
@@ -625,7 +617,7 @@
></q-btn>
</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
v-if="selectedExtension"
v-show="manageExtensionTab === 'sell'"
@@ -711,7 +703,7 @@
</a>
</div>
<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 class="row q-mt-lg">
@@ -959,12 +951,12 @@
value="false"
:label="item.name + ` (v${item.latestRelease?.version})`"
>
<q-spinner-bars
<q-spinner
v-if="item.inProgress"
color="primary"
size="1.5em"
class="q-ml-md"
></q-spinner-bars>
></q-spinner>
</q-checkbox>
</div>
</div>
@@ -988,4 +980,674 @@
</div>
</q-card>
</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 %}
+133 -144
View File
@@ -1,150 +1,139 @@
{% extends "public.html" %} {% block page_container %}
<q-page-container>
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
{% block page %}
<div class="row q-col-gutter-md justify-center main">
<div class="col-10 col-md-8 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section class="grid">
<div>
<h6 class="q-my-none text-center">
<strong v-text="$t('welcome_lnbits')"></strong>
<p><span v-text="$t('setup_su_account')"></span></p>
</h6>
<br />
<q-form class="q-gutter-md">
<q-input
filled
v-model="loginData.username"
:label="$t('username')"
></q-input>
<q-input
filled
v-model.trim="loginData.password"
:type="loginData.isPwd ? 'password' : 'text'"
autocomplete="off"
:label="$t('password')"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
><template v-slot:append>
<q-icon
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwd = !loginData.isPwd"
/> </template
></q-input>
<q-input
filled
v-model.trim="loginData.passwordRepeat"
:type="loginData.isPwdRepeat ? 'password' : 'text'"
autocomplete="off"
:label="$t('password_repeat')"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || $t('invalid_password_repeat')]"
><template v-slot:append>
<q-icon
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
/> </template
></q-input>
<q-btn
@click="setPassword()"
unelevated
color="primary"
:label="$t('login')"
:disable="checkPasswordsMatch || !loginData.username || !loginData.password || !loginData.passwordRepeat"
></q-btn>
</q-form>
</div>
<div class="hero-wrapper">
<div class="hero q-mx-auto"></div>
</div>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<style>
main {
display: flex;
flex-direction: column;
justify-content: center;
}
.grid {
display: block;
}
.hero-wrapper {
display: none;
}
.hero {
display: block;
height: 100%;
max-width: 250px;
background-image: url("{{ static_url_for('static', 'images/logos/lnbits.svg') }}");
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center main">
<div class="col-10 col-md-8 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section class="grid">
<div>
<h6 class="q-my-none text-center">
<strong>Welcome to LNbits</strong>
<p>Set up the Superuser account below.</p>
</h6>
<br />
<q-form class="q-gutter-md">
<q-input
filled
v-model="loginData.username"
:label="$t('username')"
></q-input>
<q-input
filled
v-model.trim="loginData.password"
:type="loginData.isPwd ? 'password' : 'text'"
autocomplete="off"
:label="$t('password')"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
><template v-slot:append>
<q-icon
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwd = !loginData.isPwd"
/> </template
></q-input>
<q-input
filled
v-model.trim="loginData.passwordRepeat"
:type="loginData.isPwdRepeat ? 'password' : 'text'"
autocomplete="off"
:label="$t('password_repeat')"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || 'Passwords_dont_match']"
><template v-slot:append>
<q-icon
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
/> </template
></q-input>
<q-btn
@click="setPassword()"
unelevated
color="primary"
:label="$t('login')"
:disable="checkPasswordsMatch || !loginData.username || !loginData.password || !loginData.passwordRepeat"
></q-btn>
</q-form>
</div>
<div class="hero-wrapper">
<div class="hero q-mx-auto"></div>
</div>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<style>
main {
display: flex;
flex-direction: column;
justify-content: center;
}
.grid {
display: block;
}
.hero-wrapper {
display: none;
}
.hero {
display: block;
height: 100%;
max-width: 250px;
background-image: url("{{ static_url_for('static', 'images/logos/lnbits.svg') }}");
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
@media (min-width: 992px) {
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
.hero-wrapper {
display: block;
position: relative;
height: 100%;
padding: 1rem;
@media (min-width: 992px) {
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
.hero-wrapper {
display: block;
position: relative;
height: 100%;
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>
window.app = Vue.createApp({
el: '#vue',
mixins: [window.windowMixin],
data: function () {
return {
loginData: {
isPwd: true,
isPwdRepeat: true,
username: '',
password: '',
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
}
)
},
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'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
}
window.location.href = '/admin'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
})
</script>
{% endblock %}
</q-page>
</q-page-container>
}
}
})
</script>
{% endblock %}
File diff suppressed because it is too large Load Diff
+169 -458
View File
@@ -1,11 +1,16 @@
{% if not ajax %} {% extends "base.html" %} {% endif %}
{% extends "base.html" %}
<!---->
{% 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">
{% if HIDE_API and AD_SPACE_ENABLED and AD_SPACE %}
{% if HIDE_API and AD_SPACE %}
<div class="col-12 col-md-8 q-gutter-y-md">
{% elif HIDE_API %}
<div class="col-12 q-gutter-y-md">
@@ -20,51 +25,44 @@
} : ''"
>
{% endif %}
<div class="row">
<div class="col-lg-9 col-12">
<q-card
:style="$q.screen.lt.md ? {
background: $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': ''
} : ''"
>
<q-card-section style="height: 130px">
<div class="row q-gutter-md">
<div
v-if="isSatsDenomination"
class="col-1"
style="max-width: 30px"
>
<q-card-section>
<div class="row">
<div class="col-1" style="max-width: 30px">
<q-btn
v-if="g.fiatTracking"
v-if="update.currency"
@click="swapBalancePriority"
style="height: 50px"
class="q-mt-lg"
color="primary"
flat
dense
icon="swap_vert"
></q-btn>
</div>
<div class="col">
<div class="col-11">
<div
class="column"
:class="{
'q-pt-sm': g.fiatTracking,
'q-pt-lg': !g.fiatTracking
}"
v-if="!isFiatPriority || !g.fiatTracking"
style="height: 100px"
'q-pt-sm': update.currency,
'q-pt-lg': !update.currency
}"
v-if="!isPrioritySwapped"
style="height: 90px"
>
<div class="col-7">
<div class="row">
<div class="col-auto">
<div class="text-h3 q-my-none full-width">
<strong
v-text="walletFormatBalance(this.g.wallet.sat)"
class="text-no-wrap"
:style="{fontSize: 'clamp(0.75rem, 10vw, 3rem)', display: 'inline-block', maxWidth: '100%'}"
></strong>
<div class="text-h3 q-my-none text-no-wrap">
<strong v-text="formattedBalance"></strong>
<small> {{LNBITS_DENOMINATION}}</small>
</div>
</div>
<div class="col-auto">
@@ -78,7 +76,7 @@
</div>
</div>
<div class="col-2">
<div v-if="g.fiatTracking">
<div v-if="update.currency">
<span
class="text-h5 text-italic"
v-text="formattedFiatAmount"
@@ -90,18 +88,18 @@
<div
class="column"
v-if="isFiatPriority && g.fiatTracking"
v-if="isPrioritySwapped"
:class="{
'q-pt-sm': g.fiatTracking,
'q-pt-lg': !g.fiatTracking
}"
'q-pt-sm': update.currency,
'q-pt-lg': !update.currency
}"
style="height: 100px"
>
<div class="col-7">
<div class="row">
<div class="col-auto">
<div
v-if="g.fiatTracking"
v-if="update.currency"
class="text-h3 q-my-none text-no-wrap"
>
<strong v-text="formattedFiatAmount"></strong>
@@ -129,7 +127,7 @@
<div
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>
<span
@@ -139,7 +137,7 @@
</div>
<q-btn
v-if="$q.screen.lt.md"
@click="simpleMobile()"
@click="mobileSimple = !mobileSimple"
color="primary"
class="q-ml-xl absolute-right"
dense
@@ -168,12 +166,7 @@
@click="showReceiveDialog"
:label="$t('create_invoice')"
></q-btn>
<q-btn
unelevated
color="secondary"
icon="qr_code_scanner"
@click="showCamera"
>
<q-btn unelevated color="secondary" icon="qr_code_scanner">
<q-tooltip
><span v-text="$t('camera_tooltip')"></span
></q-tooltip>
@@ -187,14 +180,30 @@
</div>
</div>
</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
:style="
$q.screen.lt.md
? {
background: $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' : ''
}
: ''
@@ -202,47 +211,23 @@
>
<q-card-section>
<payment-list
@filter-changed="handleFilterChange"
:update="updatePayments"
:wallet="this.g.wallet"
:mobile-simple="mobileSimple"
:expand-details="expandDetails"
></payment-list>
</q-card-section>
</q-card>
<div id="hiddenQrCodeContainer" style="display: none">
<lnbits-qrcode
:value="'lightning:' + this.receive.paymentReq"
></lnbits-qrcode>
</div>
</div>
{% if HIDE_API %}
<div class="col-12 col-md-4 q-gutter-y-md">
{% 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-section class="q-pb-xs">
<div class="row items-center">
<q-avatar
size="lg"
:icon="g.wallet.extra.icon"
: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>
</div>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
{{ SITE_TITLE }} Wallet:
<strong><em>{{wallet_name}}</em></strong>
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
@@ -269,196 +254,102 @@
{% endif %}
<q-expansion-item
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
group="extras"
icon="phone_android"
:label="$t('access_wallet_on_mobile')"
icon="settings_cell"
: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-section>
You can connect to this wallet from a mobile app:
<ul>
<li>
Download
<a class="text-secondary" href="https://zeusln.app"
>Zeus</a
>
or
<a
class="text-secondary"
href="https://bluewallet.io/"
>BlueWallet</a
>
from App Store or Google Play
</li>
<li>
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>
<div class="" style="max-width: 320px">
<q-input
filled
v-model.trim="update.name"
label="Name"
dense
/>
</div>
<q-btn
:disable="!update.name.length"
unelevated
class="q-mt-sm"
color="primary"
:label="$t('update_name')"
@click="updateWallet({ name: update.name })"
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="extras"
icon="settings"
:label="$t('wallet_config')"
icon="attach_money"
:label="$t('fiat_tracking')"
>
<q-card>
<q-card-section>
<div class="row">
<div class="col-6">
<q-input
filled
v-model.trim="update.name"
label="Name"
dense
/>
<div style="max-width: 360px">
<div class="row">
<div class="col-auto">
<q-toggle
v-model="fiatTracking"
color="primary"
@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 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>
</q-card-section>
</q-card>
</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-checkbox
dense
@click="saveChartsPreferences"
v-model="chartConfig.showBalanceInOut"
:label="$t('payments_balance_in_out_chart')"
>
</q-checkbox>
</div>
<div class="col-md-4 col-sm-12">
<q-checkbox
dense
@click="saveChartsPreferences"
v-model="chartConfig.showPaymentCountInOut"
:label="$t('payments_count_in_out_chart')"
>
</q-checkbox>
</div>
</div>
<q-separator></q-separator>
<q-expansion-item
group="extras"
icon="remove_circle"
:label="$t('delete_wallet')"
>
<q-card>
<q-card-section>
<p v-text="$t('delete_wallet_desc')"></p>
<q-btn
unelevated
color="red-10"
@click="deleteWallet()"
:label="$t('delete_wallet')"
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -468,7 +359,7 @@
</q-list>
</q-card-section>
</q-card>
{% endif %} {% if AD_SPACE_ENABLED and AD_SPACE %}
{% endif %} {% if AD_SPACE %}
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
@@ -482,6 +373,7 @@
style="display: inline-block"
href="{{ AD[0] }}"
class="q-ml-md q-mb-xs q-mr-md"
style="max-width: 80%"
>
<img
style="max-width: 100%; height: auto"
@@ -497,84 +389,9 @@
>{% endfor %}
</q-card>
{% 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>
<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
v-model="receive.show"
position="top"
@@ -592,7 +409,7 @@
<q-input
filled
dense
v-model="receive.data.amount"
v-model.number="receive.data.amount"
:label="$t('amount')+' ({{LNBITS_DENOMINATION}}) *'"
mask="#.##"
fill-mask="0"
@@ -639,7 +456,7 @@
>
<span
v-if="receive.lnurl"
v-text="`${$t('withdraw_from')} ${receive.lnurl.domain}`"
v-text="$t('withdraw_from') + receive.lnurl.domain"
></span>
<span v-else v-text="$t('create_invoice')"></span>
</q-btn>
@@ -651,11 +468,11 @@
:label="$t('cancel')"
></q-btn>
</div>
<q-spinner-bars
<q-spinner
v-if="receive.status == 'loading'"
color="primary"
size="2.55em"
></q-spinner-bars>
></q-spinner>
</q-form>
</q-card>
<q-card
@@ -664,7 +481,9 @@
>
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<div v-html="invoiceQrCode"></div>
<lnbits-qrcode
:value="'lightning:' + receive.paymentReq.toUpperCase()"
></lnbits-qrcode>
</a>
</div>
<div class="text-center">
@@ -680,13 +499,9 @@
color="positive"
text-color="white"
></q-avatar>
<span v-text="$t('nfc_supported')"></span>
NFC supported
</q-chip>
<span
v-else
class="text-caption text-grey"
v-text="$t('nfc_not_supported')"
></span>
<span v-else class="text-caption text-grey">NFC not supported</span>
</div>
<div class="row q-mt-lg">
<q-btn
@@ -709,131 +524,27 @@
<q-dialog v-model="parse.show" @hide="closeParseDialog" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice">
<div class="column content-center text-center q-mb-md">
<div v-if="!isFiatPriority">
<h4 class="q-my-none text-bold">
<span v-text="walletFormatBalance(parse.invoice.sat)"></span>
</h4>
</div>
<div v-else>
<h4
class="q-my-none text-bold"
v-text="parse.invoice.fiatAmount"
></h4>
</div>
<div class="q-my-md absolute">
<q-btn
v-if="g.fiatTracking"
@click="swapBalancePriority"
flat
dense
icon="swap_vert"
color="primary"
></q-btn>
</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>
<h6
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
class="q-my-none"
v-text="parseFloat(String(parse.invoice.fsat).replaceAll(',', '')) / 100 + '{{LNBITS_DENOMINATION}}'"
></h6>
<h6
v-else
class="q-my-none"
v-text="parse.invoice.fsat + '{{LNBITS_DENOMINATION}}'"
></h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong v-text="$t('memo') + ': '"></strong>
<span v-text="parse.invoice.description"></span>
<br />
<strong>Expire date: </strong>
<span v-text="parse.invoice.expireDate"></span>
<br />
<strong>Hash: </strong>
<span v-text="parse.invoice.hash"></span>
</p>
<div v-if="canPay" class="row q-mt-lg">
<q-btn
unelevated
@@ -961,8 +672,8 @@
dense
v-model.number="parse.data.amount"
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
:mask="parse.data.unit == 'sat' ? '#' : ''"
:step="parse.data.unit == 'sat' ? '1': '0.01'"
:mask="parse.data.unit != 'sat' ? '#.##' : '#'"
:step="parse.data.unit != 'sat' ? '0.01' : '1'"
fill-mask="0"
reverse-fill-mask
:min="parse.lnurlpay.minSendable / 1000"
+5 -76
View File
@@ -29,46 +29,7 @@
</q-card>
</q-dialog>
<q-dialog v-model="setFeeDialog.show">
<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-dialog v-model="openChannelDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<q-input
@@ -210,41 +171,10 @@
<q-tr :props="props">
<div class="q-pb-sm">
<div class="row items-center q-gutter-sm">
<div class="text-subtitle1" v-text="props.row.name"></div>
<div class="text-caption" v-if="props.row.peer_id">
<span>Peer ID</span>
<q-btn
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-subtitle1 col-grow"
v-text="props.row.name"
></div>
<div class="text-caption" v-if="props.row.short_id">
<span v-text="props.row.short_id"></span>
<q-btn
@@ -271,7 +201,6 @@
color="pink"
></q-btn>
</div>
<lnbits-channel-balance
:balance="props.row.balance"
:color="props.row.color"
+334 -5
View File
@@ -1,8 +1,5 @@
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<q-dialog v-model="nodeInfoDialog.show" position="top">
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
@@ -43,4 +40,336 @@
</q-card>
</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 %}
-390
View File
@@ -1,390 +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-select
v-else-if="['status'].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>
<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 %}
@@ -35,7 +35,6 @@
<q-table :rows="wallets" :columns="walletTable.columns">
<template v-slot:header="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
@@ -48,14 +47,12 @@
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width v-if="g.user.super_user">
<q-td auto-width>
<lnbits-update-balance
:wallet_id="props.row.id"
@credit-value="handleBalanceUpdate"
class="q-mr-md"
></lnbits-update-balance>
</q-td>
<q-td auto-width>
<q-btn
round
icon="menu"
+12 -45
View File
@@ -1,8 +1,5 @@
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col">
@@ -14,35 +11,13 @@
{%include "users/_createWalletDialog.html" %}
</div>
<div v-else>
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<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-btn
@click="showAccountPage()"
:label="$t('create_account')"
color="primary"
class="q-mb-md"
>
</q-btn>
<q-card class="q-pa-md">
<q-table
@@ -127,17 +102,7 @@
:label="props.row.wallet_count"
@click="fetchWallets(props.row.id)"
>
</q-btn>
<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-tooltip>Show Wallets</q-tooltip>
</q-btn>
</q-td>
@@ -183,4 +148,6 @@
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/users.js') }}"></script>
{% endblock %}
+11 -130
View File
@@ -1,38 +1,28 @@
import os
import time
from http import HTTPStatus
from pathlib import Path
from shutil import make_archive, move
from shutil import make_archive
from subprocess import Popen
from tempfile import NamedTemporaryFile
from typing import IO, Optional
from typing import Optional
from urllib.parse import urlparse
import filetype
from fastapi import APIRouter, Depends, File, Header, HTTPException, UploadFile
from fastapi import APIRouter, Depends
from fastapi.responses import FileResponse
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 (
enqueue_notification,
get_balance_delta,
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.helpers import safe_upload_file_path
from lnbits.server import server_restart
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
from lnbits.tasks import invoice_listeners
from .. import core_app_extra
from ..crud import get_admin_settings, reset_core_settings, update_admin_settings
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
admin_router = APIRouter(tags=["Admin UI"], prefix="/admin")
file_upload = File(...)
@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])
async def api_get_settings(
user: User = Depends(check_admin),
@@ -82,7 +60,6 @@ async def api_get_settings(
status_code=HTTPStatus.OK,
)
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)
admin_settings = await get_admin_settings(user.super_user)
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"}
@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(
"/api/v1/settings/default",
status_code=HTTPStatus.OK,
@@ -110,10 +78,13 @@ async def api_reset_settings(field_name: str):
return {"default_value": getattr(default_settings, field_name)}
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
enqueue_notification(NotificationType.settings_update, {"username": user.username})
await reset_core_settings()
@admin_router.delete(
"/api/v1/settings",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_super_user)],
)
async def api_delete_settings() -> None:
await delete_admin_settings()
server_restart.set()
@@ -165,93 +136,3 @@ async def api_download_backup() -> FileResponse:
return FileResponse(
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")
+9 -7
View File
@@ -15,6 +15,7 @@ from fastapi import (
from fastapi.exceptions import HTTPException
from fastapi.responses import StreamingResponse
from lnbits.core.crud import get_user
from lnbits.core.models import (
BaseWallet,
ConversionData,
@@ -29,7 +30,6 @@ from lnbits.decorators import (
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import check_callback_url
from lnbits.lnurl import decode as lnurl_decode
from lnbits.settings import settings
from lnbits.utils.exchange_rates import (
@@ -50,18 +50,21 @@ api_router = APIRouter(tags=["Core"])
async def health() -> dict:
return {
"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)
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] = {
"server_time": int(time()),
"up_time": settings.lnbits_server_up_time,
"up_time_seconds": int(time() - settings.server_startup_time),
"up_time": int(time() - settings.server_startup_time),
}
user = await get_user(wallet.wallet.user)
if not user:
return stat
stat["version"] = settings.version
if not user.admin:
return stat
@@ -129,7 +132,6 @@ async def api_lnurlscan(
else:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
check_callback_url(url)
r = await client.get(url, timeout=5)
r.raise_for_status()
if r.is_error:
@@ -225,7 +227,7 @@ async def api_perform_lnurlauth(
@api_router.get(
"/api/v1/rate/history",
dependencies=[Depends(check_user_exists)],
dependencies=[Depends(require_invoice_key)],
)
async def api_exchange_rate_history() -> list[dict]:
return settings.lnbits_exchange_rate_history
+18 -170
View File
@@ -11,26 +11,12 @@ from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_sso.sso.base import OpenID, SSOBase
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.decorators import access_token_payload, check_user_exists
from lnbits.helpers import (
create_access_token,
decrypt_internal_message,
encrypt_internal_message,
get_api_routes,
is_valid_email_address,
is_valid_username,
)
@@ -58,7 +44,6 @@ from ..models import (
UpdateUserPassword,
UpdateUserPubkey,
User,
UserAcls,
UserExtra,
)
@@ -74,7 +59,7 @@ async def get_auth_user(user: User = Depends(check_user_exists)) -> User:
async def login(data: LoginUsernamePassword) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException(
HTTPStatus.FORBIDDEN, "Login by 'Username and Password' not allowed."
HTTPStatus.UNAUTHORIZED, "Login by 'Username and Password' not allowed."
)
account = await get_account_by_username_or_email(data.username)
if not account or not account.verify_password(data.password):
@@ -85,7 +70,9 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
@auth_router.post("/nostr", description="Login via Nostr")
async def nostr_login(request: Request) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
raise HTTPException(
HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed."
)
event = _nostr_nip98_event(request)
account = await get_account_by_pubkey(event["pubkey"])
if not account:
@@ -102,137 +89,15 @@ async def nostr_login(request: Request) -> JSONResponse:
async def login_usr(data: LoginUsr) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
raise HTTPException(
HTTPStatus.FORBIDDEN,
HTTPStatus.UNAUTHORIZED,
"Login by 'User ID' not allowed.",
)
account = await get_account(data.usr)
if not account:
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)
@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")
async def login_with_sso_provider(
request: Request, provider: str, user_id: Optional[str] = None
@@ -240,7 +105,7 @@ async def login_with_sso_provider(
provider_sso = _new_sso(provider)
if not provider_sso:
raise HTTPException(
HTTPStatus.FORBIDDEN,
HTTPStatus.UNAUTHORIZED,
f"Login by '{provider}' not allowed.",
)
@@ -255,7 +120,7 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons
provider_sso = _new_sso(provider)
if not provider_sso:
raise HTTPException(
HTTPStatus.FORBIDDEN,
HTTPStatus.UNAUTHORIZED,
f"Login by '{provider}' not allowed.",
)
@@ -283,7 +148,7 @@ async def logout() -> JSONResponse:
async def register(data: RegisterUser) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException(
HTTPStatus.FORBIDDEN,
HTTPStatus.UNAUTHORIZED,
"Register by 'Username and Password' not allowed.",
)
@@ -373,7 +238,7 @@ async def update_password(
async def reset_password(data: ResetUserPassword) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException(
HTTPStatus.FORBIDDEN, "Auth by 'Username and Password' not allowed."
HTTPStatus.UNAUTHORIZED, "Auth by 'Username and Password' not allowed."
)
assert data.password == data.password_repeat, "Passwords do not match."
@@ -447,7 +312,7 @@ async def update(
@auth_router.put("/first_install")
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
if not settings.first_install:
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "This is not your first install")
account = await get_account(settings.super_user)
if not account:
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
@@ -470,10 +335,10 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
if verified_user_id:
if account:
raise HTTPException(HTTPStatus.FORBIDDEN, "Email already used.")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Email already used.")
account = await get_account(verified_user_id)
if not account:
raise HTTPException(HTTPStatus.FORBIDDEN, "Cannot verify user email.")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Cannot verify user email.")
redirect_path = "/account"
if account:
@@ -497,37 +362,20 @@ def _auth_success_response(
sub=username or "", usr=user_id, email=email, auth_time=int(time())
)
access_token = create_access_token(data=payload.dict())
max_age = settings.auth_token_expire_minutes * 60
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
response.set_cookie(
"cookie_access_token", access_token, httponly=True, max_age=max_age
)
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
response.set_cookie("cookie_access_token", access_token, httponly=True)
response.set_cookie("is_lnbits_user_authorized", "true")
response.delete_cookie("is_access_token_expired")
return response
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:
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
access_token = create_access_token(data=payload.dict())
max_age = settings.auth_token_expire_minutes * 60
response = RedirectResponse(path)
response.set_cookie(
"cookie_access_token", access_token, httponly=True, max_age=max_age
)
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
response.set_cookie("cookie_access_token", access_token, httponly=True)
response.set_cookie("is_lnbits_user_authorized", "true")
response.delete_cookie("is_access_token_expired")
return response
@@ -578,10 +426,10 @@ def _find_auth_provider_class(provider: str) -> Callable:
def _nostr_nip98_event(request: Request) -> dict:
auth_header = request.headers.get("Authorization")
if not auth_header:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr Auth header missing.")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Nostr Auth header missing.")
scheme, token = auth_header.split()
if scheme.lower() != "nostr":
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid Authorization scheme.")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid Authorization scheme.")
event = None
try:
event_json = base64.b64decode(token.encode("ascii"))
+34 -51
View File
@@ -1,4 +1,5 @@
from http import HTTPStatus
from pathlib import Path
from typing import Annotated, List, Optional, Union
from urllib.parse import urlencode, urlparse
@@ -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.extensions import get_valid_extensions
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.wallets import get_funding_source
@@ -36,7 +37,7 @@ generic_router = APIRouter(
@generic_router.get("/favicon.ico", response_class=FileResponse)
async def favicon():
return RedirectResponse(settings.lnbits_qr_logo)
return FileResponse(Path("lnbits", "static", "favicon.ico"))
@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)
async def first_install(request: Request):
if not settings.first_install:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Super user account has already been configured.",
return template_renderer().TemplateResponse(
request,
"error.html",
{
"err": "Super user account has already been configured.",
},
)
return template_renderer().TemplateResponse(
request,
@@ -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)]
db_versions = await get_db_versions()
extension_data = [
extensions = [
{
"id": ext.id,
"name": ext.name,
@@ -148,8 +152,7 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
"core/extensions.html",
{
"user": user.json(),
"extension_data": extension_data,
"ajax": _is_ajax_request(request),
"extensions": extensions,
},
)
@@ -176,25 +179,25 @@ async def wallet(
wallet = user.wallets[0]
if not wallet or wallet.deleted:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Wallet not found",
return template_renderer().TemplateResponse(
request, "error.html", {"err": "Wallet not found"}, HTTPStatus.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,
"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(
@@ -211,7 +214,6 @@ async def account(
"core/account.html",
{
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@@ -325,7 +327,6 @@ async def node(request: Request, user: User = Depends(check_admin)):
"settings": settings.dict(),
"balance": balance,
"wallets": user.wallets[0].json(),
"ajax": _is_ajax_request(request),
},
)
@@ -364,7 +365,6 @@ async def admin_index(request: Request, user: User = Depends(check_admin)):
"settings": settings.dict(),
"balance": balance,
"currencies": list(currencies.keys()),
"ajax": _is_ajax_request(request),
},
)
@@ -381,7 +381,6 @@ async def users_index(request: Request, user: User = Depends(check_admin)):
"user": user.json(),
"settings": settings.dict(),
"currencies": list(currencies.keys()),
"ajax": _is_ajax_request(request),
},
)
@@ -396,19 +395,6 @@ async def audit_index(request: Request, user: User = Depends(check_admin)):
{
"request": request,
"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_admin)):
return template_renderer().TemplateResponse(
"payments/index.html",
{
"request": request,
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@@ -425,22 +411,23 @@ async def hex_to_uuid4(hex_value: str):
@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
their smartphone camera, or a QR scanner app, they can follow the link provided to
claim their satoshis and get an instant LNbits wallet! lnbits/withdraw docs
"""
if not lightning:
lightning_param = request.query_params.get("lightning")
if not lightning_param:
return {"status": "ERROR", "reason": "lightning parameter not provided."}
if not settings.lnbits_allow_new_accounts:
return {"status": "ERROR", "reason": "New accounts are not allowed."}
lnurl = lnurl_decode(lightning)
lnurl = lnurl_decode(lightning_param)
async with httpx.AsyncClient() as client:
check_callback_url(lnurl)
res1 = await client.get(lnurl, timeout=2)
res1.raise_for_status()
data1 = res1.json()
@@ -455,23 +442,19 @@ async def lnurlwallet(request: Request, lightning: str = ""):
detail="Invalid lnurl. Expected maxWithdrawable",
)
account = await create_user_account()
wallet = account.wallets[0]
payment = await create_invoice(
wallet = await create_wallet(user_id=account.id)
_, payment_request = await create_invoice(
wallet_id=wallet.id,
amount=data1.get("maxWithdrawable") / 1000,
memo=data1.get("defaultDescription", "lnurl wallet withdraw"),
)
url = data1.get("callback")
params = {"k1": data1.get("k1"), "pr": payment.bolt11}
params = {"k1": data1.get("k1"), "pr": payment_request}
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()
return RedirectResponse(
f"/wallet?usr={account.id}&wal={wallet.id}",
)
def _is_ajax_request(request: Request):
return request.headers.get("X-Requested-With", None) == "XMLHttpRequest"
+3 -21
View File
@@ -95,14 +95,6 @@ async def api_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)
async def api_create_channel(
node: Node = Depends(require_node),
@@ -133,17 +125,7 @@ async def api_delete_channel(
)
@super_node_router.put("/channels/{channel_id}")
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")
@node_router.get("/payments", response_model=Page[NodePayment])
async def api_get_payments(
node: Node = Depends(require_node),
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
@@ -156,7 +138,7 @@ async def api_get_payments(
return await node.get_payments(filters)
@node_router.get("/invoices")
@node_router.get("/invoices", response_model=Page[NodeInvoice])
async def api_get_invoices(
node: Node = Depends(require_node),
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
@@ -169,7 +151,7 @@ async def api_get_invoices(
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]:
return await node.get_peers()
+5 -114
View File
@@ -1,5 +1,4 @@
import json
import ssl
from http import HTTPStatus
from math import ceil
from typing import List, Optional
@@ -17,10 +16,6 @@ from fastapi.responses import JSONResponse
from loguru import logger
from lnbits import bolt11
from lnbits.core.crud.payments import (
get_payment_count_stats,
get_wallets_stats,
)
from lnbits.core.models import (
CreateInvoice,
CreateLnurl,
@@ -28,33 +23,18 @@ from lnbits.core.models import (
KeyType,
PayLnurlWData,
Payment,
PaymentCountField,
PaymentCountStat,
PaymentDailyStats,
PaymentFilters,
PaymentHistoryPoint,
PaymentWalletStats,
Wallet,
)
from lnbits.core.models.users import User
from lnbits.core.services.payments import (
get_payments_daily_stats,
update_pending_payment,
)
from lnbits.db import Filters, Page
from lnbits.decorators import (
WalletTypeInfo,
check_admin,
check_user_exists,
parse_filters,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import (
check_callback_url,
filter_dict_keys,
generate_filter_params_openapi,
)
from lnbits.helpers import filter_dict_keys, generate_filter_params_openapi
from lnbits.lnurl import decode as lnurl_decode
from lnbits.settings import settings
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
@@ -113,64 +93,6 @@ async def api_payments_history(
return await get_payments_history(key_info.wallet.id, group, filters)
@payment_router.get(
"/stats/count",
name="Get payments history for all users",
dependencies=[Depends(check_admin)],
response_model=List[PaymentCountStat],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_counting_stats(
count_by: PaymentCountField = Query("tag"),
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
):
return await get_payment_count_stats(count_by, filters)
@payment_router.get(
"/stats/wallets",
name="Get payments history for all users",
dependencies=[Depends(check_admin)],
response_model=List[PaymentWalletStats],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_wallets_stats(
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
):
return await get_wallets_stats(filters)
@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 not user.admin:
exc = HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Missing wallet id.",
)
wallet_filter = next(
(f for f in filters.filters if f.field == "wallet_id"), None
)
if not wallet_filter:
raise exc
wallet_id = list((wallet_filter.values or {}).values())
if len(wallet_id) == 0:
raise exc
if not user.get_wallet(wallet_id[0]):
raise exc
return await get_payments_daily_stats(filters)
@payment_router.get(
"/paginated",
name="Payment List",
@@ -183,14 +105,11 @@ async def api_payments_paginated(
key_info: WalletTypeInfo = Depends(require_invoice_key),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
await update_pending_payments(key_info.wallet.id)
page = await get_payments_paginated(
wallet_id=key_info.wallet.id,
filters=filters,
)
for payment in page.data:
if payment.pending:
await update_pending_payment(payment)
return page
@@ -236,7 +155,6 @@ async def _api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
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},
@@ -257,31 +175,6 @@ async def _api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
return payment
@payment_router.get(
"/all/paginated",
name="Payment List",
summary="get paginated list of payments",
response_description="list of payments",
response_model=Page[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_all_payments_paginated(
filters: Filters = Depends(parse_filters(PaymentFilters)),
user: User = Depends(check_user_exists),
):
if user.admin:
# admin user can see payments from all wallets
for_user_id = None
else:
# regular user can only see payments from their wallets
for_user_id = user.id
return await get_payments_paginated(
filters=filters,
user_id=for_user_id,
)
@payment_router.post(
"",
summary="Create or pay an invoice",
@@ -322,7 +215,7 @@ async def api_payments_create(
return await _api_payments_create_invoice(invoice_data, wallet.wallet)
else:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invoice (or Admin) key required.",
)
@@ -357,7 +250,6 @@ async def api_payments_pay_lnurl(
amount_msat = ceil(amount_msat // 1000) * 1000
else:
amount_msat = data.amount
check_callback_url(data.callback)
r = await client.get(
data.callback,
params={"amount": amount_msat, "comment": data.comment},
@@ -366,8 +258,7 @@ async def api_payments_pay_lnurl(
if r.is_error:
raise httpx.ConnectError("LNURL callback connection error")
r.raise_for_status()
except (httpx.HTTPError, ssl.SSLError) as exc:
logger.warning(exc)
except (httpx.ConnectError, httpx.RequestError) as exc:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Failed to connect to {domain}.",
@@ -475,6 +366,7 @@ async def api_payment_pay_with_nfc(
payment_request: str,
lnurl_data: PayLnurlWData,
) -> JSONResponse:
lnurl = lnurl_data.lnurl_w.lower()
# Follow LUD-17 -> https://github.com/lnurl/luds/blob/luds/17.md
@@ -483,7 +375,6 @@ async def api_payment_pay_with_nfc(
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
try:
check_callback_url(url)
lnurl_req = await client.get(url, timeout=10)
if lnurl_req.is_error:
return JSONResponse(
+3 -32
View File
@@ -31,11 +31,9 @@ from lnbits.core.models import (
UserExtra,
Wallet,
)
from lnbits.core.models.notifications import NotificationType
from lnbits.core.models.users import Account
from lnbits.core.services import (
create_user_account_no_ckeck,
enqueue_notification,
update_user_account,
update_user_extensions,
update_wallet_balance,
@@ -104,24 +102,16 @@ async def api_create_user(data: CreateUser) -> CreateUser:
)
account.validate_fields()
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
return data
@users_router.put("/user/{user_id}", name="Update user")
async def api_update_user(
user_id: str, data: CreateUser, user: User = Depends(check_admin)
) -> CreateUser:
async def api_update_user(user_id: str, data: CreateUser) -> CreateUser:
if user_id != data.id:
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:
raise HTTPException(
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
@@ -263,22 +253,13 @@ async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleSta
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",
)
async def api_users_delete_user_wallet(
user_id: str, wallet: str, user: User = Depends(check_admin)
) -> SimpleStatus:
async def api_users_delete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
wal = await get_wallet(wallet)
if not wal:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
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:
await force_delete_wallet(wallet)
await delete_wallet(user_id=user_id, wallet_id=wallet)
@@ -296,14 +277,4 @@ async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
if not wallet:
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
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.")
+9 -29
View File
@@ -1,6 +1,5 @@
from http import HTTPStatus
from typing import Optional
from uuid import uuid4
from fastapi import (
APIRouter,
@@ -9,10 +8,13 @@ from fastapi import (
HTTPException,
)
from lnbits.core.models import CreateWallet, KeyType, User, Wallet
from lnbits.core.models import (
CreateWallet,
KeyType,
Wallet,
)
from lnbits.decorators import (
WalletTypeInfo,
check_user_exists,
require_admin_key,
require_invoice_key,
)
@@ -54,25 +56,9 @@ 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("")
async def api_update_wallet(
name: Optional[str] = Body(None),
icon: Optional[str] = Body(None),
color: Optional[str] = Body(None),
currency: Optional[str] = Body(None),
key_info: WalletTypeInfo = Depends(require_admin_key),
) -> Wallet:
@@ -80,24 +66,18 @@ async def api_update_wallet(
if not wallet:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
wallet.name = name or wallet.name
wallet.extra.icon = icon or wallet.extra.icon
wallet.extra.color = color or wallet.extra.color
wallet.currency = currency if currency is not None else wallet.currency
await update_wallet(wallet)
return wallet
@wallet_router.delete("/{wallet_id}")
@wallet_router.delete("")
async def api_delete_wallet(
wallet_id: str, user: User = Depends(check_user_exists)
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> 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(
user_id=wallet.user,
wallet_id=wallet.id,
user_id=wallet.wallet.user,
wallet_id=wallet.wallet.id,
)
+7 -7
View File
@@ -15,9 +15,9 @@ from lnbits.core.models import (
CreateWebPushSubscription,
WebPushSubscription,
)
from lnbits.core.models.users import User
from lnbits.decorators import (
check_user_exists,
WalletTypeInfo,
require_admin_key,
)
from ..crud import (
@@ -33,20 +33,20 @@ webpush_router = APIRouter(prefix="/api/v1/webpush", tags=["Webpush"])
async def api_create_webpush_subscription(
request: Request,
data: CreateWebPushSubscription,
user: User = Depends(check_user_exists),
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> WebPushSubscription:
try:
subscription = json.loads(data.subscription)
endpoint = subscription["endpoint"]
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:
return subscription
else:
return await create_webpush_subscription(
endpoint,
user.id,
wallet.wallet.user,
data.subscription,
host,
)
@@ -61,13 +61,13 @@ async def api_create_webpush_subscription(
@webpush_router.delete("", status_code=HTTPStatus.OK)
async def api_delete_webpush_subscription(
request: Request,
user: User = Depends(check_user_exists),
wallet: WalletTypeInfo = Depends(require_admin_key),
):
try:
endpoint = unquote(
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}
except Exception as exc:
logger.debug(exc)
+46 -77
View File
@@ -8,7 +8,7 @@ import time
from contextlib import asynccontextmanager
from datetime import datetime, timezone
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 pydantic import BaseModel, ValidationError, root_validator
@@ -22,13 +22,6 @@ POSTGRES = "POSTGRES"
COCKROACH = "COCKROACH"
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:
database_uri = settings.lnbits_database_url
if database_uri.startswith("cockroachdb://"):
@@ -65,8 +58,8 @@ def get_placeholder(model: Any, field: str) -> str:
class Compat:
type: str | None = "<inherited>"
schema: str | None = "<inherited>"
type: Optional[str] = "<inherited>"
schema: Optional[str] = "<inherited>"
def interval_seconds(self, seconds: int) -> str:
if self.type in {POSTGRES, COCKROACH}:
@@ -82,18 +75,6 @@ class Compat:
return time.mktime(date.timetuple())
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
def timestamp_now(self) -> str:
if self.type in {POSTGRES, COCKROACH}:
@@ -167,8 +148,8 @@ class Connection(Compat):
async def fetchall(
self,
query: str,
values: dict | None = None,
model: type[TModel] | None = None,
values: Optional[dict] = None,
model: Optional[type[TModel]] = None,
) -> list[TModel]:
params = self.rewrite_values(values) if values else {}
result = await self.conn.execute(text(self.rewrite_query(query)), params)
@@ -183,8 +164,8 @@ class Connection(Compat):
async def fetchone(
self,
query: str,
values: dict | None = None,
model: type[TModel] | None = None,
values: Optional[dict] = None,
model: Optional[type[TModel]] = None,
) -> TModel:
params = self.rewrite_values(values) if values else {}
result = await self.conn.execute(text(self.rewrite_query(query)), params)
@@ -211,11 +192,11 @@ class Connection(Compat):
async def fetch_page(
self,
query: str,
where: list[str] | None = None,
values: dict | None = None,
filters: Filters | None = None,
model: type[TModel] | None = None,
group_by: list[str] | None = None,
where: Optional[list[str]] = None,
values: Optional[dict] = None,
filters: Optional[Filters] = None,
model: Optional[type[TModel]] = None,
group_by: Optional[list[str]] = None,
) -> Page[TModel]:
if not filters:
filters = Filters()
@@ -268,7 +249,7 @@ class Connection(Compat):
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 {}
result = await self.conn.execute(text(self.rewrite_query(query)), params)
await self.conn.commit()
@@ -350,8 +331,8 @@ class Database(Compat):
async def fetchall(
self,
query: str,
values: dict | None = None,
model: type[TModel] | None = None,
values: Optional[dict] = None,
model: Optional[type[TModel]] = None,
) -> list[TModel]:
async with self.connect() as conn:
return await conn.fetchall(query, values, model)
@@ -359,8 +340,8 @@ class Database(Compat):
async def fetchone(
self,
query: str,
values: dict | None = None,
model: type[TModel] | None = None,
values: Optional[dict] = None,
model: Optional[type[TModel]] = None,
) -> TModel:
async with self.connect() as conn:
return await conn.fetchone(query, values, model)
@@ -378,16 +359,16 @@ class Database(Compat):
async def fetch_page(
self,
query: str,
where: list[str] | None = None,
values: dict | None = None,
filters: Filters | None = None,
model: type[TModel] | None = None,
group_by: list[str] | None = None,
where: Optional[list[str]] = None,
values: Optional[dict] = None,
filters: Optional[Filters] = None,
model: Optional[type[TModel]] = None,
group_by: Optional[list[str]] = None,
) -> Page[TModel]:
async with self.connect() as conn:
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:
return await conn.execute(query, values)
@@ -445,7 +426,7 @@ class Operator(Enum):
class FilterModel(BaseModel):
__search_fields__: list[str] = []
__sort_fields__: list[str] | None = None
__sort_fields__: Optional[list[str]] = None
T = TypeVar("T")
@@ -461,8 +442,8 @@ class Page(BaseModel, Generic[T]):
class Filter(BaseModel, Generic[TFilterModel]):
field: str
op: Operator = Operator.EQ
model: type[TFilterModel] | None
values: dict | None = None
model: Optional[type[TFilterModel]]
values: Optional[dict] = None
@classmethod
def parse_query(
@@ -517,15 +498,15 @@ class Filters(BaseModel, Generic[TFilterModel]):
"""
filters: list[Filter[TFilterModel]] = []
search: str | None = None
search: Optional[str] = None
offset: int | None = None
limit: int | None = None
offset: Optional[int] = None
limit: Optional[int] = None
sortby: str | None = None
direction: Literal["asc", "desc"] | None = None
sortby: Optional[str] = None
direction: Optional[Literal["asc", "desc"]] = None
model: type[TFilterModel] | None = None
model: Optional[type[TFilterModel]] = None
@root_validator(pre=True)
def validate_sortby(cls, values):
@@ -547,21 +528,18 @@ class Filters(BaseModel, Generic[TFilterModel]):
stmt += f"OFFSET {self.offset}"
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:
where_stmts = []
if self.filters:
for page_filter in self.filters:
where_stmts.append(page_filter.statement)
if self.search and self.model and self.model.__search_fields__:
# Use `COALESCE` to handle `NULL` values and `||`
# for cross-database compatible string concatenation
_fields = self.model.__search_fields__
search_expr = " || ".join(
f"COALESCE(CAST({field} AS TEXT), '')" for field in _fields
)
where_stmts.append(f"lower({search_expr}) LIKE :search")
if self.search and self.model:
fields = self.model.__search_fields__
if DB_TYPE == POSTGRES:
where_stmts.append(f"lower(concat({', '.join(fields)})) LIKE :search")
elif DB_TYPE == SQLITE:
where_stmts.append(f"lower({'||'.join(fields)}) LIKE :search")
if where_stmts:
return "WHERE " + " AND ".join(where_stmts)
return ""
@@ -571,7 +549,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
return ""
def values(self, values: dict | None = None) -> dict:
def values(self, values: Optional[dict] = None) -> dict:
if not values:
values = {}
if self.filters:
@@ -580,7 +558,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
for key, value in page_filter.values.items():
values[key] = value
if self.search and self.model:
values["search"] = f"%{self.search.lower()}%"
values["search"] = f"%{self.search}%"
return values
@@ -645,12 +623,12 @@ def model_to_dict(model: BaseModel) -> 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"""
if isinstance(value, str):
if value == "null" or value == "":
if value == "null":
return None
_subdict = _safe_load_json(value)
_subdict = json.loads(value)
elif isinstance(value, dict):
_subdict = value
@@ -674,7 +652,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
type_ = model.__fields__[key].type_
outertype_ = model.__fields__[key].outer_type_
if get_origin(outertype_) is list:
_items = _safe_load_json(value) if isinstance(value, str) else value
_items = json.loads(value) if isinstance(value, str) else value
_dict[key] = [
dict_to_submodel(type_, v) if issubclass(type_, BaseModel) else v
for v in _items
@@ -695,7 +673,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
# TODO: remove this when all sub models are migrated to Pydantic
# NOTE: this is for type dict on BaseModel, (used in Payment class)
if type_ is dict and value:
_dict[key] = _safe_load_json(value)
_dict[key] = json.loads(value)
continue
_dict[key] = value
continue
@@ -703,12 +681,3 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
if isinstance(_model, BaseModel):
_model.__init__(**_dict) # type: ignore
return _model
def _safe_load_json(value: str) -> dict:
try:
return json.loads(value)
except json.JSONDecodeError:
# DB is corrupted if it gets here
logger.error(f"Failed to decode JSON: '{value}'")
return {}
+31 -81
View File
@@ -5,7 +5,7 @@ import jwt
from fastapi import Cookie, Depends, Query, Request, Security
from fastapi.exceptions import HTTPException
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 loguru import logger
from pydantic.types import UUID4
@@ -18,7 +18,6 @@ from lnbits.core.crud import (
get_user_from_account,
get_wallet_for_key,
)
from lnbits.core.crud.users import get_user_access_control_lists
from lnbits.core.models import (
AccessTokenPayload,
Account,
@@ -28,18 +27,10 @@ from lnbits.core.models import (
WalletTypeInfo,
)
from lnbits.db import Connection, Filter, Filters, TFilterModel
from lnbits.helpers import path_segments
from lnbits.settings import AuthMethods, settings
oauth2_scheme = OAuth2PasswordBearer(
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",
)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth", auto_error=False)
api_key_header = APIKeyHeader(
name="X-API-KEY",
auto_error=False,
@@ -102,7 +93,7 @@ class KeyChecker(SecurityBase):
request.scope["user_id"] = wallet.user
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invalid adminkey.",
)
@@ -139,9 +130,8 @@ async def require_invoice_key(
async def check_access_token(
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
bearer_access_token: Annotated[Union[str, None], Depends(http_bearer)] = None,
) -> 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(
@@ -150,13 +140,9 @@ async def check_user_exists(
usr: Optional[UUID4] = None,
) -> User:
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):
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:
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
if not settings.is_user_allowed(account.id):
raise HTTPException(HTTPStatus.FORBIDDEN, "User not allowed.")
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not allowed.")
user = await get_user_from_account(account)
if not user:
@@ -175,14 +161,13 @@ async def check_user_exists(
async def optional_user_id(
r: Request,
access_token: Annotated[Optional[str], Depends(check_access_token)],
usr: Optional[UUID4] = None,
) -> Optional[str]:
if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
return usr.hex
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 None
@@ -201,11 +186,7 @@ async def access_token_payload(
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
raise HTTPException(
HTTPStatus.FORBIDDEN, "User not authorized. No admin privileges."
)
if not user.has_password:
raise HTTPException(
HTTPStatus.FORBIDDEN, "Admin users must have credentials configured."
HTTPStatus.UNAUTHORIZED, "User not authorized. No admin privileges."
)
return user
@@ -214,11 +195,7 @@ async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
if user.id != settings.super_user:
raise HTTPException(
HTTPStatus.FORBIDDEN, "User not authorized. No super user privileges."
)
if not user.has_password:
raise HTTPException(
HTTPStatus.FORBIDDEN, "Super user must have credentials configured."
HTTPStatus.UNAUTHORIZED, "User not authorized. No super user privileges."
)
return user
@@ -270,18 +247,19 @@ async def check_user_extension_access(
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)
if ext_id not in ext_ids:
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")
async def _check_user_extension_access(user_id: str, path: str):
ext_id = path_segments(path)[0]
async def _check_user_extension_access(user_id: str, current_path: str):
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)
if not status.success:
raise HTTPException(
@@ -290,14 +268,16 @@ async def _check_user_extension_access(user_id: str, path: str):
)
async def _get_account_from_token(
access_token: str, path: str, method: str
) -> Optional[Account]:
async def _get_account_from_token(access_token) -> Optional[Account]:
try:
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
return await _get_account_from_jwt_payload(
AccessTokenPayload(**payload), path, method
)
user = await _get_user_from_jwt_payload(payload)
if not user:
raise HTTPException(
HTTPStatus.UNAUTHORIZED, "Data missing for access token."
)
return user
except jwt.ExpiredSignatureError as exc:
raise HTTPException(
@@ -308,41 +288,11 @@ async def _get_account_from_token(
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid access token.") from exc
async def _get_account_from_jwt_payload(
payload: AccessTokenPayload, path: str, method: str
) -> Optional[Account]:
account = None
if payload.sub:
account = await get_account_by_username(payload.sub)
elif payload.usr:
account = await get_account(payload.usr)
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.")
async def _get_user_from_jwt_payload(payload) -> Optional[Account]:
if "sub" in payload and payload.get("sub"):
return await get_account_by_username(str(payload.get("sub")))
if "usr" in payload and payload.get("usr"):
return await get_account(str(payload.get("usr")))
if "email" in payload and payload.get("email"):
return await get_account_by_email(str(payload.get("email")))
return None
+3 -54
View File
@@ -8,9 +8,7 @@ from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, RedirectResponse, Response
from loguru import logger
from lnbits.settings import settings
from .helpers import path_segments, template_renderer
from .helpers import template_renderer
class PaymentError(Exception):
@@ -28,11 +26,9 @@ class InvoiceError(Exception):
def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
if not request.headers:
return None
if not _is_browser_request(request):
if "text/html" not in request.headers.get("accept", ""):
return None
if (
@@ -53,14 +49,7 @@ def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
)
return template_renderer().TemplateResponse(
request,
"error.html",
{
"err": f"Error: {exc!s}",
"status_code": int(status_code),
"message": str(exc).split(":")[-1].strip(),
},
status_code,
request, "error.html", {"err": f"Error: {exc!s}"}, status_code
)
@@ -130,43 +119,3 @@ def register_exception_handlers(app: FastAPI):
status_code=520,
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 path in settings.lnbits_all_extensions_ids:
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
+50 -123
View File
@@ -5,12 +5,10 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Optional, Type
from urllib import request
from urllib.parse import urlparse
import jinja2
import jwt
import shortuuid
from fastapi.routing import APIRoute
from packaging import version
from pydantic.schema import field_schema
@@ -64,50 +62,43 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
t.env.globals["static_url_for"] = static_url_for
window_settings = {
"AD_SPACE": settings.lnbits_ad_space.split(","),
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
"HIDE_API": settings.lnbits_hide_api,
"SITE_TITLE": settings.lnbits_site_title,
"SITE_TAGLINE": settings.lnbits_site_tagline,
"SITE_DESCRIPTION": settings.lnbits_site_description,
"LNBITS_ADMIN_UI": settings.lnbits_admin_ui,
"LNBITS_AUDIT_ENABLED": settings.lnbits_audit_enabled,
"LNBITS_AUTH_METHODS": settings.auth_allowed_methods,
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
"LNBITS_NODE_UI": settings.lnbits_node_ui and get_node_class() is not None,
"LNBITS_NODE_UI_AVAILABLE": get_node_class() is not None,
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
"LNBITS_VERSION": settings.version,
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
"USE_DEFAULT_REACTION": settings.lnbits_default_reaction,
"USE_DEFAULT_THEME": settings.lnbits_default_theme,
"USE_DEFAULT_BORDER": settings.lnbits_default_border,
"USE_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
"LNBITS_DENOMINATION": (
settings.lnbits_denomination
if settings.lnbits_denomination == "FakeWallet"
else "sats"
),
}
if settings.lnbits_ad_space_enabled:
t.env.globals["AD_SPACE"] = settings.lnbits_ad_space.split(",")
t.env.globals["AD_SPACE_TITLE"] = settings.lnbits_ad_space_title
t.env.globals["WINDOW_SETTINGS"] = window_settings
for key, value in window_settings.items():
t.env.globals[key] = value
t.env.globals["VOIDWALLET"] = settings.lnbits_backend_wallet_class == "VoidWallet"
t.env.globals["HIDE_API"] = settings.lnbits_hide_api
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
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:
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
@@ -121,6 +112,8 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
t.env.globals["WEBPUSH_PUBKEY"] = settings.lnbits_webpush_pubkey
return t
@@ -205,42 +198,27 @@ def is_valid_pubkey(pubkey: str) -> bool:
return False
def create_access_token(data: dict, token_expire_minutes: Optional[int] = None) -> str:
minutes = token_expire_minutes or settings.auth_token_expire_minutes
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
to_encode = {k: v for k, v in data.items() if v is not None}
to_encode.update({"exp": expire}) # todo:check expiration
def create_access_token(data: dict):
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.auth_token_expire_minutes
)
to_encode = data.copy()
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
def encrypt_internal_message(
m: Optional[str] = None, urlsafe: bool = False
) -> Optional[str]:
"""
Encrypt message with the internal secret key
Args:
m: Message to encrypt
urlsafe: Whether to use URL-safe base64 encoding
"""
def encrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
"""Encrypt message with the internal secret key"""
if not m:
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(
m: Optional[str] = None, urlsafe: bool = False
) -> Optional[str]:
"""
Decrypt message with the internal secret key
Args:
m: Message to decrypt
urlsafe: Whether the message uses URL-safe base64 encoding
"""
def decrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
"""Decrypt message with the internal secret key"""
if not m:
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:
@@ -278,15 +256,6 @@ def is_lnbits_version_ok(
return True
def check_callback_url(url: str):
netloc = urlparse(url).netloc
for rule in settings.lnbits_callback_url_rules:
if re.match(rule, netloc) is None:
raise ValueError(
f"Callback not allowed. URL: {url}. Netloc: {netloc}. Rule: {rule}"
)
def download_url(url, save_path):
with request.urlopen(url, timeout=60) as dl_file:
with open(save_path, "wb") as out_file:
@@ -301,45 +270,3 @@ def file_hash(filename):
while n := f.readinto(mv):
h.update(mv[:n])
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()
+5 -10
View File
@@ -15,7 +15,7 @@ from starlette.types import ASGIApp, Receive, Scope, Send
from lnbits.core.db import core_app_extra
from lnbits.core.models import AuditEntry
from lnbits.helpers import normalize_path, template_renderer
from lnbits.helpers import template_renderer
from lnbits.settings import settings
@@ -82,11 +82,7 @@ class InstalledExtensionMiddleware:
return HTMLResponse(
status_code=status_code,
content=template_renderer()
.TemplateResponse(
Request(scope),
"error.html",
{"err": msg, "status_code": status_code, "message": msg},
)
.TemplateResponse(Request(scope), "error.html", {"err": msg})
.body,
)
@@ -118,6 +114,7 @@ class ExtensionsRedirectMiddleware:
class AuditMiddleware(BaseHTTPMiddleware):
def __init__(self, app: ASGIApp, audit_queue: asyncio.Queue) -> None:
super().__init__(app)
self.audit_queue = audit_queue
@@ -134,9 +131,8 @@ class AuditMiddleware(BaseHTTPMiddleware):
assert response
return response
finally:
if request_details:
duration = (datetime.now(timezone.utc) - start_time).total_seconds()
await self._log_audit(request, response, duration, request_details)
duration = (datetime.now(timezone.utc) - start_time).total_seconds()
await self._log_audit(request, response, duration, request_details)
async def _log_audit(
self,
@@ -160,7 +156,6 @@ class AuditMiddleware(BaseHTTPMiddleware):
if settings.is_super_user(user_id):
user_id = "super_user"
component = "core"
path = normalize_path(path)
if path and not path.startswith("/api"):
component = path.split("/")[1]
+40 -54
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from pydantic import BaseModel
@@ -15,10 +15,10 @@ if TYPE_CHECKING:
class NodePeerInfo(BaseModel):
id: str
alias: str | None = None
color: str | None = None
last_timestamp: int | None = None
addresses: list[str] | None = None
alias: Optional[str] = None
color: Optional[str] = None
last_timestamp: Optional[int] = None
addresses: Optional[list[str]] = None
class ChannelState(Enum):
@@ -38,29 +38,22 @@ class ChannelPoint(BaseModel):
funding_txid: str
output_index: int
def __str__(self):
return f"{self.funding_txid}:{self.output_index}"
class NodeChannel(BaseModel):
short_id: Optional[str] = None
point: Optional[ChannelPoint] = None
peer_id: str
balance: ChannelBalance
state: ChannelState
# could be optional for closing/pending channels on lndrest
id: str | None = None
short_id: str | None = None
point: ChannelPoint | None = None
name: str | None = None
color: str | None = None
fee_ppm: int | None = None
fee_base_msat: int | None = None
name: Optional[str]
color: Optional[str]
class ChannelStats(BaseModel):
counts: dict[ChannelState, int]
avg_size: int
biggest_size: int | None
smallest_size: int | None
biggest_size: Optional[int]
smallest_size: Optional[int]
total_capacity: int
@classmethod
@@ -95,9 +88,9 @@ class ChannelStats(BaseModel):
class NodeFees(BaseModel):
total_msat: int
daily_msat: int | None = None
weekly_msat: int | None = None
monthly_msat: int | None = None
daily_msat: Optional[int] = None
weekly_msat: Optional[int] = None
monthly_msat: Optional[int] = None
class PublicNodeInfo(BaseModel):
@@ -121,25 +114,25 @@ class NodeInfoResponse(PublicNodeInfo):
class NodePayment(BaseModel):
pending: bool
amount: int
fee: int | None = None
memo: str | None = None
fee: Optional[int] = None
memo: Optional[str] = None
time: int
bolt11: str | None = None
preimage: str | None
bolt11: Optional[str] = None
preimage: Optional[str]
payment_hash: str
expiry: float | None = None
destination: NodePeerInfo | None = None
expiry: Optional[float] = None
destination: Optional[NodePeerInfo] = None
class NodeInvoice(BaseModel):
pending: bool
amount: int
memo: str | None
memo: Optional[str]
bolt11: str
preimage: str | None
preimage: Optional[str]
payment_hash: str
paid_at: int | None = None
expiry: int | None = None
paid_at: Optional[int] = None
expiry: Optional[int] = None
class NodeInvoiceFilters(FilterModel):
@@ -151,10 +144,11 @@ class NodePaymentsFilters(FilterModel):
class Node(ABC):
wallet: Wallet
def __init__(self, wallet: Wallet):
self.wallet = wallet
self.id: str | None = None
self.id: Optional[str] = None
@property
def name(self):
@@ -167,7 +161,7 @@ class Node(ABC):
@abstractmethod
async def _get_id(self) -> str:
raise NotImplementedError
pass
async def get_peers(self) -> list[NodePeerInfo]:
peer_ids = await self.get_peer_ids()
@@ -175,15 +169,15 @@ class Node(ABC):
@abstractmethod
async def get_peer_ids(self) -> list[str]:
raise NotImplementedError
pass
@abstractmethod
async def connect_peer(self, uri: str):
raise NotImplementedError
pass
@abstractmethod
async def disconnect_peer(self, peer_id: str):
raise NotImplementedError
pass
@abstractmethod
async def _get_peer_info(self, peer_id: str) -> NodePeerInfo:
@@ -203,35 +197,27 @@ class Node(ABC):
self,
peer_id: str,
local_amount: int,
push_amount: int | None = None,
fee_rate: int | None = None,
push_amount: Optional[int] = None,
fee_rate: Optional[int] = None,
) -> ChannelPoint:
raise NotImplementedError
pass
@abstractmethod
async def close_channel(
self,
short_id: str | None = None,
point: ChannelPoint | None = None,
short_id: Optional[str] = None,
point: Optional[ChannelPoint] = None,
force: bool = False,
):
raise NotImplementedError
@abstractmethod
async def get_channel(self, channel_id: str) -> NodeChannel | None:
raise NotImplementedError
pass
@abstractmethod
async def get_channels(self) -> list[NodeChannel]:
raise NotImplementedError
@abstractmethod
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
raise NotImplementedError
pass
@abstractmethod
async def get_info(self) -> NodeInfoResponse:
raise NotImplementedError
pass
async def get_public_info(self) -> PublicNodeInfo:
info = await self.get_info()
@@ -241,10 +227,10 @@ class Node(ABC):
async def get_payments(
self, filters: Filters[NodePaymentsFilters]
) -> Page[NodePayment]:
raise NotImplementedError
pass
@abstractmethod
async def get_invoices(
self, filters: Filters[NodeInvoiceFilters]
) -> Page[NodeInvoice]:
raise NotImplementedError
pass
+33 -57
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import asyncio
from http import HTTPStatus
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from fastapi import HTTPException
@@ -66,28 +66,6 @@ class CoreLightningNode(Node):
fn = getattr(self.wallet.ln, method)
return await loop.run_in_executor(None, lambda: fn(*args, **kwargs))
def _parse_state(self, state: str) -> ChannelState:
if state == "CHANNELD_NORMAL":
return ChannelState.ACTIVE
if state in (
# wait for force close
"AWAITING_UNILATERAL",
# waiting for close
"CHANNELD_SHUTTING_DOWN",
# waiting for open
"CHANNELD_AWAITING_LOCKIN",
"OPENINGD",
):
return ChannelState.PENDING
if state in (
"CHANNELD_CLOSING",
"CLOSINGD_COMPLETE",
"CLOSINGD_SIGEXCHANGE",
"ONCHAIN",
):
return ChannelState.CLOSED
return ChannelState.INACTIVE
@catch_rpc_errors
async def connect_peer(self, uri: str):
# https://docs.corelightning.org/reference/lightning-connect
@@ -119,8 +97,8 @@ class CoreLightningNode(Node):
self,
peer_id: str,
local_amount: int,
push_amount: int | None = None,
fee_rate: int | None = None,
push_amount: Optional[int] = None,
fee_rate: Optional[int] = None,
) -> ChannelPoint:
try:
result = await self.ln_rpc(
@@ -173,8 +151,8 @@ class CoreLightningNode(Node):
@catch_rpc_errors
async def close_channel(
self,
short_id: str | None = None,
point: ChannelPoint | None = None,
short_id: Optional[str] = None,
point: Optional[ChannelPoint] = None,
force: bool = False,
):
if not short_id:
@@ -224,45 +202,48 @@ class CoreLightningNode(Node):
else:
return NodePeerInfo(id=node["nodeid"])
@catch_rpc_errors
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
await self.ln_rpc("setchannel", channel_id, feebase=base_msat, feeppm=ppm)
@catch_rpc_errors
async def get_channel(self, channel_id: str) -> NodeChannel | None:
channels = await self.get_channels()
for channel in channels:
if channel.id == channel_id:
return channel
return None
@catch_rpc_errors
async def get_channels(self) -> list[NodeChannel]:
channels = await self.ln_rpc("listpeerchannels")
funds = await self.ln_rpc("listfunds")
nodes = await self.ln_rpc("listnodes")
nodes_by_id = {n["nodeid"]: n for n in nodes["nodes"]}
return [
NodeChannel(
id=ch["channel_id"],
short_id=ch.get("short_channel_id"),
point=ChannelPoint(
funding_txid=ch["funding_txid"],
output_index=ch["funding_outnum"],
output_index=ch["funding_output"],
),
peer_id=ch["peer_id"],
balance=ChannelBalance(
local_msat=ch["spendable_msat"],
remote_msat=ch["receivable_msat"],
total_msat=ch["total_msat"],
local_msat=ch["our_amount_msat"],
remote_msat=ch["amount_msat"] - ch["our_amount_msat"],
total_msat=ch["amount_msat"],
),
fee_ppm=ch["fee_proportional_millionths"],
fee_base_msat=ch["fee_base_msat"],
name=nodes_by_id.get(ch["peer_id"], {}).get("alias"),
color=nodes_by_id.get(ch["peer_id"], {}).get("color"),
state=self._parse_state(ch["state"]),
state=(
ChannelState.ACTIVE
if ch["state"] == "CHANNELD_NORMAL"
else (
ChannelState.PENDING
if ch["state"] in ("CHANNELD_AWAITING_LOCKIN", "OPENINGD")
else (
ChannelState.CLOSED
if ch["state"]
in (
"CHANNELD_CLOSING",
"CLOSINGD_COMPLETE",
"CLOSINGD_SIGEXCHANGE",
"ONCHAIN",
)
else ChannelState.INACTIVE
)
)
),
)
for ch in channels["channels"]
for ch in funds["channels"]
]
@catch_rpc_errors
@@ -304,19 +285,14 @@ class CoreLightningNode(Node):
return [
NodePayment(
bolt11=pay.get("bolt11"),
amount=pay.get("amount_msat", 0),
fee=int(pay.get("amount_msat", 0))
- int(pay.get("amount_sent_msat", 0)),
amount=pay["amount_msat"],
fee=int(pay["amount_msat"]) - int(pay["amount_sent_msat"]),
memo=pay.get("description"),
time=pay["created_at"],
preimage=pay.get("preimage"),
payment_hash=pay["payment_hash"],
pending=pay["status"] != "complete",
destination=(
await self.get_peer_info(pay.get("destination"))
if pay.get("destination")
else None
),
destination=await self.get_peer_info(pay["destination"]),
)
for pay in reversed(result["pays"])
if pay["status"] != "failed"
+17 -98
View File
@@ -4,7 +4,7 @@ import asyncio
import base64
import json
from http import HTTPStatus
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from fastapi import HTTPException
from httpx import HTTPStatusError
@@ -41,14 +41,6 @@ def _decode_bytes(data: str) -> str:
return base64.b64decode(data).hex()
def _encode_bytes(data: str) -> str:
return base64.b64encode(bytes.fromhex(data)).decode()
def _encode_urlsafe_bytes(data: str) -> str:
return base64.urlsafe_b64encode(bytes.fromhex(data)).decode()
def _parse_channel_point(raw: str) -> ChannelPoint:
funding_tx, output_index = raw.split(":")
return ChannelPoint(
@@ -60,7 +52,9 @@ def _parse_channel_point(raw: str) -> ChannelPoint:
class LndRestNode(Node):
wallet: LndRestWallet
async def request(self, method: str, path: str, json: dict | None = None, **kwargs):
async def request(
self, method: str, path: str, json: Optional[dict] = None, **kwargs
):
response = await self.wallet.client.request(
method, f"{self.wallet.endpoint}{path}", json=json, **kwargs
)
@@ -129,18 +123,21 @@ class LndRestNode(Node):
self,
peer_id: str,
local_amount: int,
push_amount: int | None = None,
fee_rate: int | None = None,
push_amount: Optional[int] = None,
fee_rate: Optional[int] = None,
) -> ChannelPoint:
response = await self.request(
"POST",
"/v1/channels",
json={
"node_pubkey": _encode_bytes(peer_id),
"sat_per_vbyte": fee_rate,
"local_funding_amount": local_amount,
"push_sat": push_amount,
},
data=json.dumps(
{
# 'node_pubkey': base64.b64encode(peer_id.encode()).decode(),
"node_pubkey_string": peer_id,
"sat_per_vbyte": fee_rate,
"local_funding_amount": local_amount,
"push_sat": push_amount,
}
),
)
return ChannelPoint(
# WHY IS THIS REVERSED?!
@@ -174,8 +171,8 @@ class LndRestNode(Node):
async def close_channel(
self,
short_id: str | None = None,
point: ChannelPoint | None = None,
short_id: Optional[str] = None,
point: Optional[ChannelPoint] = None,
force: bool = False,
):
if short_id:
@@ -187,81 +184,6 @@ class LndRestNode(Node):
asyncio.create_task(self._close_channel(point, force)) # noqa: RUF006
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
# https://lightning.engineering/api-docs/api/lnd/lightning/update-channel-policy/
channel = await self.get_channel(channel_id)
if not channel:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Channel not found"
)
if not channel.point:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Channel point required"
)
await self.request(
"POST",
"/v1/chanpolicy",
json={
"base_fee_msat": base_msat,
"fee_rate_ppm": ppm,
"chan_point": {
"funding_txid_str": channel.point.funding_txid,
"output_index": channel.point.output_index,
},
# https://docs.lightning.engineering/lightning-network-tools/lnd/optimal-configuration-of-a-routing-node#channel-defaults
"time_lock_delta": 80,
# 'max_htlc_msat': <uint64>,
# 'min_htlc_msat': <uint64>,
# 'inbound_fee': <InboundFee>,
},
)
async def get_channel(self, channel_id: str) -> NodeChannel | None:
channel_info = await self.get(f"/v1/graph/edge/{channel_id}")
info = await self.get("/v1/getinfo")
if info["identity_pubkey"] == channel_info["node1_pub"]:
my_node_key = "node1"
peer_node_key = "node2"
else:
my_node_key = "node2"
peer_node_key = "node1"
peer_id = channel_info[f"{peer_node_key}_pub"]
peer_b64 = _encode_urlsafe_bytes(peer_id)
channels = await self.get(f"/v1/channels?peer={peer_b64}")
if "error" in channel_info and "error" in channels:
logger.debug("LND get_channel", channels)
return None
if len(channels["channels"]) == 0:
logger.debug(f"LND get_channel no channels founds with id {peer_b64}")
return None
for channel in channels["channels"]:
if channel["chan_id"] == channel_id:
peer_info = await self.get_peer_info(peer_id)
return NodeChannel(
id=channel.get("chan_id"),
peer_id=peer_info.id,
name=peer_info.alias,
color=peer_info.color,
state=(
ChannelState.ACTIVE
if channel["active"]
else ChannelState.INACTIVE
),
fee_ppm=channel_info[f"{my_node_key}_policy"][
"fee_rate_milli_msat"
],
fee_base_msat=channel_info[f"{my_node_key}_policy"][
"fee_base_msat"
],
point=_parse_channel_point(channel["channel_point"]),
balance=ChannelBalance(
local_msat=msat(channel["local_balance"]),
remote_msat=msat(channel["remote_balance"]),
total_msat=msat(channel["capacity"]),
),
)
return None
async def get_channels(self) -> list[NodeChannel]:
normal, pending, closed = await asyncio.gather(
self.get("/v1/channels"),
@@ -281,7 +203,6 @@ class LndRestNode(Node):
state=state,
name=info.alias,
color=info.color,
id=channel.get("chan_id", "node is for pending channels"),
point=_parse_channel_point(channel["channel_point"]),
balance=ChannelBalance(
local_msat=msat(channel["local_balance"]),
@@ -301,7 +222,6 @@ class LndRestNode(Node):
info = await self.get_peer_info(channel["remote_pubkey"])
channels.append(
NodeChannel(
id=channel.get("chan_id", "node is for closing channels"),
peer_id=info.id,
state=ChannelState.CLOSED,
name=info.alias,
@@ -319,7 +239,6 @@ class LndRestNode(Node):
info = await self.get_peer_info(channel["remote_pubkey"])
channels.append(
NodeChannel(
id=channel["chan_id"],
short_id=channel["chan_id"],
point=_parse_channel_point(channel["channel_point"]),
peer_id=channel["remote_pubkey"],
+168 -216
View File
@@ -4,16 +4,15 @@ import importlib
import importlib.metadata
import inspect
import json
import os
import re
from datetime import datetime, timezone
from enum import Enum
from hashlib import sha256
from os import path
from pathlib import Path
from time import gmtime, strftime, time
from typing import Any
from uuid import uuid4
from time import time
from typing import Any, Optional
import httpx
from loguru import logger
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
@@ -77,7 +76,7 @@ class RedirectPath(BaseModel):
other.from_path, list(other.header_filters.items())
) or other.redirect_matches(self.from_path, list(self.header_filters.items()))
def find_in_conflict(self, others: list[RedirectPath]) -> RedirectPath | None:
def find_in_conflict(self, others: list[RedirectPath]) -> Optional[RedirectPath]:
for other in others:
if self.in_conflict(other):
return other
@@ -149,11 +148,11 @@ class InstalledExtensionsSettings(LNbitsSettings):
lnbits_extensions_redirects: list[RedirectPath] = Field(default=[])
# list of all extension ids
lnbits_installed_extensions_ids: set[str] = Field(default=[])
lnbits_all_extensions_ids: set[str] = Field(default=[])
def find_extension_redirect(
self, path: str, req_headers: list[tuple[bytes, bytes]]
) -> RedirectPath | None:
) -> Optional[RedirectPath]:
headers = [(k.decode(), v.decode()) for k, v in req_headers]
return next(
(
@@ -167,8 +166,8 @@ class InstalledExtensionsSettings(LNbitsSettings):
def activate_extension_paths(
self,
ext_id: str,
upgrade_hash: str | None = None,
ext_redirects: list[dict] | None = None,
upgrade_hash: Optional[str] = None,
ext_redirects: Optional[list[dict]] = None,
):
self.lnbits_deactivated_extensions.discard(ext_id)
@@ -182,7 +181,7 @@ class InstalledExtensionsSettings(LNbitsSettings):
if ext_redirects:
self._activate_extension_redirects(ext_id, ext_redirects)
self.lnbits_installed_extensions_ids.add(ext_id)
self.lnbits_all_extensions_ids.add(ext_id)
def deactivate_extension_paths(self, ext_id: str):
self.lnbits_deactivated_extensions.add(ext_id)
@@ -216,6 +215,7 @@ class InstalledExtensionsSettings(LNbitsSettings):
class ExchangeHistorySettings(LNbitsSettings):
lnbits_exchange_rate_history: list[dict] = Field(default=[])
def append_exchange_rate_datapoint(self, rates: dict, max_size: int):
@@ -231,12 +231,12 @@ class ExchangeHistorySettings(LNbitsSettings):
class ThemesSettings(LNbitsSettings):
lnbits_site_title: str = Field(default="LNbits")
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
lnbits_site_description: str | None = Field(
lnbits_site_description: Optional[str] = Field(
default="The world's most powerful suite of bitcoin tools."
)
lnbits_show_home_page_elements: bool = Field(default=True)
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
lnbits_custom_badge: str | None = Field(default=None)
lnbits_custom_badge: Optional[str] = Field(default=None)
lnbits_custom_badge_color: str = Field(default="warning")
lnbits_theme_options: list[str] = Field(
default=[
@@ -247,56 +247,33 @@ class ThemesSettings(LNbitsSettings):
"monochrome",
"autumn",
"cyber",
"flamingo",
"bitcoin",
]
)
lnbits_custom_logo: str | None = Field(default=None)
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
lnbits_custom_logo: Optional[str] = Field(default=None)
lnbits_ad_space_title: str = Field(default="Supported by")
lnbits_ad_space: str = Field(
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
) # sneaky sneaky
lnbits_ad_space_enabled: bool = Field(default=False)
lnbits_allowed_currencies: list[str] = Field(default=[])
lnbits_default_accounting_currency: str | None = Field(default=None)
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
lnbits_default_reaction: str = Field(default="confettiBothSides")
lnbits_default_theme: str = Field(default="salvador")
lnbits_default_border: str = Field(default="hard-border")
lnbits_default_gradient: bool = Field(default=True)
lnbits_default_bgimage: str = Field(default=None)
lnbits_default_accounting_currency: Optional[str] = Field(default=None)
lnbits_qr_logo: str = Field(default="/static/images/logos/lnbits.png")
class OpsSettings(LNbitsSettings):
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
lnbits_hide_api: bool = Field(default=False)
lnbits_upload_size_bytes: int = Field(default=512_000, ge=0) # 500kb
lnbits_upload_allowed_types: list[str] = Field(
default=[
"image/png",
"image/jpeg",
"image/jpg",
"image/heic",
"image/heif",
"image/heics",
"png",
"jpeg",
"jpg",
"heic",
"heif",
"heics",
]
)
lnbits_denomination: str = Field(default="sats")
class FeeSettings(LNbitsSettings):
lnbits_reserve_fee_min: int = Field(default=2000, ge=0)
lnbits_reserve_fee_percent: float = Field(default=1.0, ge=0)
lnbits_service_fee: float = Field(default=0, ge=0)
lnbits_reserve_fee_min: int = Field(default=2000)
lnbits_reserve_fee_percent: float = Field(default=1.0)
lnbits_service_fee: float = Field(default=0)
lnbits_service_fee_ignore_internal: bool = Field(default=True)
lnbits_service_fee_max: int = Field(default=0)
lnbits_service_fee_wallet: str | None = Field(default=None)
lnbits_service_fee_wallet: Optional[str] = Field(default=None)
# WARN: this same value must be used for balance check and passed to
# funding_source.pay_invoice(), it may cause a vulnerability if the values differ
@@ -309,9 +286,9 @@ class FeeSettings(LNbitsSettings):
class ExchangeProvidersSettings(LNbitsSettings):
lnbits_exchange_rate_cache_seconds: int = Field(default=30, ge=0)
lnbits_exchange_history_size: int = Field(default=60, ge=0)
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300, ge=0)
lnbits_exchange_rate_cache_seconds: int = Field(default=30)
lnbits_exchange_history_size: int = Field(default=60)
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300)
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
default=[
@@ -357,6 +334,13 @@ class ExchangeProvidersSettings(LNbitsSettings):
exclude_to=[],
ticker_conversion=[],
),
ExchangeRateProvider(
name="CoinMate",
api_url="https://coinmate.io/api/ticker?currencyPair=BTC_{TO}",
path="$.data.last",
exclude_to=[],
ticker_conversion=["USD:USDT"],
),
ExchangeRateProvider(
name="Kraken",
api_url="https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
@@ -376,25 +360,25 @@ class ExchangeProvidersSettings(LNbitsSettings):
class SecuritySettings(LNbitsSettings):
lnbits_rate_limit_no: int = Field(default=200, ge=0)
lnbits_rate_limit_no: str = Field(default="200")
lnbits_rate_limit_unit: str = Field(default="minute")
lnbits_allowed_ips: list[str] = Field(default=[])
lnbits_blocked_ips: list[str] = Field(default=[])
lnbits_callback_url_rules: list[str] = Field(
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
lnbits_notifications: bool = Field(default=False)
lnbits_killswitch: bool = Field(default=False)
lnbits_killswitch_interval: int = Field(default=60)
lnbits_wallet_limit_max_balance: int = Field(default=0)
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
lnbits_wallet_limit_secs_between_trans: int = Field(default=0)
lnbits_watchdog: bool = Field(default=False)
lnbits_watchdog_interval: int = Field(default=60)
lnbits_watchdog_delta: int = Field(default=1_000_000)
lnbits_status_manifest: str = Field(
default=(
"https://raw.githubusercontent.com/lnbits/lnbits-status/main/manifest.json"
)
)
lnbits_wallet_limit_max_balance: int = Field(default=0, ge=0)
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0, ge=0)
lnbits_wallet_limit_secs_between_trans: int = Field(default=0, ge=0)
lnbits_only_allow_incoming_payments: bool = Field(default=False)
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
lnbits_watchdog_interval_minutes: int = Field(default=60, gt=0)
lnbits_watchdog_delta: int = Field(default=1_000_000, gt=0)
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
def is_wallet_max_balance_exceeded(self, amount):
return (
self.lnbits_wallet_limit_max_balance
@@ -403,161 +387,130 @@ class SecuritySettings(LNbitsSettings):
)
class NotificationsSettings(LNbitsSettings):
lnbits_nostr_notifications_enabled: bool = Field(default=False)
lnbits_nostr_notifications_private_key: str = Field(default="")
lnbits_nostr_notifications_identifiers: list[str] = Field(default=[])
lnbits_telegram_notifications_enabled: bool = Field(default=False)
lnbits_telegram_notifications_access_token: str = Field(default="")
lnbits_telegram_notifications_chat_id: str = Field(default="")
lnbits_email_notifications_enabled: bool = Field(default=False)
lnbits_email_notifications_email: str = Field(default="")
lnbits_email_notifications_username: str = Field(default="")
lnbits_email_notifications_password: str = Field(default="")
lnbits_email_notifications_server: str = Field(default="smtp.protonmail.ch")
lnbits_email_notifications_port: int = Field(default=587)
lnbits_email_notifications_to_emails: list[str] = Field(default=[])
lnbits_notification_settings_update: bool = Field(default=True)
lnbits_notification_credit_debit: bool = Field(default=True)
notification_balance_delta_changed: bool = Field(default=True)
lnbits_notification_server_start_stop: bool = Field(default=True)
lnbits_notification_watchdog: bool = Field(default=False)
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
lnbits_notification_incoming_payment_amount_sats: int = Field(
default=1_000_000, ge=0
)
lnbits_notification_outgoing_payment_amount_sats: int = Field(
default=1_000_000, ge=0
)
class FakeWalletFundingSource(LNbitsSettings):
fake_wallet_secret: str = Field(default="ToTheMoon1")
lnbits_denomination: str = Field(default="sats")
class LNbitsFundingSource(LNbitsSettings):
lnbits_endpoint: str = Field(default="https://demo.lnbits.com")
lnbits_key: str | None = Field(default=None)
lnbits_admin_key: str | None = Field(default=None)
lnbits_invoice_key: str | None = Field(default=None)
lnbits_key: Optional[str] = Field(default=None)
lnbits_admin_key: Optional[str] = Field(default=None)
lnbits_invoice_key: Optional[str] = Field(default=None)
class ClicheFundingSource(LNbitsSettings):
cliche_endpoint: str | None = Field(default=None)
cliche_endpoint: Optional[str] = Field(default=None)
class CoreLightningFundingSource(LNbitsSettings):
corelightning_rpc: str | None = Field(default=None)
corelightning_rpc: Optional[str] = Field(default=None)
corelightning_pay_command: str = Field(default="pay")
clightning_rpc: str | None = Field(default=None)
clightning_rpc: Optional[str] = Field(default=None)
class CoreLightningRestFundingSource(LNbitsSettings):
corelightning_rest_url: str | None = Field(default=None)
corelightning_rest_macaroon: str | None = Field(default=None)
corelightning_rest_cert: str | None = Field(default=None)
corelightning_rest_url: Optional[str] = Field(default=None)
corelightning_rest_macaroon: Optional[str] = Field(default=None)
corelightning_rest_cert: Optional[str] = Field(default=None)
class EclairFundingSource(LNbitsSettings):
eclair_url: str | None = Field(default=None)
eclair_pass: str | None = Field(default=None)
eclair_url: Optional[str] = Field(default=None)
eclair_pass: Optional[str] = Field(default=None)
class LndRestFundingSource(LNbitsSettings):
lnd_rest_endpoint: str | None = Field(default=None)
lnd_rest_cert: str | None = Field(default=None)
lnd_rest_macaroon: str | None = Field(default=None)
lnd_rest_macaroon_encrypted: str | None = Field(default=None)
lnd_rest_endpoint: Optional[str] = Field(default=None)
lnd_rest_cert: Optional[str] = Field(default=None)
lnd_rest_macaroon: Optional[str] = Field(default=None)
lnd_rest_macaroon_encrypted: Optional[str] = Field(default=None)
lnd_rest_route_hints: bool = Field(default=True)
lnd_rest_allow_self_payment: bool = Field(default=False)
lnd_cert: str | None = Field(default=None)
lnd_admin_macaroon: str | None = Field(default=None)
lnd_invoice_macaroon: str | None = Field(default=None)
lnd_rest_admin_macaroon: str | None = Field(default=None)
lnd_rest_invoice_macaroon: str | None = Field(default=None)
lnd_cert: Optional[str] = Field(default=None)
lnd_admin_macaroon: Optional[str] = Field(default=None)
lnd_invoice_macaroon: Optional[str] = Field(default=None)
lnd_rest_admin_macaroon: Optional[str] = Field(default=None)
lnd_rest_invoice_macaroon: Optional[str] = Field(default=None)
class LndGrpcFundingSource(LNbitsSettings):
lnd_grpc_endpoint: str | None = Field(default=None)
lnd_grpc_cert: str | None = Field(default=None)
lnd_grpc_port: int | None = Field(default=None)
lnd_grpc_admin_macaroon: str | None = Field(default=None)
lnd_grpc_invoice_macaroon: str | None = Field(default=None)
lnd_grpc_macaroon: str | None = Field(default=None)
lnd_grpc_macaroon_encrypted: str | None = Field(default=None)
lnd_grpc_endpoint: Optional[str] = Field(default=None)
lnd_grpc_cert: Optional[str] = Field(default=None)
lnd_grpc_port: Optional[int] = Field(default=None)
lnd_grpc_admin_macaroon: Optional[str] = Field(default=None)
lnd_grpc_invoice_macaroon: Optional[str] = Field(default=None)
lnd_grpc_macaroon: Optional[str] = Field(default=None)
lnd_grpc_macaroon_encrypted: Optional[str] = Field(default=None)
class LnPayFundingSource(LNbitsSettings):
lnpay_api_endpoint: str | None = Field(default=None)
lnpay_api_key: str | None = Field(default=None)
lnpay_wallet_key: str | None = Field(default=None)
lnpay_admin_key: str | None = Field(default=None)
lnpay_api_endpoint: Optional[str] = Field(default=None)
lnpay_api_key: Optional[str] = Field(default=None)
lnpay_wallet_key: Optional[str] = Field(default=None)
lnpay_admin_key: Optional[str] = Field(default=None)
class BlinkFundingSource(LNbitsSettings):
blink_api_endpoint: str | None = Field(default="https://api.blink.sv/graphql")
blink_ws_endpoint: str | None = Field(default="wss://ws.blink.sv/graphql")
blink_token: str | None = Field(default=None)
blink_api_endpoint: Optional[str] = Field(default="https://api.blink.sv/graphql")
blink_ws_endpoint: Optional[str] = Field(default="wss://ws.blink.sv/graphql")
blink_token: Optional[str] = Field(default=None)
class ZBDFundingSource(LNbitsSettings):
zbd_api_endpoint: str | None = Field(default="https://api.zebedee.io/v0/")
zbd_api_key: str | None = Field(default=None)
zbd_api_endpoint: Optional[str] = Field(default="https://api.zebedee.io/v0/")
zbd_api_key: Optional[str] = Field(default=None)
class PhoenixdFundingSource(LNbitsSettings):
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
phoenixd_api_password: str | None = Field(default=None)
phoenixd_api_endpoint: Optional[str] = Field(default="http://localhost:9740/")
phoenixd_api_password: Optional[str] = Field(default=None)
class AlbyFundingSource(LNbitsSettings):
alby_api_endpoint: str | None = Field(default="https://api.getalby.com/")
alby_access_token: str | None = Field(default=None)
alby_api_endpoint: Optional[str] = Field(default="https://api.getalby.com/")
alby_access_token: Optional[str] = Field(default=None)
class OpenNodeFundingSource(LNbitsSettings):
opennode_api_endpoint: str | None = Field(default=None)
opennode_key: str | None = Field(default=None)
opennode_admin_key: str | None = Field(default=None)
opennode_invoice_key: str | None = Field(default=None)
opennode_api_endpoint: Optional[str] = Field(default=None)
opennode_key: Optional[str] = Field(default=None)
opennode_admin_key: Optional[str] = Field(default=None)
opennode_invoice_key: Optional[str] = Field(default=None)
class SparkFundingSource(LNbitsSettings):
spark_url: str | None = Field(default=None)
spark_token: str | None = Field(default=None)
spark_url: Optional[str] = Field(default=None)
spark_token: Optional[str] = Field(default=None)
class LnTipsFundingSource(LNbitsSettings):
lntips_api_endpoint: str | None = Field(default=None)
lntips_api_key: str | None = Field(default=None)
lntips_admin_key: str | None = Field(default=None)
lntips_invoice_key: str | None = Field(default=None)
lntips_api_endpoint: Optional[str] = Field(default=None)
lntips_api_key: Optional[str] = Field(default=None)
lntips_admin_key: Optional[str] = Field(default=None)
lntips_invoice_key: Optional[str] = Field(default=None)
class NWCFundingSource(LNbitsSettings):
nwc_pairing_url: str | None = Field(default=None)
nwc_pairing_url: Optional[str] = Field(default=None)
class BreezSdkFundingSource(LNbitsSettings):
breez_api_key: str | None = Field(default=None)
breez_greenlight_seed: str | None = Field(default=None)
breez_greenlight_invite_code: str | None = Field(default=None)
breez_greenlight_device_key: str | None = Field(default=None)
breez_greenlight_device_cert: str | None = Field(default=None)
breez_use_trampoline: bool = Field(default=True)
breez_api_key: Optional[str] = Field(default=None)
breez_greenlight_seed: Optional[str] = Field(default=None)
breez_greenlight_invite_code: Optional[str] = Field(default=None)
breez_greenlight_device_key: Optional[str] = Field(default=None)
breez_greenlight_device_cert: Optional[str] = Field(default=None)
class BoltzFundingSource(LNbitsSettings):
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
boltz_client_macaroon: str | None = Field(default=None)
boltz_client_wallet: str | None = Field(default="lnbits")
boltz_client_cert: str | None = Field(default=None)
boltz_client_endpoint: Optional[str] = Field(default="127.0.0.1:9002")
boltz_client_macaroon: Optional[str] = Field(default=None)
boltz_client_wallet: Optional[str] = Field(default="lnbits")
boltz_client_cert: Optional[str] = Field(default=None)
class LightningSettings(LNbitsSettings):
lightning_invoice_expiry: int = Field(default=3600, gt=0)
lightning_invoice_expiry: int = Field(default=3600)
class FundingSourcesSettings(
@@ -582,14 +535,11 @@ class FundingSourcesSettings(
BreezSdkFundingSource,
):
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
# How long to wait for the payment to be confirmed before returning a pending status
# It will not fail the payment, it will make it return pending after the timeout
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
class WebPushSettings(LNbitsSettings):
lnbits_webpush_pubkey: str | None = Field(default=None)
lnbits_webpush_privkey: str | None = Field(default=None)
lnbits_webpush_pubkey: Optional[str] = Field(default=None)
lnbits_webpush_privkey: Optional[str] = Field(default=None)
class NodeUISettings(LNbitsSettings):
@@ -623,7 +573,7 @@ class AuthMethods(Enum):
class AuthSettings(LNbitsSettings):
auth_token_expire_minutes: int = Field(default=525600, gt=0)
auth_token_expire_minutes: int = Field(default=525600)
auth_all_methods = [a.value for a in AuthMethods]
auth_allowed_methods: list[str] = Field(
default=[
@@ -633,7 +583,7 @@ class AuthSettings(LNbitsSettings):
)
# How many seconds after login the user is allowed to update its credentials.
# A fresh login is required afterwards.
auth_credetials_update_threshold: int = Field(default=120, gt=0)
auth_credetials_update_threshold: int = Field(default=120)
def is_auth_method_allowed(self, method: AuthMethods):
return method.value in self.auth_allowed_methods
@@ -665,7 +615,7 @@ class AuditSettings(LNbitsSettings):
lnbits_audit_enabled: bool = Field(default=True)
# number of days to keep the audit entry
lnbits_audit_retention_days: int = Field(default=7, ge=0)
lnbits_audit_retention_days: int = Field(default=7)
lnbits_audit_log_ip_address: bool = Field(default=False)
lnbits_audit_log_path_params: bool = Field(default=True)
@@ -695,9 +645,9 @@ class AuditSettings(LNbitsSettings):
def audit_http_request(
self,
http_method: str | None = None,
path: str | None = None,
http_response_code: str | None = None,
http_method: Optional[str] = None,
path: Optional[str] = None,
http_response_code: Optional[str] = None,
) -> bool:
if not self.lnbits_audit_enabled:
return False
@@ -715,24 +665,24 @@ class AuditSettings(LNbitsSettings):
return True
def _is_http_request_path_auditable(self, path: str | None):
def _is_http_request_path_auditable(self, path: Optional[str]):
if len(self.lnbits_audit_exclude_paths) != 0 and path:
for exclude_path in self.lnbits_audit_exclude_paths:
if _re_fullmatch_safe(exclude_path, path):
return False
if len(self.lnbits_audit_include_paths) == 0:
return True
if not path:
return False
for include_path in self.lnbits_audit_include_paths:
if _re_fullmatch_safe(include_path, path):
return True
if len(self.lnbits_audit_include_paths) != 0:
if not path:
return False
for include_path in self.lnbits_audit_include_paths:
if _re_fullmatch_safe(include_path, path):
return True
return False
def _is_http_response_code_auditable(self, http_response_code: str | None) -> bool:
def _is_http_response_code_auditable(
self, http_response_code: Optional[str]
) -> bool:
if not http_response_code:
# No response code means only request filters should apply
return True
@@ -755,7 +705,6 @@ class EditableSettings(
FeeSettings,
ExchangeProvidersSettings,
SecuritySettings,
NotificationsSettings,
FundingSourcesSettings,
LightningSettings,
WebPushSettings,
@@ -802,7 +751,7 @@ class EnvSettings(LNbitsSettings):
debug_database: bool = Field(default=False)
bundle_assets: bool = Field(default=True)
host: str = Field(default="127.0.0.1")
port: int = Field(default=5000, gt=0)
port: int = Field(default=5000)
forwarded_allow_ips: str = Field(default="*")
lnbits_title: str = Field(default="LNbits API")
lnbits_path: str = Field(default=".")
@@ -814,27 +763,19 @@ class EnvSettings(LNbitsSettings):
enable_log_to_file: bool = Field(default=True)
log_rotation: str = Field(default="100 MB")
log_retention: str = Field(default="3 months")
cleanup_wallets_days: int = Field(default=90, ge=0)
funding_source_max_retries: int = Field(default=4, ge=0)
server_startup_time: int = Field(default=time())
cleanup_wallets_days: int = Field(default=90)
funding_source_max_retries: int = Field(default=4)
@property
def has_default_extension_path(self) -> bool:
return self.lnbits_extensions_path == "lnbits"
def check_auth_secret_key(self):
if self.auth_secret_key:
return
if not os.path.isdir(settings.lnbits_data_folder):
os.mkdir(settings.lnbits_data_folder)
auth_key_file = Path(settings.lnbits_data_folder, ".lnbits_auth_key")
if auth_key_file.is_file():
with open(auth_key_file) as file:
self.auth_secret_key = file.readline()
return
self.auth_secret_key = uuid4().hex
with open(auth_key_file, "w+") as file:
file.write(self.auth_secret_key)
class SaaSSettings(LNbitsSettings):
lnbits_saas_callback: Optional[str] = Field(default=None)
lnbits_saas_secret: Optional[str] = Field(default=None)
lnbits_saas_instance_id: Optional[str] = Field(default=None)
class PersistenceSettings(LNbitsSettings):
@@ -881,18 +822,6 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
# Long running while loops should use this flag instead of `while True:`
lnbits_running: bool = Field(default=True)
# Remember the latest balance delta in order to compare with the current one
latest_balance_delta_sats: int = Field(default=None)
lnbits_all_extensions_ids: set[str] = Field(default=[])
server_startup_time: int = Field(default=time())
@property
def lnbits_server_up_time(self) -> str:
up_time = int(time() - self.server_startup_time)
return strftime("%H:%M:%S", gmtime(up_time))
@classmethod
def readonly_fields(cls):
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
@@ -901,15 +830,12 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
class ReadOnlySettings(
EnvSettings,
ExtensionsInstallSettings,
SaaSSettings,
PersistenceSettings,
SuperUserSettings,
):
lnbits_admin_ui: bool = Field(default=True)
@property
def lnbits_extensions_upgrade_path(self) -> str:
return str(Path(self.lnbits_data_folder, "upgrades"))
@validator(
"lnbits_allowed_funding_sources",
pre=True,
@@ -938,7 +864,7 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
or user_id == self.super_user
)
def is_super_user(self, user_id: str | None = None) -> bool:
def is_super_user(self, user_id: Optional[str] = None) -> bool:
return user_id == self.super_user
def is_admin_user(self, user_id: str) -> bool:
@@ -947,8 +873,8 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
def is_admin_extension(self, ext_id: str) -> bool:
return ext_id in self.lnbits_admin_extensions
def is_installed_extension_id(self, ext_id: str) -> bool:
return ext_id in self.lnbits_installed_extensions_ids
def is_extension_id(self, ext_id: str) -> bool:
return ext_id in self.lnbits_all_extensions_ids
class SuperSettings(EditableSettings):
@@ -957,12 +883,12 @@ class SuperSettings(EditableSettings):
class AdminSettings(EditableSettings):
is_super_user: bool
lnbits_allowed_funding_sources: list[str] | None
lnbits_allowed_funding_sources: Optional[list[str]]
class SettingsField(BaseModel):
id: str
value: Any | None
value: Optional[Any]
tag: str = "core"
@@ -979,6 +905,31 @@ def set_cli_settings(**kwargs):
setattr(settings, key, value)
def send_admin_user_to_saas():
if settings.lnbits_saas_callback:
with httpx.Client() as client:
headers = {
"Content-Type": "application/json; charset=utf-8",
"X-API-KEY": settings.lnbits_saas_secret,
}
payload = {
"instance_id": settings.lnbits_saas_instance_id,
"adminuser": settings.super_user,
}
try:
client.post(
settings.lnbits_saas_callback,
headers=headers,
json=payload,
)
logger.success("sent super_user to saas application")
except Exception as e:
logger.error(
"error sending super_user to saas:"
f" {settings.lnbits_saas_callback}. Error: {e!s}"
)
readonly_variables = ReadOnlySettings.readonly_fields()
transient_variables = TransientSettings.readonly_fields()
@@ -987,8 +938,9 @@ settings = Settings()
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
settings.version = importlib.metadata.version("lnbits")
settings.check_auth_secret_key()
settings.auth_secret_key = (
settings.auth_secret_key or sha256(settings.super_user.encode("utf-8")).hexdigest()
)
if not settings.user_agent:
settings.user_agent = f"LNbits/{settings.version}"
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+21 -13
View File
File diff suppressed because one or more lines are too long
+7 -174
View File
@@ -1,11 +1,3 @@
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
[data-theme=classic] .q-drawer--dark,
body[data-theme=classic].body--dark,
[data-theme=classic] .q-menu--dark {
@@ -18,23 +10,11 @@ body[data-theme='classic'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
[data-theme=classic] .q-card--dark,
[data-theme=classic] .q-stepper--dark {
background: #333646 !important;
}
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
[data-theme=classic] .bg-primary {
background: #673ab7 !important;
}
@@ -72,14 +52,6 @@ body[data-theme=classic].body--light {
color: #fff !important;
}
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
[data-theme=bitcoin] .q-drawer--dark,
body[data-theme=bitcoin].body--dark,
[data-theme=bitcoin] .q-menu--dark {
@@ -92,34 +64,22 @@ body[data-theme='bitcoin'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
[data-theme=bitcoin] .q-card--dark,
[data-theme=bitcoin] .q-stepper--dark {
background: #333646 !important;
}
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
[data-theme=bitcoin] .bg-primary {
background: #ea611d !important;
background: #ff9853 !important;
}
[data-theme=bitcoin] .text-primary {
color: #ea611d !important;
color: #ff9853 !important;
}
[data-theme=bitcoin] .bg-secondary {
background: #e56f35 !important;
background: #ff7353 !important;
}
[data-theme=bitcoin] .text-secondary {
color: #e56f35 !important;
color: #ff7353 !important;
}
[data-theme=bitcoin] .bg-dark {
background: #2d293b !important;
@@ -134,10 +94,10 @@ body[data-theme=bitcoin].body--light {
color: #333646 !important;
}
[data-theme=bitcoin] .bg-marginal-bg {
background: #000000 !important;
background: #2d293b !important;
}
[data-theme=bitcoin] .text-marginal-bg {
color: #000000 !important;
color: #2d293b !important;
}
[data-theme=bitcoin] .bg-marginal-text {
background: #fff !important;
@@ -146,14 +106,6 @@ body[data-theme=bitcoin].body--light {
color: #fff !important;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
[data-theme=freedom] .q-drawer--dark,
body[data-theme=freedom].body--dark,
[data-theme=freedom] .q-menu--dark {
@@ -166,23 +118,11 @@ body[data-theme='freedom'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
[data-theme=freedom] .q-card--dark,
[data-theme=freedom] .q-stepper--dark {
background: #1b1b1b !important;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
[data-theme=freedom] .bg-primary {
background: #e22156 !important;
}
@@ -220,14 +160,6 @@ body[data-theme=freedom].body--light {
color: #fff !important;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
[data-theme=cyber] .q-drawer--dark,
body[data-theme=cyber].body--dark,
[data-theme=cyber] .q-menu--dark {
@@ -240,23 +172,11 @@ body[data-theme='cyber'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
[data-theme=cyber] .q-card--dark,
[data-theme=cyber] .q-stepper--dark {
background: #1b1b1b !important;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
[data-theme=cyber] .bg-primary {
background: #7cb342 !important;
}
@@ -294,14 +214,6 @@ body[data-theme=cyber].body--light {
color: #fff !important;
}
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
[data-theme=mint] .q-drawer--dark,
body[data-theme=mint].body--dark,
[data-theme=mint] .q-menu--dark {
@@ -314,23 +226,11 @@ body[data-theme='mint'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
[data-theme=mint] .q-card--dark,
[data-theme=mint] .q-stepper--dark {
background: #334642 !important;
}
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
[data-theme=mint] .bg-primary {
background: #3ab77d !important;
}
@@ -368,14 +268,6 @@ body[data-theme=mint].body--light {
color: #fff !important;
}
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
[data-theme=autumn] .q-drawer--dark,
body[data-theme=autumn].body--dark,
[data-theme=autumn] .q-menu--dark {
@@ -388,23 +280,11 @@ body[data-theme='autumn'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
[data-theme=autumn] .q-card--dark,
[data-theme=autumn] .q-stepper--dark {
background: #463f33 !important;
}
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
[data-theme=autumn] .bg-primary {
background: #b7763a !important;
}
@@ -442,14 +322,6 @@ body[data-theme=autumn].body--light {
color: rgb(255, 255, 255) !important;
}
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
[data-theme=flamingo] .q-drawer--dark,
body[data-theme=flamingo].body--dark,
[data-theme=flamingo] .q-menu--dark {
@@ -462,23 +334,11 @@ body[data-theme='flamingo'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
[data-theme=flamingo] .q-card--dark,
[data-theme=flamingo] .q-stepper--dark {
background: #bc23bc !important;
}
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
[data-theme=flamingo] .bg-primary {
background: #ff00ff !important;
}
@@ -516,14 +376,6 @@ body[data-theme=flamingo].body--light {
color: rgb(255, 255, 255) !important;
}
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
[data-theme=monochrome] .q-drawer--dark,
body[data-theme=monochrome].body--dark,
[data-theme=monochrome] .q-menu--dark {
@@ -536,23 +388,11 @@ body[data-theme='monochrome'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
[data-theme=monochrome] .q-card--dark,
[data-theme=monochrome] .q-stepper--dark {
background: rgb(39, 39, 39) !important;
}
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
[data-theme=monochrome] .bg-primary {
background: #494949 !important;
}
@@ -642,12 +482,6 @@ body.body--dark .q-field--error .q-field__messages {
width: 500px;
}
.lnbits__table-bordered td,
.lnbits__table-bordered th {
border: 1px solid black;
border-collapse: collapse;
}
.q-table--dense th:first-child,
.q-table--dense td:first-child,
.q-table--dense .q-table__bottom {
@@ -701,13 +535,12 @@ video {
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.qrcode__wrapper canvas {
width: 100% !important;
height: 100% !important;
max-width: 320px;
max-width: 350px;
}
.qrcode__image {
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+16 -198
View File
@@ -2,10 +2,8 @@ window.localisation.br = {
confirm: 'Sim',
server: 'Servidor',
theme: 'Tema',
site_customisation: 'Customização do Site',
funding: 'Financiamento',
users: 'Usuários',
audit: 'Auditoria',
apps: 'Aplicativos',
channels: 'Canais',
transactions: 'Transações',
@@ -28,11 +26,9 @@ window.localisation.br = {
restart: 'Reiniciar servidor',
save: 'Salvar',
save_tooltip: 'Salvar suas alterações',
credit_debit: 'Crédito / Débito',
credit_hint: 'Pressione Enter para creditar a conta',
credit_label: '{denomination} para creditar',
credit_ok:
'Sucesso ao creditar/debitar fundos virtuais ({amount} sats). Os pagamentos dependem dos fundos reais na fonte de financiamento.',
topup: 'Recarregar',
topup_wallet: 'Recarregar uma carteira',
topup_hint: 'Use o ID da carteira para recarregar qualquer carteira',
restart_tooltip: 'Reinicie o servidor para que as alterações tenham efeito',
add_funds_tooltip: 'Adicionar fundos a uma carteira.',
reset_defaults: 'Redefinir para padrões',
@@ -40,13 +36,14 @@ window.localisation.br = {
'Apagar todas as configurações e redefinir para os padrões.',
download_backup: 'Fazer backup do banco de dados',
name_your_wallet: 'Nomeie sua carteira {name}',
wallet_topup_ok:
'Sucesso ao criar fundos virtuais ({amount} sats). Pagamentos dependem dos fundos reais na fonte de financiamento.',
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
lnbits_description:
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da Lightning Network e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
export_to_phone: 'Exportar para o telefone com código QR',
export_to_phone_desc:
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
wallet: 'Carteira:',
wallets: 'Carteiras',
add_wallet: 'Adicionar nova carteira',
delete_wallet: 'Excluir carteira',
@@ -73,6 +70,8 @@ window.localisation.br = {
api_keys_api_docs: 'URL do Node, chaves da API e documentação da API',
lnbits_version: 'Versão do LNbits',
runs_on: 'Executa em',
credit_hint: 'Pressione Enter para creditar a conta',
credit_label: '{denomination} para creditar',
paste: 'Colar',
paste_from_clipboard: 'Cole do clipboard',
paste_request: 'Colar Pedido',
@@ -89,7 +88,7 @@ window.localisation.br = {
pay: 'Pagar',
memo: 'Memo',
date: 'Data',
payment_processing: 'Processando pagamento...',
processing_payment: 'Processando pagamento...',
not_enough_funds: 'Fundos insuficientes!',
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
invoice_waiting: 'Fatura aguardando pagamento',
@@ -108,15 +107,10 @@ window.localisation.br = {
'Funcionalidade de login a ser lançada em uma atualização futura, por enquanto, certifique-se de marcar esta página para acesso futuro à sua carteira! Este serviço está em BETA, e não nos responsabilizamos por pessoas que perderem o acesso aos fundos.',
no_transactions: 'Ainda não foram feitas transações',
manage: 'Gerenciar',
exchanges: 'Bolsas de valores',
extensions: 'Extensões',
no_extensions: 'Você não possui nenhuma extensão instalada :(',
created: 'Criado',
search_extensions: 'Extensões de pesquisa',
extension_sources: 'Fontes de Extensão',
ext_sources_hint: 'Repositórios de onde as extensões podem ser baixadas',
ext_sources_label:
'URL de origem (use apenas a fonte oficial da extensão LNbits e fontes confiáveis)',
warning: 'Aviso',
repository: 'Repositório',
confirm_continue: 'Você tem certeza de que deseja continuar?',
@@ -125,10 +119,8 @@ window.localisation.br = {
uninstall: 'Desinstalar',
drop_db: 'Remover Dados',
enable: 'Ativar',
pay_to_enable: 'Pague para Habilitar',
enable_extension_details: 'Ativar extensão para o usuário atual',
disable: 'Desativar',
delete: 'Excluir',
installed: 'Instalado',
activated: 'Ativado',
deactivated: 'Desativado',
@@ -153,19 +145,15 @@ window.localisation.br = {
'Você está prestes a remover todos os dados para a extensão. Por favor, digite o nome da extensão para continuar:',
extension_required_lnbits_version:
'Esta versão requer no mínimo a versão do LNbits',
min_version: 'Mínimo (incluído)',
max_version: 'Máximo (excluído)',
payment_hash: 'Hash de pagamento',
fee: 'Taxa',
amount: 'Quantidade',
amount_sats: 'Quantidade (sats)',
tag: 'Etiqueta',
unit: 'Unidade',
description: 'Descrição',
expiry: 'Validade',
webhook: 'Webhook',
payment_proof: 'Comprovante de pagamento',
update: 'Atualizar',
update_available: 'Atualização {version} disponível!',
latest_update: 'Você está na versão mais recente {version}.',
notifications: 'Notificações',
@@ -175,6 +163,12 @@ window.localisation.br = {
enable_notifications: 'Ativar notificações',
enable_notifications_desc:
'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.',
enable_killswitch: 'Ativar Killswitch',
enable_killswitch_desc:
'Se ativado, mudará sua fonte de fundos para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
killswitch_interval: 'Intervalo do Killswitch',
killswitch_interval_desc:
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNbits proveniente da fonte de status (em minutos).',
enable_watchdog: 'Ativar Watchdog',
enable_watchdog_desc:
'Se ativado, ele mudará automaticamente sua fonte de financiamento para VoidWallet se o seu saldo for inferior ao saldo do LNbits. Você precisará ativar manualmente após uma atualização.',
@@ -191,6 +185,7 @@ window.localisation.br = {
more: 'mais',
less: 'menos',
releases: 'Lançamentos',
killswitch: 'Dispositivo de desativação',
watchdog: 'Cão de guarda',
server_logs: 'Registros do Servidor',
ip_blocker: 'Bloqueador de IP',
@@ -222,7 +217,6 @@ window.localisation.br = {
login_to_account: 'Faça login na sua conta',
create_account: 'Criar conta',
account_settings: 'Configurações da Conta',
signin_with_nostr: 'Continuar com Nostr',
signin_with_google: 'Entrar com o Google',
signin_with_github: 'Entrar com GitHub',
signin_with_keycloak: 'Entrar com Keycloak',
@@ -231,14 +225,11 @@ window.localisation.br = {
password_config: 'Configuração de Senha',
password_repeat: 'Repetição de senha',
change_password: 'Alterar Senha',
update_credentials: 'Atualizar credenciais',
update_pubkey: 'Atualizar Chave Pública',
set_password: 'Definir Senha',
invalid_password: 'A senha deve ter pelo menos 8 caracteres',
login: 'Entrar',
register: 'Registrar',
username: 'Nome de usuário',
pubkey: 'Chave Pública',
user_id: 'ID do Usuário',
email: 'E-mail',
first_name: 'Primeiro Nome',
@@ -253,187 +244,14 @@ window.localisation.br = {
back: 'Voltar',
logout: 'Sair',
look_and_feel: 'Aparência',
toggle_gradient: 'Alternar Gradiente',
gradient_background: 'Fundo em Degradê',
language: 'Idioma',
color_scheme: 'Esquema de Cores',
admin_settings: 'Configurações do Administrador',
extension_cost: 'Este lançamento requer um pagamento mínimo de {cost} sats.',
extension_paid_sats: 'Você já pagou {paid_sats} sats.',
release_details_error: 'Não é possível obter os detalhes da versão.',
pay_from_wallet: 'Pagar com a Carteira',
wallet_required: 'Carteira *',
show_qr: 'Exibir QR',
retry_install: 'Repetir Instalação',
new_payment: 'Efetuar Novo Pagamento',
update_payment: 'Atualizar Pagamento',
already_paid_question: 'Você já pagou?',
sell: 'Vender',
sell_require: 'Peça pagamento para habilitar a extensão',
sell_info:
'A extensão {name} requer um pagamento mínimo de {amount} sats para habilitar.',
hide_empty_wallets: 'Ocultar carteiras vazias',
recheck: 'Verificar novamente',
contributors: 'Contribuidores',
license: 'Licença',
reset_key: 'Redefinir Chave',
reset_password: 'Redefinir senha',
border_choices: 'Opções de Borda',
select_all: 'Selecionar tudo',
nfc_supported: 'Compatível com NFC',
nfc_not_supported: 'NFC não suportado',
expire_date: 'Data de Expiração:',
hash: 'Hash:',
welcome_lnbits: 'Bem-vindo ao LNbits',
setup_su_account: 'Configure a conta Superuser abaixo.',
create_ticker_converter: 'Criar Conversor de Ticker de Moeda',
enable_audit: 'Habilitar Auditoria',
recommended: 'Recomendado',
audit_desc: 'Gravar solicitações HTTP de acordo com os filtros especificados',
audit_record_req: 'Gravar Corpo da Requisição',
audit_record_warning: 'Aviso:',
audit_record_req_warning_1:
'dados confidenciais (como senhas) serão registrados.',
audit_record_req_warning_2:
'o corpo da solicitação pode ter um tamanho grande.',
audit_record_use: 'Use com cuidado.',
audit_ip: 'Registrar endereço IP',
audit_ip_desc: 'Registre o endereço IP do cliente',
audit_path_params: 'Registrar Parâmetros de Caminho',
audit_query_params: 'Registrar Parâmetros de Consulta',
audit_http_methods: 'Incluir métodos HTTP',
audit_http_methods_hint:
'Lista de métodos HTTP a serem incluídos. Listas vazias significam todos.',
audit_http_methods_label: 'Métodos HTTP',
audit_resp_codes: 'Incluir Códigos de Resposta HTTP',
audit_resp_codes_hint:
'Lista de códigos HTTP a serem incluídos (correspondência regex). Listas vazias significam todos. Ex: 4.*, 5.*',
audit_resp_codes_label: 'Código de resposta HTTP (regex)',
audit_paths: 'Incluir Caminhos',
audit_paths_hint:
'Lista de caminhos a serem incluídos (correspondência de regex). Lista vazia significa todos.',
audit_paths_label: 'Caminho HTTP (regex)',
audit_paths_exclude: 'Excluir Caminhos',
audit_paths_exclude_hint:
'Lista de caminhos a serem excluídos (correspondência regex). Lista vazia significa nenhum.',
audit_paths_exclude_label: 'Caminho HTTP (regex)',
exchange_providers: 'Provedores de Câmbio',
admin_extensions: 'Extensões de Administração',
admin_extensions_label: 'Extensões de administração',
admin_extensions_hint:
'Somente usuários com privilégios de administrador podem usar extensões.',
user_default_extensions: 'Extensões Padrão do Usuário',
user_default_extensions_label: 'Extensões do usuário',
user_default_extensions_hint:
'Extensões que serão ativadas por padrão para os usuários.',
miscellanous: 'Diversos',
misc_disable_extensions: 'Desativar extensões',
misc_disable_extensions_label: 'Desativar todas as extensões',
misc_hide_api: 'Ocultar API',
misc_hide_api_label:
'Oculta a API de carteira, extensões podem optar por honrar',
wallets_management: 'Gerenciamento de Carteiras',
funding_source_info: 'Informações da Fonte de Financiamento',
funding_source: 'Fonte de Financiamento: {wallet_class}',
node_balance: 'Saldo do Nó: {balance} sats',
lnbits_balance: 'Saldo do LNbits: {balance} sats',
funding_reserve_percent: 'Reserve Percentual: {percent} %',
node_managment: 'Gerenciamento de Nós',
node_management_not_supported:
'Gerenciamento de nó não suportado pela fonte de financiamento ativa',
toggle_node_ui: 'Interface do Nó',
toggle_public_node_ui: 'Interface Pública do Nó',
toggle_transactions_node_ui:
'Guia de Transações (Desativar em nós grandes CLN)',
invoice_expiry: 'Expiração da Fatura',
invoice_expiry_label: 'Validade da fatura (segundos)',
fee_reserve: 'Reserva de Taxa',
fee_reserve_msats: 'Taxa de reserva em msats',
fee_reserve_percent: 'Taxa de reserva em porcentagem',
server_management: 'Gerenciamento de Servidor',
base_url: 'URL base',
base_url_label: 'URL estática/base para o servidor',
authentication: 'Autenticação',
auth_token_expiry_label: 'Minutos para expiração do token',
auth_token_expiry_hint: 'Tempo em minutos até o token expirar',
auth_allowed_methods_label: 'Métodos de autorização permitidos',
auth_allowed_methods_hint: 'Selecione métodos de autorização',
auth_nostr_label: 'URL de Solicitação Nostr',
auth_nostr_hint: 'URL absoluta que os clientes usarão para fazer login.',
auth_google_ci_label: 'ID do Cliente do Google',
auth_google_ci_hint:
'Certifique-se de que os URIs de redirecionamento autorizados contenham https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Segredo do Cliente do Google',
auth_gh_client_id_label: 'ID do Cliente do GitHub',
auth_gh_client_id_hint:
'Certifique-se de que a URL de callback de autorização esteja definida como https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'Segredo do Cliente do GitHub',
auth_keycloak_label: 'URL de Descoberta do Keycloak',
auth_keycloak_ci_label: 'ID do Cliente Keycloak',
auth_keycloak_ci_hint:
'Certifique-se de que a URL de retorno de chamada de autorização esteja definida para https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Segredo do Cliente Keycloak',
currency_settings: 'Configurações de Moeda',
allowed_currencies: 'Moedas Permitidas',
allowed_currencies_hint: 'Limite o número de moedas fiduciárias disponíveis',
default_account_currency: 'Moeda Padrão da Conta',
default_account_currency_hint: 'Moeda padrão para contabilidade',
service_fee_label: 'Taxa de serviço (%)',
service_fee_hint: 'Taxa cobrada por tx (%)',
service_fee_max_label: 'Taxa de serviço máx (sats)',
service_fee_max_hint: 'Taxa máxima de serviço a cobrar em (sats)',
fee_wallet: 'Carteira de Taxas',
fee_wallet_label: 'Carteira de tarifas (ID da carteira)',
fee_wallet_hint: 'ID da carteira para enviar fundos para',
disable_fee: 'Desativar Taxa',
disable_fee_internal: 'Desativar taxa de serviço para pagamentos internos',
disable_fee_internal_desc:
'Desativar Taxa de Serviço para Pagamentos Internos Lightning',
ui_management: 'Gerenciamento de UI',
ui_site_title: 'Título do Site',
ui_site_tagline: 'Tagline do site',
ui_elements_enable: 'Habilitar elementos na página inicial',
ui_elements_disable: 'Desativar elementos na página inicial',
ui_toggle_elements_tip:
"Remover elementos da página inicial, como 'funciona com', etc.",
ui_site_description: 'Descrição do Site',
ui_site_description_hint: 'Use texto simples, Markdown ou HTML bruto',
ui_default_wallet_name: 'Nome Padrão da Carteira',
lnbits_wallet: 'Carteira LNbits',
denomination: 'Denominação',
denomination_hint: 'O nome para o token FakeWallet',
ui_qr_code_logo: 'Logo do QR Code',
ui_qr_code_logo_hint: 'URL para imagem de logo no código QR',
ui_custom_badge: 'Distintivo Personalizado',
ui_custom_badge_label:
"Distintivo Personalizado 'USE COM CUIDADO - a carteira LNbits ainda está em BETA'",
ui_custom_badge_color_label: 'Cor Personalizada do Distintivo',
themes: 'Temas',
themes_hint: 'Escolha temas disponíveis para usuários',
custom_logo: 'Logotipo personalizado',
custom_logo_hint: 'URL para a imagem do logotipo',
ad_space_title: 'Título do Espaço Publicitário',
ad_space_title_label: 'Suportado por',
ad_slots: 'Slots de Anúncio',
ad_slots_hint:
'Adicionar URL e caminhos de arquivo de imagem no formato CSV, as extensões podem optar por honrar',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Anúncios Ativados',
ads_disabled: 'Anúncios Desativados',
user_management: 'Gerenciamento de Usuários',
admin_users: 'Usuários Administradores',
admin_users_hint: 'Usuários com privilégios de administrador',
admin_users_label: 'ID do Usuário',
allowed_users: 'Usuários Permitidos',
allowed_users_hint: 'Somente esses usuários podem usar o LNbits',
allowed_users_label: 'ID do Usuário',
allow_creation_user: 'Permitir a criação de novos usuários',
allow_creation_user_desc:
'Permitir a criação de novos usuários na página de índice',
components: 'Componentes',
long_running_endpoints: 'Top 5 Endpoints de Longa Execução',
http_request_methods: 'Métodos de Requisição HTTP',
http_response_codes: 'Códigos de Resposta HTTP',
request_details: 'Detalhes do Pedido',
http_request_details: 'Detalhes da Requisição HTTP'
hide_empty_wallets: 'Ocultar carteiras vazias'
}
+16 -184
View File
@@ -2,10 +2,8 @@ window.localisation.cn = {
confirm: '确定',
server: '服务器',
theme: '主题',
site_customisation: '网站定制',
funding: '资金',
users: '用户',
audit: '审计',
apps: '应用程序',
channels: '频道',
transactions: '交易记录',
@@ -28,24 +26,23 @@ window.localisation.cn = {
restart: '重新启动服务器',
save: '保存',
save_tooltip: '保存更改',
credit_debit: '信用卡 / 借记卡',
credit_hint: '按 Enter 键充值账户',
credit_label: '{denomination} 充值',
credit_ok:
'成功记入/扣除虚拟资金 ({amount} sats)。付款取决于资金来源的实际资金。',
topup: '充值',
topup_wallet: '给钱包充值',
topup_hint: '使用钱包ID为任何钱包充值',
restart_tooltip: '重新启动服务器以使更改生效',
add_funds_tooltip: '为钱包添加资金',
reset_defaults: '重置为默认设置',
reset_defaults_tooltip: '删除所有设置并重置为默认设置',
download_backup: '下载数据库备份',
name_your_wallet: '给你的 {name}钱包起个名字',
wallet_topup_ok:
'成功创建虚拟资金({amount} sats)。付款取决于资金来源的实际资金。',
paste_invoice_label: '粘贴发票,付款请求或lnurl*',
lnbits_description:
'LNbits 设置简单、轻量级,可以在任何闪电网络的资金来源上运行,甚至可以在LNbits自身上运行!您可以为自己运行LNbits,或者轻松为他人提供托管解决方案。每个钱包都有自己的 API 密钥,你可以创建的钱包数量没有限制。能够把资金分开管理使 LNbits 成为一款有用的资金管理和开发工具。扩展程序增加了 LNbits 的额外功能,所以你可以在闪电网络上尝试各种尖端技术。我们已经尽可能简化了开发扩展程序的过程,作为一个免费和开源的项目,我们鼓励人们开发并提交自己的扩展程序。',
export_to_phone: '通过二维码导出到手机',
export_to_phone_desc:
'这个二维码包含您钱包的URL。您可以使用手机扫描的方式打开您的钱包。',
wallet: '钱包:',
wallets: '钱包',
add_wallet: '添加新钱包',
delete_wallet: '删除钱包',
@@ -70,6 +67,8 @@ window.localisation.cn = {
api_keys_api_docs: '节点URL、API密钥和API文档',
lnbits_version: 'LNbits版本',
runs_on: '可运行在',
credit_hint: '按 Enter 键充值账户',
credit_label: '{denomination} 充值',
paste: '粘贴',
paste_from_clipboard: '从剪贴板粘贴',
paste_request: '粘贴请求',
@@ -86,7 +85,7 @@ window.localisation.cn = {
pay: '付款',
memo: '备注',
date: '日期',
payment_processing: '正在处理支付...',
processing_payment: '正在处理支付...',
not_enough_funds: '资金不足!',
search_by_tag_memo_amount: '按标签、备注、金额搜索',
invoice_waiting: '待支付的发票',
@@ -105,15 +104,10 @@ window.localisation.cn = {
'登录功能将在以后的更新中发布,请将此页面加为书签,以便将来访问您的钱包!此服务处于测试阶段,我们不对资金的丢失承担任何责任。',
no_transactions: '尚未进行任何交易',
manage: '管理',
exchanges: '交易所',
extensions: '扩展程序',
no_extensions: '你没有安装任何扩展程序 :(',
created: '已创建',
search_extensions: '搜索扩展程序',
extension_sources: '扩展源',
ext_sources_hint: '可以下载扩展的存储库',
ext_sources_label:
'来源网址(仅使用官方LNbits扩展程序来源和您可以信任的来源)',
warning: '警告',
repository: '代码库',
confirm_continue: '你确定要继续吗?',
@@ -122,10 +116,8 @@ window.localisation.cn = {
uninstall: '卸载',
drop_db: '删除数据',
enable: '启用',
pay_to_enable: '支付以启用',
enable_extension_details: '为当前用户启用扩展程序',
disable: '禁用',
delete: '删除',
installed: '已安装',
activated: '已激活',
deactivated: '已停用',
@@ -145,19 +137,15 @@ window.localisation.cn = {
extension_db_drop_warning:
'您即将删除该扩展的所有数据。请继续输入扩展程序名称以确认操作:',
extension_required_lnbits_version: '此版本要求最低的 LNbits 版本为',
min_version: '最小值(包含)',
max_version: '最大值(不含)',
payment_hash: '付款哈希',
fee: '费',
amount: '金额',
amount_sats: '金额(聪)',
tag: '标签',
unit: '单位',
description: '详情',
expiry: '过期时间',
webhook: 'Webhook',
payment_proof: '付款证明',
update: '更新',
update_available: '更新{version}可用!',
latest_update: '您当前使用的是最新版本{version}。',
notifications: '通知',
@@ -166,7 +154,12 @@ window.localisation.cn = {
enable_notifications: '启用通知',
enable_notifications_desc:
'如果启用,它将获取最新的LNbits状态更新,如安全事件和更新。',
enable_killswitch: '启用紧急停止开关',
enable_killswitch_desc:
'如果启用,当LNbits发送终止信号时,系统将自动将您的资金来源更改为VoidWallet。更新后,您将需要手动启用。',
killswitch_interval: 'Killswitch 间隔',
killswitch_interval_desc:
'后台任务应该多久检查一次来自状态源的LNbits断路信号(以分钟为单位)。',
enable_watchdog: '启用看门狗',
enable_watchdog_desc:
'如果启用,当您的余额低于LNbits余额时,系统将自动将您的资金来源更改为VoidWallet。更新后您将需要手动启用。',
@@ -182,6 +175,7 @@ window.localisation.cn = {
more: '更多',
less: '少',
releases: '版本',
killswitch: '杀手锏',
watchdog: '监控程序',
server_logs: '服务器日志',
ip_blocker: 'IP 阻止器',
@@ -212,7 +206,6 @@ window.localisation.cn = {
login_to_account: '登录您的账户',
create_account: '创建账户',
account_settings: '账户设置',
signin_with_nostr: '继续使用 Nostr',
signin_with_google: '使用谷歌账号登录',
signin_with_github: '使用GitHub登录',
signin_with_keycloak: '使用Keycloak登录',
@@ -221,14 +214,11 @@ window.localisation.cn = {
password_config: '密码配置',
password_repeat: '密码重复',
change_password: '修改密码',
update_credentials: '更新凭证',
update_pubkey: '更新公钥',
set_password: '设置密码',
invalid_password: '密码至少需要有8个字符',
login: '登录',
register: '注册',
username: '用户名',
pubkey: '公钥',
user_id: '用户ID',
email: '电子邮件',
first_name: '名字',
@@ -243,172 +233,14 @@ window.localisation.cn = {
back: '返回',
logout: '注销',
look_and_feel: '外观和感觉',
toggle_gradient: '切换渐变',
gradient_background: '渐变背景',
language: '语言',
color_scheme: '配色方案',
admin_settings: '管理员设置',
extension_cost: '此版本需要支付最低 {cost} sats。',
extension_paid_sats: '您已经支付了{paid_sats} sats。',
release_details_error: '无法获取发布详情。',
pay_from_wallet: '从钱包支付',
wallet_required: '钱包 *',
show_qr: '显示QR码',
retry_install: '重试安装',
new_payment: '创建新支付',
update_payment: '更新付款',
already_paid_question: '你已经付款了吗?',
sell: '出售',
sell_require: '请求付款以启用扩展',
sell_info: '{name} 扩展需要支付至少 {amount} sat 才能启用。',
hide_empty_wallets: '隐藏空钱包',
recheck: '重新检查',
contributors: '贡献者们',
license: '许可证',
reset_key: '重置密钥',
reset_password: '重置密码',
border_choices: '边框选项',
select_all: '全选',
nfc_supported: '支持NFC',
nfc_not_supported: '不支持NFC',
expire_date: '有效期:',
hash: '哈希:',
welcome_lnbits: '欢迎来到LNbits',
setup_su_account: '设置超级用户账户如下。',
create_ticker_converter: '创建货币代码转换器',
enable_audit: '启用审核',
recommended: '推荐',
audit_desc: '根据指定的过滤器记录HTTP请求',
audit_record_req: '记录请求主体',
audit_record_warning: '警告:',
audit_record_req_warning_1: '机密数据(如密码)将被记录。',
audit_record_req_warning_2: '请求主体可能会有较大尺寸。',
audit_record_use: '请谨慎使用。',
audit_ip: '记录 IP 地址',
audit_ip_desc: '记录客户端的IP地址',
audit_path_params: '记录路径参数',
audit_query_params: '记录查询参数',
audit_http_methods: '包括 HTTP 方法',
audit_http_methods_hint: '要包含的 HTTP 方法列表。空列表表示全部。',
audit_http_methods_label: 'HTTP 方法',
audit_resp_codes: '包括 HTTP 响应代码',
audit_resp_codes_hint:
'要包含的 HTTP 代码列表(正则表达式匹配)。空列表表示全部。例如:4.*,5.*',
audit_resp_codes_label: 'HTTP响应代码(正则表达式)',
audit_paths: '包含路径',
audit_paths_hint: '要包含的路径列表(正则表达式匹配)。空列表意味着全部。',
audit_paths_label: 'HTTP 路径(正则表达式)',
audit_paths_exclude: '排除路径',
audit_paths_exclude_hint:
'要排除的路径列表(正则表达式匹配)。空列表表示没有。',
audit_paths_exclude_label: 'HTTP 路径(正则表达式)',
exchange_providers: '兑换提供商',
admin_extensions: '管理员扩展',
admin_extensions_label: '管理员扩展件',
admin_extensions_hint: '只有具有管理员权限的用户才能使用扩展程序',
user_default_extensions: '用户默认扩展',
user_default_extensions_label: '用户扩展',
user_default_extensions_hint: '对用户默认启用的扩展。',
miscellanous: '杂项',
misc_disable_extensions: '禁用扩展程序',
misc_disable_extensions_label: '禁用所有扩展程序',
misc_hide_api: '隐藏 API',
misc_hide_api_label: '隐藏钱包 api,扩展程序可以选择遵守',
wallets_management: '钱包管理',
funding_source_info: '资金来源信息',
funding_source: '资金来源:{wallet_class}',
node_balance: '节点余额:{balance} sats',
lnbits_balance: 'LNbits 余额:{balance} sats',
funding_reserve_percent: '保留百分比: {percent} %',
node_managment: '节点管理',
node_management_not_supported: '活动资金来源不支持节点管理',
toggle_node_ui: '节点用户界面',
toggle_public_node_ui: '公共节点用户界面',
toggle_transactions_node_ui: '交易选项卡(在大型 CLN 节点上禁用)',
invoice_expiry: '发票到期',
invoice_expiry_label: '发票到期(秒)',
fee_reserve: '费用储备',
fee_reserve_msats: '以msats计的保留费',
fee_reserve_percent: '以百分比计的保留费用',
server_management: '服务器管理',
base_url: '基本URL',
base_url_label: '服务器的静态/基本网址',
authentication: '认证',
auth_token_expiry_label: '令牌过期分钟数',
auth_token_expiry_hint: '令牌过期的剩余时间(分钟)',
auth_allowed_methods_label: '允许的授权方法',
auth_allowed_methods_hint: '选择授权方法',
auth_nostr_label: 'Nostr请求URL',
auth_nostr_hint: '客户端将用于登录的绝对URL。',
auth_google_ci_label: '谷歌客户ID',
auth_google_ci_hint:
'确保授权重定向URI包含https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Google客户端密钥',
auth_gh_client_id_label: 'GitHub 客户端 ID',
auth_gh_client_id_hint:
'确保授权回调 URL 设置为 https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'GitHub客户端密码',
auth_keycloak_label: 'Keycloak 发现 URL',
auth_keycloak_ci_label: 'Keycloak 客户端 ID',
auth_keycloak_ci_hint:
'确保授权回调URL设置为https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Keycloak客户端密钥',
currency_settings: '货币设置',
allowed_currencies: '允许的货币',
allowed_currencies_hint: '限制可用法定货币的数量',
default_account_currency: '默认账户货币',
default_account_currency_hint: '默认的会计货币',
service_fee_label: '服务费 (%)',
service_fee_hint: '每笔交易收取的费用 (%)',
service_fee_max_label: '服务费最大值(聪)',
service_fee_max_hint: '最大服务费以 (sats) 收取',
fee_wallet: '费用钱包',
fee_wallet_label: '费用钱包(钱包 ID',
fee_wallet_hint: '用于接收资金的钱包 ID',
disable_fee: '禁用费用',
disable_fee_internal: '禁用内部付款服务费',
disable_fee_internal_desc: '禁用内部闪电支付的服务费',
ui_management: '用户界面管理',
ui_site_title: '网站标题',
ui_site_tagline: '网站标语',
ui_elements_enable: '在主页上启用元素',
ui_elements_disable: '禁用主页上的元素',
ui_toggle_elements_tip: '移除主页元素,例如“运行于”等。',
ui_site_description: '网站描述',
ui_site_description_hint: '使用纯文本、Markdown或原始HTML',
ui_default_wallet_name: '默认钱包名称',
lnbits_wallet: 'LNbits 钱包',
denomination: '面额',
denomination_hint: 'FakeWallet 代币的名称',
ui_qr_code_logo: '二维码标志',
ui_qr_code_logo_hint: '二维码中标志图像的 URL',
ui_custom_badge: '自定义徽章',
ui_custom_badge_label: '自定义徽章“慎用 - LNbits 钱包仍在测试阶段”',
ui_custom_badge_color_label: '自定义徽章颜色',
themes: '主题',
themes_hint: '选择可供用户使用的主题',
custom_logo: '自定义徽标',
custom_logo_hint: '徽标图像的URL',
ad_space_title: '广告位标题',
ad_space_title_label: '由...支持',
ad_slots: '广告位',
ad_slots_hint: '广告网址和图像文件路径以CSV格式存储,扩展可以选择遵循。',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: '启用广告',
ads_disabled: '广告已禁用',
user_management: '用户管理',
admin_users: '管理员用户',
admin_users_hint: '具有管理员权限的用户',
admin_users_label: '用户ID',
allowed_users: '允许的用户',
allowed_users_hint: '仅这些用户可以使用LNbits',
allowed_users_label: '用户 ID',
allow_creation_user: '允许创建新用户',
allow_creation_user_desc: '允许在索引页面上创建新用户',
components: '组件',
long_running_endpoints: '前五个长时间运行的端点',
http_request_methods: 'HTTP请求方法',
http_response_codes: 'HTTP响应代码',
request_details: '请求详情',
http_request_details: 'HTTP请求详细信息'
hide_empty_wallets: '隐藏空钱包'
}
+19 -196
View File
@@ -2,10 +2,8 @@ window.localisation.cs = {
confirm: 'Ano',
server: 'Server',
theme: 'Téma',
site_customisation: 'Přizpůsobení stránek',
funding: 'Financování',
users: 'Uživatelé',
audit: 'Audit',
apps: 'Aplikace',
channels: 'Kanály',
transactions: 'Transakce',
@@ -28,24 +26,23 @@ window.localisation.cs = {
restart: 'Restartovat server',
save: 'Uložit',
save_tooltip: 'Uložit změny',
credit_debit: 'Kreditní / Debetní',
credit_hint: 'Stiskněte Enter pro připsání na účet',
credit_label: '{denomination} k připsání',
credit_ok:
'Úspěšné připsání/odepsání virtuálních prostředků ({amount} satů). Platby závisí na skutečných prostředcích z financujícího zdroje.',
topup: 'Dobít',
topup_wallet: 'Dobít peněženku',
topup_hint: 'Použijte ID peněženky pro dobíjení jakékoliv peněženky',
restart_tooltip: 'Restartujte server pro aplikaci změn',
add_funds_tooltip: 'Přidat prostředky do peněženky.',
reset_defaults: 'Obnovit výchozí',
reset_defaults_tooltip: 'Smazat všechna nastavení a obnovit výchozí.',
download_backup: 'Stáhnout zálohu databáze',
name_your_wallet: 'Pojmenujte svou {name} peněženku',
wallet_topup_ok:
'Úspěšně vytvořeny virtuální prostředky ({amount} sats). Platby závisí na skutečných prostředcích na zdrojovém účtu.',
paste_invoice_label: 'Vložte fakturu, platební požadavek nebo lnurl kód *',
lnbits_description:
'Snadno nastavitelný a lehkotonážní, LNbits může běžet na jakémkoliv zdroji financování Lightning Network a dokonce LNbits samotné! LNbits můžete provozovat pro sebe, nebo snadno nabízet správu peněženek pro ostatní. Každá peněženka má své vlastní API klíče a není omezen počet peněženek, které můžete vytvořit. Možnost rozdělení prostředků dělá z LNbits užitečný nástroj pro správu peněz a jako vývojový nástroj. Rozšíření přidávají extra funkčnost k LNbits, takže můžete experimentovat s řadou špičkových technologií na lightning network. Vývoj rozšíření jsme učinili co nejjednodušší a jako svobodný a open-source projekt podporujeme lidi ve vývoji a zasílání vlastních rozšíření.',
export_to_phone: 'Exportovat do telefonu pomocí QR kódu',
export_to_phone_desc:
'Tento QR kód obsahuje URL vaší peněženky s plným přístupem. Můžete jej naskenovat z telefonu a otevřít peněženku odtamtud.',
wallet: 'Peněženka:',
wallets: 'Peněženky',
add_wallet: 'Přidat novou peněženku',
delete_wallet: 'Smazat peněženku',
@@ -74,6 +71,8 @@ window.localisation.cs = {
api_keys_api_docs: 'Adresa uzlu, API klíče a API dokumentace',
lnbits_version: 'Verze LNbits',
runs_on: 'Běží na',
credit_hint: 'Stiskněte Enter pro připsání na účet',
credit_label: '{denomination} k připsání',
paste: 'Vložit',
paste_from_clipboard: 'Vložit ze schránky',
paste_request: 'Vložit požadavek',
@@ -90,7 +89,7 @@ window.localisation.cs = {
pay: 'Platit',
memo: 'Poznámka',
date: 'Datum',
payment_processing: 'Zpracování platby...',
processing_payment: 'Zpracování platby...',
not_enough_funds: 'Nedostatek prostředků!',
search_by_tag_memo_amount: 'Hledat podle tagu, poznámky, částky',
invoice_waiting: 'Faktura čeká na platbu',
@@ -109,15 +108,10 @@ window.localisation.cs = {
'Funkcionalita přihlášení bude vydána v budoucí aktualizaci, zatím si ujistěte, že jste si tuto stránku uložili do záložek pro budoucí přístup k vaší peněžence! Tato služba je v BETA verzi a nepřebíráme žádnou zodpovědnost za ztrátu přístupu k prostředkům.',
no_transactions: 'Zatím žádné transakce',
manage: 'Spravovat',
exchanges: 'Burzy',
extensions: 'Rozšíření',
no_extensions: 'Nemáte nainstalováno žádné rozšíření :(',
created: 'Vytvořeno',
search_extensions: 'Hledat rozšíření',
extension_sources: 'Zdroje rozšíření',
ext_sources_hint: 'Úložiště, odkud lze rozšíření stáhnout.',
ext_sources_label:
'Zdrojová URL (používejte pouze oficiální zdroj rozšíření LNbits a zdroje, kterým můžete důvěřovat)',
warning: 'Varování',
repository: 'Repositář',
confirm_continue: 'Jste si jistí, že chcete pokračovat?',
@@ -126,10 +120,8 @@ window.localisation.cs = {
uninstall: 'Odinstalovat',
drop_db: 'Odstranit data',
enable: 'Povolit',
pay_to_enable: 'Zaplatit pro aktivaci',
enable_extension_details: 'Povolit rozšíření pro aktuálního uživatele',
disable: 'Zakázat',
delete: 'Smazat',
installed: 'Nainstalováno',
activated: 'Aktivováno',
deactivated: 'Deaktivováno',
@@ -153,19 +145,15 @@ window.localisation.cs = {
'Chystáte se odstranit všechna data pro rozšíření. Prosím, pokračujte zadáním názvu rozšíření:',
extension_required_lnbits_version:
'Toto vydání vyžaduje alespoň verzi LNbits',
min_version: 'Minimum (včetně)',
max_version: 'Maximální (vyloučeno)',
payment_hash: 'Hash platby',
fee: 'Poplatek',
amount: 'Částka',
amount_sats: 'Částka (sats)',
tag: 'Tag',
unit: 'Jednotka',
description: 'Popis',
expiry: 'Expirace',
webhook: 'Webhook',
payment_proof: 'Důkaz platby',
update: 'Aktualizovat',
update_available: 'Dostupná aktualizace {version}!',
latest_update: 'Máte nejnovější verzi {version}.',
notifications: 'Notifikace',
@@ -174,6 +162,15 @@ window.localisation.cs = {
enable_notifications: 'Povolit notifikace',
enable_notifications_desc:
'Pokud je povoleno, bude stahovat nejnovější aktualizace stavu LNbits, jako jsou bezpečnostní incidenty a aktualizace.',
enable_killswitch: 'Povolit Killswitch',
enable_killswitch_desc:
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud LNbits odešle signál killswitch. Po aktualizaci budete muset povolit ručně.',
killswitch_interval: 'Interval Killswitch',
killswitch_interval_desc:
'Jak často by měl úkol na pozadí kontrolovat signál killswitch od LNbits ze zdroje stavu (v minutách).',
enable_watchdog: 'Povolit Watchdog',
enable_watchdog_desc:
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud je váš zůstatek nižší než zůstatek LNbits. Po aktualizaci budete muset povolit ručně.',
watchdog_interval: 'Interval Watchdog',
watchdog_interval_desc:
'Jak často by měl úkol na pozadí kontrolovat signál killswitch v watchdog delta [node_balance - lnbits_balance] (v minutách).',
@@ -187,6 +184,7 @@ window.localisation.cs = {
more: 'více',
less: 'méně',
releases: 'Vydání',
killswitch: 'Killswitch',
watchdog: 'Watchdog',
server_logs: 'Logy serveru',
ip_blocker: 'Blokování IP',
@@ -218,7 +216,6 @@ window.localisation.cs = {
login_to_account: 'Přihlaste se ke svému účtu',
create_account: 'Vytvořit účet',
account_settings: 'Nastavení účtu',
signin_with_nostr: 'Pokračovat s Nostr',
signin_with_google: 'Přihlásit se přes Google',
signin_with_github: 'Přihlásit se přes GitHub',
signin_with_keycloak: 'Přihlásit se přes Keycloak',
@@ -227,14 +224,11 @@ window.localisation.cs = {
password_config: 'Konfigurace hesla',
password_repeat: 'Opakujte heslo',
change_password: 'Změnit heslo',
update_credentials: 'Aktualizovat přihlašovací údaje',
update_pubkey: 'Aktualizovat veřejný klíč',
set_password: 'Nastavit heslo',
invalid_password: 'Heslo musí mít alespoň 8 znaků',
login: 'Přihlášení',
register: 'Registrovat',
username: 'Uživatelské jméno',
pubkey: 'Veřejný klíč',
user_id: 'ID uživatele',
email: 'Email',
first_name: 'Křestní jméno',
@@ -249,185 +243,14 @@ window.localisation.cs = {
back: 'Zpět',
logout: 'Odhlásit se',
look_and_feel: 'Vzhled a chování',
toggle_gradient: 'Přepnout gradient',
gradient_background: 'Barevný přechod pozadí',
language: 'Jazyk',
color_scheme: 'Barevné schéma',
admin_settings: 'Nastavení administrátora',
extension_cost: 'Toto vydání vyžaduje minimální platbu {cost} satoshi.',
extension_paid_sats: 'Již jste zaplatili {paid_sats} sats.',
release_details_error: 'Nelze získat podrobnosti o vydání.',
pay_from_wallet: 'Platit z peněženky',
wallet_required: 'Peněženka *',
show_qr: 'Zobrazit QR',
retry_install: 'Zkusit znovu nainstalovat',
new_payment: 'Vytvořit novou platbu',
update_payment: 'Aktualizovat platbu',
already_paid_question: 'Už jste zaplatili?',
sell: 'Prodat',
sell_require: 'Požádejte o platbu, abyste povolili rozšíření',
sell_info:
'Rozšíření {name} vyžaduje platbu minimálně {amount} sats pro aktivaci.',
hide_empty_wallets: 'Skrýt prázdné peněženky',
recheck: 'Znovu zkontrolovat',
contributors: 'Přispěvatelé',
license: 'Licence',
reset_key: 'Obnovit klíč',
reset_password: 'Obnovit heslo',
border_choices: 'Možnosti ohraničení',
select_all: 'Vybrat vše',
nfc_supported: 'Podpora NFC',
nfc_not_supported: 'NFC není podporováno',
expire_date: 'Datum expirace:',
hash: 'Hash:',
welcome_lnbits: 'Vítejte v LNbits',
setup_su_account: 'Nastavte účet Superuser níže.',
create_ticker_converter: 'Vytvořit převodník měnových tickerů',
enable_audit: 'Povolit audit',
recommended: 'Doporučeno',
audit_desc: 'Zaznamenávejte HTTP požadavky podle zadaných filtrů',
audit_record_req: 'Záznam Tělo Požadavku',
audit_record_warning: 'Varování:',
audit_record_req_warning_1: 'důvěrná data (jako hesla) budou zaznamenána.',
audit_record_req_warning_2: 'tělo žádosti může mít velkou velikost.',
audit_record_use: 'Používejte to opatrně.',
audit_ip: 'Zaznamenat IP adresu',
audit_ip_desc: 'Zaznamenejte IP adresu klienta',
audit_path_params: 'Zaznamenat parametry cesty',
audit_query_params: 'Zaznamenat parametry dotazu',
audit_http_methods: 'Zahrnout metody HTTP',
audit_http_methods_hint:
'Seznam metod HTTP, které mají být zahrnuty. Prázdné seznamy znamenají všechny.',
audit_http_methods_label: 'Metody HTTP',
audit_resp_codes: 'Zahrnout kódy odpovědí HTTP',
audit_resp_codes_hint:
'Seznam kódů HTTP, které mají být zahrnuty (regex match). Prázdné seznamy znamenají všechny. Např.: 4.*, 5.*',
audit_resp_codes_label: 'Kód odpovědi HTTP (regex)',
audit_paths: 'Zahrnout cesty',
audit_paths_hint:
'Seznam cest, které mají být zahrnuty (regex shoda). Prázdný seznam znamená vše.',
audit_paths_label: 'HTTP cesta (regex)',
audit_paths_exclude: 'Vyloučit cesty',
audit_paths_exclude_hint:
'Seznam cest, které mají být vyloučeny (regex shoda). Prázdný seznam znamená žádné.',
audit_paths_exclude_label: 'HTTP cesta (regex)',
exchange_providers: 'Poskytovatelé směny',
admin_extensions: 'Rozšíření pro správce',
admin_extensions_label: 'Administrátorské rozšíření',
admin_extensions_hint:
'Rozšíření může používat pouze uživatel s administrátorskými oprávněními.',
user_default_extensions: 'Výchozí rozšíření uživatele',
user_default_extensions_label: 'Uživatelská rozšíření',
user_default_extensions_hint:
'Rozšíření, která budou u uživatelů ve výchozím nastavení povolena.',
miscellanous: 'Různé',
misc_disable_extensions: 'Zakázat rozšíření',
misc_disable_extensions_label: 'Zakázat všechna rozšíření',
misc_hide_api: 'Skrýt API',
misc_hide_api_label:
'Skrývá API peněženky, rozšíření se mohou rozhodnout ctít',
wallets_management: 'Správa peněženek',
funding_source_info: 'Informace o zdroji financování',
funding_source: 'Zdroj financování: {wallet_class}',
node_balance: 'Stav uzlu: {balance} sats',
lnbits_balance: 'Zůstatek LNbits: {balance} sats',
funding_reserve_percent: 'Rezervovat procento: {percent} %',
node_managment: 'Správa uzlů',
node_management_not_supported:
'Správa uzlů není podporována aktivním zdrojem financování',
toggle_node_ui: 'Uživatelské rozhraní uzlu',
toggle_public_node_ui: 'Veřejné rozhraní uzlu',
toggle_transactions_node_ui:
'Karta Transakce (Zakázat na velkých uzlech CLN)',
invoice_expiry: 'Datum vypršení faktury',
invoice_expiry_label: 'Vypršení faktury (sekundy)',
fee_reserve: 'Rezerva poplatku',
fee_reserve_msats: 'Rezervační poplatek v msats',
fee_reserve_percent: 'Rezervační poplatek v procentech',
server_management: 'Správa serveru',
base_url: 'Základní URL',
base_url_label: 'Statická/Základní URL pro server',
authentication: 'Ověření',
auth_token_expiry_label: 'Minuty vypršení platnosti tokenu',
auth_token_expiry_hint: 'Čas v minutách do vypršení tokenu',
auth_allowed_methods_label: 'Povolené metody autorizace',
auth_allowed_methods_hint: 'Vyberte metody autorizace',
auth_nostr_label: 'URL žádosti Nostr',
auth_nostr_hint: 'Absolutní URL, které klienti použijí pro přihlášení.',
auth_google_ci_label: 'ID klienta Google',
auth_google_ci_hint:
'Ujistěte se, že autorizované přesměrovací URI obsahují https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Heslo klienta Google',
auth_gh_client_id_label: 'ID klienta GitHub',
auth_gh_client_id_hint:
'Ujistěte se, že je nastavena zpětná adresa URL pro autorizaci na https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'GitHub Klientský tajný klíč',
auth_keycloak_label: 'URL pro zjištění Keycloak',
auth_keycloak_ci_label: 'ID klienta Keycloak',
auth_keycloak_ci_hint:
'Ujistěte se, že je autorizace callback URL nastavena na https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Klíč k aplikaci Keycloak tajemství',
currency_settings: 'Nastavení měny',
allowed_currencies: 'Povolené měny',
allowed_currencies_hint: 'Omezte počet dostupných fiat měn',
default_account_currency: 'Výchozí měna účtu',
default_account_currency_hint: 'Výchozí měna pro účetnictví',
service_fee_label: 'Poplatek za službu (%)',
service_fee_hint: 'Poplatek účtovaný za transakci (%)',
service_fee_max_label: 'Poplatek za službu max (sats)',
service_fee_max_hint: 'Maximální poplatek za službu k účtování v (sats)',
fee_wallet: 'Poplatková peněženka',
fee_wallet_label: 'Poplatková peněženka (ID peněženky)',
fee_wallet_hint: 'ID peněženky, na kterou se mají odeslat prostředky',
disable_fee: 'Zakázat poplatek',
disable_fee_internal: 'Zakázat poplatek za službu pro interní platby',
disable_fee_internal_desc:
'Zakázat servisní poplatek za interní lightning platby',
ui_management: 'Správa uživatelského rozhraní',
ui_site_title: 'Název stránky',
ui_site_tagline: 'Stránkový slogan',
ui_elements_enable: 'Povolit prvky na domovské stránce',
ui_elements_disable: 'Zakázat prvky na úvodní stránce',
ui_toggle_elements_tip:
"Odebrat prvky z domovské stránky, jako je 'běží na' atd.",
ui_site_description: 'Popis webu',
ui_site_description_hint: 'Použijte prostý text, Markdown nebo surové HTML.',
ui_default_wallet_name: 'Výchozí název peněženky',
lnbits_wallet: 'Peněženka LNbits',
denomination: 'Nominální hodnota',
denomination_hint: 'Název pro token FakeWallet',
ui_qr_code_logo: 'Logo QR kódu',
ui_qr_code_logo_hint: 'URL k obrázku loga v QR kódu',
ui_custom_badge: 'Vlastní odznak',
ui_custom_badge_label:
"Vlastní odznak 'POUŽÍVEJTE S OPATRNOSTÍ - Peněženka LNbits je stále v BETA verzi'",
ui_custom_badge_color_label: 'Barva vlastního odznaku',
themes: 'Motivy',
themes_hint: 'Vyberte motivy dostupné pro uživatele',
custom_logo: 'Vlastní logo',
custom_logo_hint: 'URL k obrázku loga',
ad_space_title: 'Název reklamního prostoru',
ad_space_title_label: 'Podporováno',
ad_slots: 'Reklamní sloty',
ad_slots_hint:
'Adresa URL reklamy a cesty k souborům obrázků ve formátu CSV, rozšíření se mohou rozhodnout respektovat',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Reklamy povoleny',
ads_disabled: 'Reklamy deaktivovány',
user_management: 'Správa uživatelů',
admin_users: 'Administrátorští uživatelé',
admin_users_hint: 'Uživatelé s administrátorskými oprávněními',
admin_users_label: 'ID uživatele',
allowed_users: 'Povolení uživatelé',
allowed_users_hint: 'Pouze tito uživatelé mohou používat LNbits.',
allowed_users_label: 'Uživatelské ID',
allow_creation_user: 'Povolit vytvoření nových uživatelů',
allow_creation_user_desc:
'Povolit vytváření nových uživatelů na úvodní stránce',
components: 'Soubory',
long_running_endpoints: 'Top 5 dlouho běžících koncových bodů',
http_request_methods: 'Metody HTTP požadavků',
http_response_codes: 'Kódy HTTP odpovědí',
request_details: 'Podrobnosti žádosti',
http_request_details: 'Podrobnosti HTTP žádosti'
hide_empty_wallets: 'Skrýt prázdné peněženky'
}
+16 -201
View File
@@ -2,10 +2,8 @@ window.localisation.de = {
confirm: 'Ja',
server: 'Server',
theme: 'Theme',
site_customisation: 'Website-Anpassung',
funding: 'Funding',
users: 'Benutzer',
audit: 'Prüfung',
apps: 'Apps',
channels: 'Kanäle',
transactions: 'Transaktionen',
@@ -28,11 +26,9 @@ window.localisation.de = {
restart: 'Server neu starten',
save: 'Speichern',
save_tooltip: 'Änderungen speichern',
credit_debit: 'Kredit / Debit',
credit_hint: 'Klicke Enter, um das Konto zu belasten',
credit_label: '{denomination} zu belasten',
credit_ok:
'Erfolgreiches Gutschreiben/Abziehen von virtuellen Geldern ({amount} Sats). Zahlungen hängen von den tatsächlichen Mitteln der Finanzierungsquelle ab.',
topup: 'Aufladen',
topup_wallet: 'Wallet aufladen',
topup_hint: 'Nutze die Wallet-ID, um eine beliebige Wallet aufzuladen',
restart_tooltip: 'Starte den Server neu, um die Änderungen zu übernehmen',
add_funds_tooltip: 'Füge Geld zu einer Wallet hinzu.',
reset_defaults: 'Zurücksetzen',
@@ -40,6 +36,8 @@ window.localisation.de = {
'Alle Einstellungen auf die Standardeinstellungen zurücksetzen.',
download_backup: 'Datenbank-Backup herunterladen',
name_your_wallet: 'Vergib deiner {name} Wallet einen Namen',
wallet_topup_ok:
'Erfolg beim Erstellen von virtuellen Mitteln ({amount} Satoshis). Zahlungen hängen von den tatsächlichen Mitteln der Finanzierungsquelle ab.',
paste_invoice_label:
'Füge eine Rechnung, Zahlungsanforderung oder LNURL ein *',
lnbits_description:
@@ -47,7 +45,6 @@ window.localisation.de = {
export_to_phone: 'Auf dem Telefon öffnen',
export_to_phone_desc:
'Dieser QR-Code beinhaltet vollständige Rechte auf deine Wallet. Du kannst den QR-Code mit Deinem Telefon scannen, um deine Wallet dort zu öffnen.',
wallet: 'Brieftasche:',
wallets: 'Wallets',
add_wallet: 'Wallet hinzufügen',
delete_wallet: 'Wallet löschen',
@@ -74,6 +71,8 @@ window.localisation.de = {
api_keys_api_docs: 'Knoten-URL, API-Schlüssel und API-Dokumentation',
lnbits_version: 'LNbits-Version',
runs_on: 'Läuft auf',
credit_hint: 'Klicke Enter, um das Konto zu belasten',
credit_label: '{denomination} zu belasten',
paste: 'Einfügen',
paste_from_clipboard: 'Einfügen aus der Zwischenablage',
paste_request: 'Anfrage einfügen',
@@ -91,7 +90,7 @@ window.localisation.de = {
pay: 'Zahlen',
memo: 'Memo',
date: 'Datum',
payment_processing: 'Zahlung wird verarbeitet ...',
processing_payment: 'Zahlung wird verarbeitet ...',
not_enough_funds: 'Geldmittel sind erschöpft!',
search_by_tag_memo_amount: 'Suche nach Tag, Memo, Betrag',
invoice_waiting: 'Rechnung wartend auf Zahlung',
@@ -110,16 +109,10 @@ window.localisation.de = {
'Login-Funktionalität wird in einem zukünftigen Update veröffentlicht. Bis dahin ist die Speicherung der Wallet-URL als Lesezeichen absolut notwendig, um Zugriff auf die Wallet zu erhalten! Dieser Service ist in BETA und wir übernehmen keine Verantwortung für Verluste durch verlorene Zugriffe.',
no_transactions: 'Keine Transaktionen',
manage: 'Verwalten',
exchanges: 'Börsenplätze',
extensions: 'Erweiterungen',
no_extensions: 'Du hast noch keine Erweiterungen installiert :(',
created: 'Erstellt',
search_extensions: 'Sucherweiterungen',
extension_sources: 'Erweiterungsquellen',
ext_sources_hint:
'Repositorys, von denen die Erweiterungen heruntergeladen werden können.',
ext_sources_label:
'Quell-URL (verwenden Sie nur die offizielle LNbits-Erweiterungsquelle und vertrauenswürdige Quellen)',
warning: 'Warnung',
repository: 'Repository',
confirm_continue: 'Bist du sicher, dass du fortfahren möchtest?',
@@ -128,10 +121,8 @@ window.localisation.de = {
uninstall: 'Deinstallieren',
drop_db: 'Daten löschen',
enable: 'Aktivieren',
pay_to_enable: 'Zahlen Sie zum Aktivieren',
enable_extension_details: 'Erweiterung für aktuellen Benutzer aktivieren',
disable: 'Deaktivieren',
delete: 'Löschen',
installed: 'Installiert',
activated: 'Aktiviert',
deactivated: 'Deaktiviert',
@@ -156,19 +147,15 @@ window.localisation.de = {
'Sie sind dabei, alle Daten für die Erweiterung zu entfernen. Bitte geben Sie den Namen der Erweiterung ein, um fortzufahren:',
extension_required_lnbits_version:
'Diese Version erfordert mindestens die LNbits-Version',
min_version: 'Mindestwert (inklusive)',
max_version: 'Maximalwert (ausgeschlossen)',
payment_hash: 'Zahlungs-Hash',
fee: 'Gebühr',
amount: 'Menge',
amount_sats: 'Betrag (sats)',
tag: 'Tag',
unit: 'Einheit',
description: 'Beschreibung',
expiry: 'Ablauf',
webhook: 'Webhook',
payment_proof: 'Beleg',
update: 'Aktualisieren',
update_available: 'Aktualisierung {version} verfügbar!',
latest_update: 'Sie sind auf der neuesten Version {version}.',
notifications: 'Benachrichtigungen',
@@ -177,6 +164,12 @@ window.localisation.de = {
enable_notifications: 'Aktiviere Benachrichtigungen',
enable_notifications_desc:
'Wenn aktiviert, werden die neuesten LNbits-Statusaktualisierungen, wie Sicherheitsvorfälle und Updates, abgerufen.',
enable_killswitch: 'Aktivieren Sie den Notausschalter',
enable_killswitch_desc:
'Falls aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn LNbits ein Killswitch-Signal sendet. Nach einem Update müssen Sie dies manuell wieder aktivieren.',
killswitch_interval: 'Intervall für den Notausschalter',
killswitch_interval_desc:
'Wie oft die Hintergrundaufgabe nach dem LNbits-Killswitch-Signal aus der Statusquelle suchen soll (in Minuten).',
enable_watchdog: 'Aktiviere Watchdog',
enable_watchdog_desc:
'Wenn aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn Ihr Guthaben niedriger als das LNbits-Guthaben ist. Nach einem Update müssen Sie dies manuell aktivieren.',
@@ -193,7 +186,7 @@ window.localisation.de = {
more: 'mehr',
less: 'weniger',
releases: 'Veröffentlichungen',
killswitch: 'Killswitch',
watchdog: 'Wachhund',
server_logs: 'Serverprotokolle',
ip_blocker: 'IP-Sperre',
@@ -227,7 +220,6 @@ window.localisation.de = {
login_to_account: 'Melden Sie sich bei Ihrem Konto an',
create_account: 'Konto erstellen',
account_settings: 'Kontoeinstellungen',
signin_with_nostr: 'Mit Nostr fortfahren',
signin_with_google: 'Mit Google anmelden',
signin_with_github: 'Anmelden mit GitHub',
signin_with_keycloak: 'Mit Keycloak anmelden',
@@ -236,14 +228,11 @@ window.localisation.de = {
password_config: 'Passwortkonfiguration',
password_repeat: 'Passwortwiederholung',
change_password: 'Passwort ändern',
update_credentials: 'Anmeldeinformationen aktualisieren',
update_pubkey: 'Öffentlichen Schlüssel aktualisieren',
set_password: 'Passwort festlegen',
invalid_password: 'Das Passwort muss mindestens 8 Zeichen haben.',
login: 'Anmelden',
register: 'Registrieren',
username: 'Benutzername',
pubkey: 'Öffentlicher Schlüssel',
user_id: 'Benutzer-ID',
email: 'E-Mail',
first_name: 'Vorname',
@@ -258,189 +247,15 @@ window.localisation.de = {
back: 'Zurück',
logout: 'Abmelden',
look_and_feel: 'Aussehen und Verhalten',
toggle_gradient: 'Verlauf umschalten',
gradient_background: 'Verlaufs-Hintergrund',
language: 'Sprache',
color_scheme: 'Farbschema',
admin_settings: 'Admin-Einstellungen',
extension_cost:
'Diese Version erfordert eine Zahlung von mindestens {cost} Sats.',
extension_paid_sats: 'Sie haben bereits {paid_sats} Sats bezahlt.',
release_details_error: 'Kann die Details zur Veröffentlichung nicht abrufen.',
pay_from_wallet: 'Zahlen aus dem Geldbeutel',
wallet_required: 'Wallet *',
show_qr: 'QR anzeigen',
retry_install: 'Installieren erneut versuchen',
new_payment: 'Neue Zahlung vornehmen',
update_payment: 'Zahlung aktualisieren',
already_paid_question: 'Haben Sie schon bezahlt?',
sell: 'Verkaufen',
sell_require: 'Zahlung anfordern, um die Erweiterung zu aktivieren',
sell_info:
'Die {name}-Erweiterung erfordert eine Zahlung von mindestens {amount} Satoshis, um aktiviert zu werden.',
hide_empty_wallets: 'Leere Geldbörsen verbergen',
recheck: 'Erneut überprüfen',
contributors: 'Mitwirkende',
license: 'Lizenz',
reset_key: 'Zurücksetzen-Schlüssel',
reset_password: 'Passwort zurücksetzen',
border_choices: 'Randoptionen',
select_all: 'Alles auswählen',
nfc_supported: 'NFC unterstützt',
nfc_not_supported: 'NFC wird nicht unterstützt',
expire_date: 'Ablaufdatum:',
hash: 'Hash:',
welcome_lnbits: 'Willkommen bei LNbits',
setup_su_account: 'Richten Sie das Superuser-Konto unten ein.',
create_ticker_converter: 'Währungsticker-Konverter erstellen',
enable_audit: 'Audit aktivieren',
recommended: 'Empfohlen',
audit_desc: 'HTTP-Anfragen entsprechend den angegebenen Filtern aufzeichnen',
audit_record_req: 'Anfragekörper aufzeichnen',
audit_record_warning: 'Warnung:',
audit_record_req_warning_1:
'Vertrauliche Daten (wie Passwörter) werden protokolliert.',
audit_record_req_warning_2: 'Der Anfragetext kann groß sein.',
audit_record_use: 'Verwenden Sie es mit Vorsicht.',
audit_ip: 'IP-Adresse aufzeichnen',
audit_ip_desc: 'Speichern Sie die IP-Adresse des Clients',
audit_path_params: 'Pfadparameter aufzeichnen',
audit_query_params: 'Abfrageparameter aufzeichnen',
audit_http_methods: 'HTTP-Methoden einschließen',
audit_http_methods_hint:
'Liste der HTTP-Methoden, die einbezogen werden sollen. Leere Listen bedeuten alle.',
audit_http_methods_label: 'HTTP-Methoden',
audit_resp_codes: 'HTTP-Antwortcodes einbeziehen',
audit_resp_codes_hint:
'Liste der einzuschließenden HTTP-Codes (regex-Match). Leere Liste bedeutet alle. Z.B.: 4.*, 5.*',
audit_resp_codes_label: 'HTTP-Antwortcode (Regex)',
audit_paths: 'Einfügepfade',
audit_paths_hint:
'Liste der aufzunehmenden Pfade (Regex-Übereinstimmung). Leere Liste bedeutet alle.',
audit_paths_label: 'HTTP-Pfad (Regex)',
audit_paths_exclude: 'Pfade ausschließen',
audit_paths_exclude_hint:
'Liste der auszuschließenden Pfade (regex-Match). Leere Liste bedeutet keine.',
audit_paths_exclude_label: 'HTTP-Pfad (Regex)',
exchange_providers: 'Austauschdienste',
admin_extensions: 'Admin-Erweiterungen',
admin_extensions_label: 'Admin-Erweiterungen',
admin_extensions_hint:
'Nur Benutzer mit Admin-Rechten können Erweiterungen verwenden.',
user_default_extensions: 'Standarderweiterungen des Benutzers',
user_default_extensions_label: 'Benutzererweiterungen',
user_default_extensions_hint:
'Erweiterungen, die standardmäßig für die Benutzer aktiviert werden.',
miscellanous: 'Verschiedenes',
misc_disable_extensions: 'Erweiterungen deaktivieren',
misc_disable_extensions_label: 'Alle Erweiterungen deaktivieren',
misc_hide_api: 'API ausblenden',
misc_hide_api_label: 'Verbirgt Wallet-API, Erweiterungen können es ehren',
wallets_management: 'Verwaltung von Geldbörsen',
funding_source_info: 'Finanzierungsquelleninformationen',
funding_source: 'Finanzierungsquelle: {wallet_class}',
node_balance: 'Kontostand: {balance} Sats',
lnbits_balance: 'LNbits-Guthaben: {balance} Sats',
funding_reserve_percent: 'Reservieren Prozent: {percent} %',
node_managment: 'Knotenverwaltung',
node_management_not_supported:
'Knotenverwaltung wird von der aktiven Finanzierungsquelle nicht unterstützt',
toggle_node_ui: 'Node-Benutzeroberfläche',
toggle_public_node_ui: 'Öffentliche Knoten-Benutzeroberfläche',
toggle_transactions_node_ui:
'Transaktionen-Tab (Bei großen CLN-Knoten deaktivieren)',
invoice_expiry: 'Rechnungsablauf',
invoice_expiry_label: 'Rechnungsablauf (Sekunden)',
fee_reserve: 'Gebührenreserve',
fee_reserve_msats: 'Reservierungsgebühr in msats',
fee_reserve_percent: 'Reservierungsgebühr in Prozent',
server_management: 'Serververwaltung',
base_url: 'Basis-URL',
base_url_label: 'Statische/Basis-URL für den Server',
authentication: 'Authentifizierung',
auth_token_expiry_label: 'Token-Ablaufminuten',
auth_token_expiry_hint: 'Zeit in Minuten bis der Token abläuft',
auth_allowed_methods_label: 'Erlaubte Autorisierungsmethoden',
auth_allowed_methods_hint: 'Wählen Sie Autorisierungsmethoden aus',
auth_nostr_label: 'Nostr-Anforderungs-URL',
auth_nostr_hint: 'Absolute URL, die die Clients für die Anmeldung verwenden.',
auth_google_ci_label: 'Google-Client-ID',
auth_google_ci_hint:
'Stellen Sie sicher, dass die autorisierten Umleitungs-URIs https://{domain}/api/v1/auth/google/token enthalten',
auth_google_cs_label: 'Google-Client-Geheimnis',
auth_gh_client_id_label: 'GitHub-Client-ID',
auth_gh_client_id_hint:
'Stellen Sie sicher, dass die URL für den Autorisierungsrückruf auf https://{domain}/api/v1/auth/github/token gesetzt ist.',
auth_gh_client_secret_label: 'GitHub-Client-Geheimnis',
auth_keycloak_label: 'Keycloak Discovery-URL',
auth_keycloak_ci_label: 'Keycloak-Client-ID',
auth_keycloak_ci_hint:
'Stellen Sie sicher, dass die Autorisierungs-Callback-URL auf https://{domain}/api/v1/auth/keycloak/token eingestellt ist.',
auth_keycloak_cs_label: 'Keycloak-Client-Geheimnis',
currency_settings: 'Währungseinstellungen',
allowed_currencies: 'Erlaubte Währungen',
allowed_currencies_hint:
'Begrenzen Sie die Anzahl der verfügbaren Fiat-Währungen',
default_account_currency: 'Standardkontowährung',
default_account_currency_hint: 'Standardwährung für Buchhaltung',
service_fee_label: 'Servicegebühr (%)',
service_fee_hint: 'Gebühr pro Transaktion (%)',
service_fee_max_label: 'Servicegebühr max. (sats)',
service_fee_max_hint: 'Maximale Servicegebühr in (sats) berechnen.',
fee_wallet: 'Gebühren-Wallet',
fee_wallet_label: 'Gebühren-Wallet (Wallet-ID)',
fee_wallet_hint: 'Wallet-ID, an die Gelder gesendet werden sollen',
disable_fee: 'Gebühr deaktivieren',
disable_fee_internal:
'Dienstleistungsgebühr für interne Zahlungen deaktivieren',
disable_fee_internal_desc:
'Dienstleistungsgebühr für interne Lightning-Zahlungen deaktivieren',
ui_management: 'UI-Verwaltung',
ui_site_title: 'Seitentitel',
ui_site_tagline: 'Seitenslogan',
ui_elements_enable: 'Elemente auf der Startseite aktivieren',
ui_elements_disable: 'Elemente auf der Startseite deaktivieren',
ui_toggle_elements_tip:
"Entfernen Sie Homepage-Elemente wie 'läuft auf' usw.",
ui_site_description: 'Seitenbeschreibung',
ui_site_description_hint:
'Verwenden Sie einfachen Text, Markdown oder rohes HTML',
ui_default_wallet_name: 'Standard-Walletname',
lnbits_wallet: 'LNbits-Wallet',
denomination: 'Nomination',
denomination_hint: 'Der Name für das FakeWallet-Token',
ui_qr_code_logo: 'QR-Code-Logo',
ui_qr_code_logo_hint: 'URL zum Logo-Bild im QR-Code',
ui_custom_badge: 'Benutzerdefiniertes Abzeichen',
ui_custom_badge_label:
"Benutzerdefiniertes Abzeichen 'MIT VORSICHT VERWENDEN - LNbits-Wallet ist noch in der BETA-Phase'",
ui_custom_badge_color_label: 'Benutzerdefinierte Abzeichenfarbe',
themes: 'Themen',
themes_hint: 'Wählen Sie Themen, die für Benutzer verfügbar sind',
custom_logo: 'Benutzerdefiniertes Logo',
custom_logo_hint: 'URL zum Logobild',
ad_space_title: 'Anzeigentitel',
ad_space_title_label: 'Unterstützt von',
ad_slots: 'Werbeplätze',
ad_slots_hint:
'URL-Adressen und Bilddateipfade im CSV-Format, Erweiterungen können darauf achten',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Anzeigen aktiviert',
ads_disabled: 'Anzeigen deaktiviert',
user_management: 'Benutzerverwaltung',
admin_users: 'Admin-Benutzer',
admin_users_hint: 'Benutzer mit Administratorrechten',
admin_users_label: 'Benutzer-ID',
allowed_users: 'Zugelassene Benutzer',
allowed_users_hint: 'Nur diese Benutzer können LNbits verwenden.',
allowed_users_label: 'Benutzer-ID',
allow_creation_user: 'Erlauben Sie die Erstellung neuer Benutzer',
allow_creation_user_desc:
'Erlauben Sie das Erstellen neuer Benutzer auf der Indexseite',
components: 'Komponenten',
long_running_endpoints: 'Top 5 lang laufende Endpunkte',
http_request_methods: 'HTTP-Anfragemethoden',
http_response_codes: 'HTTP-Antwortcodes',
request_details: 'Anfragedetails',
http_request_details: 'HTTP-Anfragedetails'
hide_empty_wallets: 'Leere Geldbörsen verbergen'
}
+21 -317
View File
@@ -6,7 +6,6 @@ window.localisation.en = {
funding: 'Funding',
users: 'Users',
audit: 'Audit',
api_watch: 'Api Watch',
apps: 'Apps',
channels: 'Channels',
transactions: 'Transactions',
@@ -27,7 +26,7 @@ window.localisation.en = {
close_channel: 'Close Channel',
close: 'Close',
restart: 'Restart server',
image_library: 'Image Library',
background_image: 'Background Image',
save: 'Save',
save_tooltip: 'Save your changes',
credit_debit: 'Credit / Debit',
@@ -47,8 +46,6 @@ window.localisation.en = {
export_to_phone: 'Export to Phone with QR Code',
export_to_phone_desc:
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
access_wallet_on_mobile: 'Mobile Access',
wallet: 'Wallet: ',
wallets: 'Wallets',
add_wallet: 'Add a new wallet',
delete_wallet: 'Delete wallet',
@@ -60,14 +57,12 @@ window.localisation.en = {
currency: 'Currency',
update_currency: 'Update currency',
press_to_claim: 'Press to claim bitcoin',
claim_desc:
'It seems you have a claimable amount of bitcoin but you dont have a wallet yet. Press the button below to claim it. This will create a new wallet for you.',
donate: 'Donate',
view_github: 'View on GitHub',
voidwallet_active: 'VoidWallet is active! Payments disabled',
service_fee_badge: 'Service fee: {amount} % per transaction',
service_fee_max_badge:
'Service fee: {amount} % per transaction (max {max} {denom})',
use_with_caution: 'USE WITH CAUTION - {name} wallet is still in BETA',
service_fee: 'Service fee: {amount} % per transaction',
service_fee_max: 'Service fee: {amount} % per transaction (max {max} sats)',
service_fee_tooltip:
'Service fee charged by the LNbits server admin per outgoing transaction',
toggle_darkmode: 'Toggle Dark Mode',
@@ -83,7 +78,6 @@ window.localisation.en = {
create_invoice: 'Create Invoice',
camera_tooltip: 'Use camera to scan an invoice/QR',
export_csv: 'Export to CSV',
export_csv_details: 'Export to CSV with details',
chart_tooltip: 'Show chart',
pending: 'Pending',
copy_invoice: 'Copy invoice',
@@ -91,22 +85,15 @@ window.localisation.en = {
cancel: 'Cancel',
scan: 'Scan',
read: 'Read',
write: 'Write',
pay: 'Pay',
memo: 'Memo',
date: 'Date',
path: 'Path',
payment_processing: 'Processing payment...',
payment_processing: 'Processing payment...',
payment_successful: 'Payment successful!',
payment_pending: 'Payment pending...',
payment_check: 'Check payment',
processing_payment: 'Processing payment...',
not_enough_funds: 'Not enough funds!',
search_by_tag_memo_amount: 'Search by tag, memo, amount',
invoice_waiting: 'Invoice waiting to be paid',
payment_received: 'Payment Received',
payment_sent: 'Payment Sent',
payment_failed: 'Payment Failed',
receive: 'receive',
send: 'send',
outgoing_payment_pending: 'Outgoing payment pending',
@@ -117,7 +104,7 @@ window.localisation.en = {
copy_wallet_url: 'Copy wallet URL',
disclaimer_dialog_title: 'Important!',
disclaimer_dialog:
'You *must* save your login credentials to be able to access your wallet again. If you lose them, you will lose access to your wallet and funds.\n\nFind your login credentials on your account settings page.\n\nLNbits holds no responsibility for loss of access to funds.',
'You *must* save your login credentials to be able to access your wallet again. If you lose them, you will lose access to your wallet and funds.\n\nFind your login credentials on your account settings page.\n\nThis service is in BETA. LNbits holds no responsibility for loss of access to funds.',
no_transactions: 'No transactions made yet',
manage: 'Manage',
exchanges: 'Exchanges',
@@ -125,10 +112,6 @@ window.localisation.en = {
no_extensions: "You don't have any extensions installed :(",
created: 'Created',
search_extensions: 'Search extensions',
extension_sources: 'Extension Sources',
ext_sources_hint: 'Repositories from where the extensions can be downloaded',
ext_sources_label:
'Source URL (only use the official LNbits extension source, and sources you can trust)',
warning: 'Warning',
repository: 'Repository',
confirm_continue: 'Are you sure you want to continue?',
@@ -176,81 +159,20 @@ window.localisation.en = {
payment_proof: 'Payment Proof',
update: 'Update',
update_available: 'Update {version} available!',
funding_sources: 'Funding Sources',
latest_update: 'You are on the latest version {version}.',
notifications: 'Notifications',
notifications_configure: 'Configure Notifications',
notifications_nostr_config: 'Nostr Configuration',
notifications_enable_nostr: 'Enable Nostr',
notifications_enable_nostr_desc: 'Send notfications over Nostr',
notifications_nostr_private_key: 'Nostr Private Key',
notifications_nostr_private_key_desc:
'Private key (hex or nsec) to sign the messages sent to Nostr',
notifications_nostr_identifiers: 'Nostr Identifiers',
notifications_nostr_identifiers_desc:
'List of identifiers to send notifications to',
notifications_telegram_config: 'Telegram Configuration',
notifications_enable_telegram: 'Enable Telegram',
notifications_enable_telegram_desc: 'Send notfications over Telegram',
notifications_telegram_access_token: 'Access Token',
notifications_telegram_access_token_desc: 'Access token for the bot',
notifications_chat_id: 'Chat ID',
notifications_chat_id_desc: 'Chat ID to send the notifications to',
notifications_email_config: 'Email Configuration',
notifications_enable_email: 'Enable Email',
notifications_enable_email_desc: 'Send notifications over email',
notifications_send_test_email: 'Send test email',
notifications_send_email: 'Send email',
notifications_send_email_desc: 'Email you will send from',
notifications_send_email_username: 'Username',
notifications_send_email_username_desc:
'Username, will use the email if not set',
notifications_send_email_password: 'Send email password',
notifications_send_email_password_desc:
'Password for the email you will send from',
notifications_send_email_server_port: 'Send email SMTP port',
notifications_send_email_server_port_desc: 'Port for the SMTP server',
notifications_send_email_server: 'Send email SMTP server',
notifications_send_email_server_desc:
'SMTP server for the email you will send from',
notifications_send_to_emails: 'Emails to send to',
notifications_send_to_emails_desc: 'Emails notifications will be sent to',
notification_settings_update: 'Settings updated',
notification_settings_update_desc:
'Notify when server settings have been updated',
notification_server_start_stop: 'Server Start/Stop',
notification_server_start_stop_desc:
'Notify when the server has been started/stopped',
notification_watchdog_limit: 'Watchdog Limit Notification',
notification_watchdog_limit_desc:
'Notify when the watchdog limit has been reached (does not affect the funding source)',
notification_server_status: 'Server Status',
notification_server_status_desc:
'Send regular notifications about the server status (interval value in hours)',
notification_incoming_payment: 'Incoming Payments',
notification_incoming_payment_desc:
'Notify when a wallet has received a payment above the specified amount (sats)',
notification_outgoing_payment: 'Outgoing Payments',
notification_outgoing_payment_desc:
'Notify when a wallet has sent a payment above the specified amount (sats)',
notification_credit_debit: 'Credit / Debit',
notification_credit_debit_desc:
'Notify when a wallet has been credited/debited by the superuser',
notification_balance_delta_changed: 'Balance Delta Changed',
notification_balance_delta_changed_desc:
'Notify when the diference between the node balance and the LNbits balance has changed even by 1 sat. This runs every minute.',
enable_watchdog: 'Enable Watchdog Switch',
no_notifications: 'No notifications',
notifications_disabled: 'LNbits status notifications are disabled.',
enable_notifications: 'Enable Notifications',
enable_notifications_desc:
'If enabled it will fetch the latest LNbits Status updates, like security incidents and updates.',
enable_killswitch: 'Enable Killswitch',
enable_killswitch_desc:
'If enabled it will change your funding source to VoidWallet automatically if LNbits sends out a killswitch signal. You will need to enable manually after an update.',
killswitch_interval: 'Killswitch Interval',
killswitch_interval_desc:
'How often the background task should check for the LNbits killswitch signal from the status source (in minutes).',
enable_watchdog: 'Enable Watchdog',
enable_watchdog_desc:
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
watchdog_interval: 'Watchdog Interval',
@@ -266,6 +188,7 @@ window.localisation.en = {
more: 'more',
less: 'less',
releases: 'Releases',
killswitch: 'Killswitch',
watchdog: 'Watchdog',
server_logs: 'Server Logs',
ip_blocker: 'IP Blocker',
@@ -275,39 +198,28 @@ window.localisation.en = {
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
enter_ip: 'Enter IP and hit enter',
rate_limiter: 'Rate Limiter',
callback_url_rules: 'Callback URL Rules',
enter_callback_url_rule: 'Enter URL rule as regex and hit enter',
callback_url_rule_hint:
'Callback URLs (like LNURL one) will be validated against all of these rules. No rule means all URLs are allowed.',
wallet_limiter: 'Wallet Limiter',
wallet_config: 'Wallet Config',
wallet_charts: 'Wallet Charts',
wallet_limit_max_withdraw_per_day:
'Max daily wallet withdrawal in sats (0 for no limit, -1 to block withdrawal)',
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
wallet_limit_secs_between_trans:
'Min secs between transactions per wallet (0 to disable)',
only_incoming_payments_allowed: 'Only incoming payments allowed',
disable_outgoing_payments: 'Disable outgoing payments',
number_of_requests: 'Number of requests',
time_unit: 'Time unit',
minute: 'minute',
settings: 'Settings',
second: 'second',
hour: 'hour',
disable_server_log: 'Disable Server Log',
enable_server_log: 'Enable Server Log',
coming_soon: 'Feature coming soon',
session_has_expired: 'Your session has expired. Please login again.',
instant_access_question: 'or instant access',
instant_access_question: 'Want instant access?',
login_with_user_id: 'Login with user ID',
or: 'or',
create_new_wallet: 'Create New Wallet',
login_to_account: 'Login to your account',
create_account: 'Create account',
account_settings: 'Account Settings',
signin_with_oauth: 'Login with',
signin_with_oauth_or: 'or Login with',
signin_with_nostr: 'Continue with Nostr',
signin_with_google: 'Sign in with Google',
signin_with_github: 'Sign in with GitHub',
@@ -316,15 +228,11 @@ window.localisation.en = {
password: 'Password',
password_config: 'Password Config',
password_repeat: 'Password repeat',
update_password: 'Update Password',
change_password: 'Change Password',
update_credentials: 'Update Credentials',
update_pubkey: 'Update Public Key',
set_password: 'Set Password',
invalid_password: 'Password must have at least 8 characters',
invalid_password_repeat: 'Passwords do not match',
reset_key_generated: 'A reset key has been generated.',
reset_key_copy: 'Click OK to copy the reset URL to your clipboard.',
login: 'Login',
register: 'Register',
username: 'Username',
@@ -340,18 +248,9 @@ window.localisation.en = {
invalid_username: 'Invalid Username',
auth_provider: 'Auth Provider',
my_account: 'My Account',
existing_account_question: 'Already have an account?',
background_image: 'Background Image',
back: 'Back',
logout: 'Logout',
look_and_feel: 'Look and Feel',
api_token: 'API Token',
api_tokens: 'API Tokens',
access_control_list: 'Access Control List',
access_control_list_admin_warning:
'This is an admin account. The generated tokens will have admin privileges.',
new_api_acl: 'New Access Control List',
api_token_id: 'Token Id',
toggle_gradient: 'Toggle Gradient',
gradient_background: 'Gradient Background',
language: 'Language',
@@ -378,200 +277,5 @@ window.localisation.en = {
reset_key: 'Reset Key',
reset_password: 'Reset Password',
border_choices: 'Border Choices',
select_all: 'Select All',
nfc_supported: 'NFC Supported',
nfc_not_supported: 'NFC not Supported',
expire_date: 'Expire Date: ',
hash: 'Hash: ',
welcome_lnbits: 'Welcome to LNbits',
setup_su_account: 'Set up the Superuser account below.',
create_ticker_converter: 'Create Currency Ticker Converter',
enable_audit: 'Enable Audit',
recommended: 'Recommended',
audit_desc: 'Record HTTP requests according with the specified filters',
audit_record_req: 'Record Request Body',
audit_record_warning: 'Warning: ',
audit_record_req_warning_1:
'confidential data (like passwords) will be logged.',
audit_record_req_warning_2: 'the request body can have large size.',
audit_record_use: 'Use it with caution.',
audit_ip: 'Record IP Address',
audit_ip_desc: 'Record the IP address of the client',
audit_path_params: 'Record Path Parameters',
audit_query_params: 'Record Query Parameters',
audit_http_methods: 'Include HTTP Methods',
audit_http_methods_hint:
'List of HTTP methods to be included. Empty lists means all.',
audit_http_methods_label: 'HTTP Methods',
audit_resp_codes: 'Include HTTP Response Codes',
audit_resp_codes_hint:
'List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*',
audit_resp_codes_label: 'HTTP Response code (regex)',
audit_paths: 'Include Paths',
audit_paths_hint:
'List of paths to be included (regex match). Empty list means all.',
audit_paths_label: 'HTTP Path (regex)',
audit_paths_exclude: 'Exclude Paths',
audit_paths_exclude_hint:
'List of paths to be excluded (regex match). Empty list means none.',
audit_paths_exclude_label: 'HTTP Path (regex)',
exchange_providers: 'Exchange Providers',
admin_extensions: 'Admin Extensions',
admin_extensions_label: 'Admin extensions',
admin_extensions_hint: 'Extensions only user with admin privileges can use',
user_default_extensions: 'User Default Extensions',
user_default_extensions_label: 'User extensions',
user_default_extensions_hint:
'Extensions that will be enabled by default for the users.',
miscellanous: 'Miscellanous',
misc_disable_extensions: 'Disable Extensions',
misc_disable_extensions_label: 'Disable all extensions',
misc_hide_api: 'Hide API',
misc_hide_api_label: 'Hides wallet api, extensions can choose to honor',
wallets_management: 'Wallets Management',
funding_source_info: 'Funding Source Info',
funding_source: 'Funding Source: {wallet_class}',
node_balance: 'Node Balance: {balance} sats',
lnbits_balance: 'LNbits Balance: {balance} sats',
funding_reserve_percent: 'Reserve Percent: {percent} %',
node_managment: 'Node Management',
node_management_not_supported:
'Node Management not supported by active funding source',
toggle_node_ui: 'Node UI',
toggle_public_node_ui: 'Public Node UI',
toggle_transactions_node_ui: 'Transactions Tab (Disable on large CLN nodes)',
invoice_expiry: 'Invoice Expiry',
invoice_expiry_label: 'Invoice expiry (seconds)',
fee_reserve: 'Fee Reserve',
fee_reserve_percent: 'Fee Reserve Percent',
fee_reserve_msats: 'Reserve fee in msats',
reserve_fee_in_percent: 'Reserve fee in percent',
payment_wait_time: 'Payment Wait Time (sec)',
payment_wait_time_desc:
'How long to wait when making a payment before marking it as pending. Set higher values for HODL invoices, Boltz, etc.',
server_management: 'Server Management',
base_url: 'Base URL',
base_url_label: 'Static/Base url for the server',
authentication: 'Authentication',
auth_token_expiry_label: 'Token expire minutes',
auth_token_expiry_hint: 'Time in minutes until the token expires',
auth_allowed_methods_label: 'Allowed authorization methods',
auth_allowed_methods_hint: 'Select authorization methods',
auth_nostr_label: 'Nostr Request URL',
auth_nostr_hint: 'Absolute URL that the clients will use to login.',
auth_google_ci_label: 'Google Client ID',
auth_google_ci_hint:
'Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Google Client Secret',
auth_gh_client_id_label: 'GitHub Client ID',
auth_gh_client_id_hint:
'Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'GitHub Client Secret',
auth_keycloak_label: 'Keycloak Discovery URL',
auth_keycloak_ci_label: 'Keycloak Client ID',
auth_keycloak_ci_hint:
'Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Keycloak Client Secret',
currency_settings: 'Currency Settings',
allowed_currencies: 'Allowed Currencies',
allowed_currencies_hint: 'Limit the number of available fiat currencies',
default_account_currency: 'Default Account Currency',
default_account_currency_hint: 'Default currency for accounting',
max_incoming_payment_amount: 'Max Incoming Payment Amount',
max_incoming_payment_amount_desc:
'Maximum amount allowed for generating an invoice',
max_outgoing_payment_amount: 'Max Outgoing Payment Amount',
max_outgoing_payment_amount_desc:
'Maximum amount allowed for making a payment',
service_fee: 'Service Fee',
service_fee_label: 'Service fee (%)',
service_fee_hint: 'Fee charged per tx (%)',
service_fee_max: 'Service Fee Max',
service_fee_max_label: 'Service fee max (sats)',
service_fee_max_hint: 'Max service fee to charge in (sats)',
fee_wallet: 'Fee Wallet',
fee_wallet_label: 'Fee wallet (wallet ID)',
fee_wallet_hint: 'Wallet ID to send funds to',
disable_fee: 'Disable Fee',
disable_fee_internal: 'Disable Service Fee for Internal Payments',
disable_fee_internal_desc:
'Disable Service Fee for Internal Lightning Payments',
ui_management: 'UI Management',
ui_site_title: 'Site Title',
ui_changing_remove_lnbits_elements:
' (changing will remove LNbits elements on the homepage and footer)',
ui_site_tagline: 'Site Tagline',
ui_elements_enable: 'Enable elements on homepage/footer',
ui_elements_disable: 'Disable elements on homepage/footer',
ui_toggle_elements_tip: "Remove homepage elements like 'runs on' etc",
ui_site_description: 'Site Description',
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
ui_default_wallet_name: 'Default Wallet Name',
ui_default_theme: 'Default Theme',
lnbits_wallet: 'LNbits wallet',
denomination: 'Denomination',
denomination_hint: 'The name for the FakeWallet token',
denomination_error: 'Denomination must be 3 characters, or `sats`',
ui_qr_code_logo: 'QR Code/Favicon Logo',
ui_qr_code_logo_hint: 'QR code and favicon logo url',
ui_custom_image: 'Custom Image',
ui_custom_image_label: 'URL to custom image',
ui_custom_image_hint: 'Image showed at homepage/login',
ui_custom_badge: 'Custom Badge',
ui_custom_badge_label: "Custom Badge 'USE WITH CAUTION'",
ui_custom_badge_color_label: 'Custom Badge Color',
themes: 'Themes',
themes_hint: 'Choose themes available for users',
custom_logo: 'Custom Logo',
custom_logo_hint: 'URL to logo image',
ad_space_title: 'Ad Space Title',
ad_space_title_label: 'Supported by',
ad_slots: 'Advertisement Slots',
ad_slots_hint:
'Ad url and image filepaths in CSV format, extensions can choose to honor',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Ads Enabled',
ads_disabled: 'Ads Disabled',
user_management: 'User Management',
admin_users: 'Admin Users',
admin_users_hint: 'Users with admin privileges',
admin_users_label: 'User ID',
allowed_users: 'Allowed Users',
allowed_users_hint: 'Only these users can use LNbits',
allowed_users_label: 'User ID',
allow_creation_user: 'Allow creation of new users',
allow_creation_user_desc: 'Allow creation of new users on the index page',
new_user_not_allowed: 'Registration is disabled.',
components: 'Components',
long_running_endpoints: 'Top 5 Long Running Endpoints',
http_request_methods: 'HTTP Request Methods',
http_response_codes: 'HTTP Response Codes',
request_details: 'Request Details',
http_request_details: 'HTTP Request Details',
payment_details: 'Payment Details',
payment_details_desc: 'Detailed information about the payment',
payments: 'Payments',
payment_show_internal: 'Show Internal Payments',
payment_chart_flow: 'Monthly Payment Flow',
payment_chart_status: 'Payment Status',
payment_chart_tx_per_wallet: 'Transactions per Wallet (balance/count)',
payment_details_back: 'Back to Payments',
payment_chart_tags: 'Payments by Tags',
payments_balance_in_out: 'Balance In/Out',
payments_count_in_out: 'Count In/Out',
payments_status_chart: 'Status Chart',
payments_tag_chart: 'Tag Chart',
payments_balance_chart: 'Balance Chart',
payments_wallets_chart: 'Wallets Chart',
payments_balance_in_out_chart: 'Balance In/Out Chart',
payments_count_in_out_chart: 'Count In/Out Chart',
reset_wallet_keys: 'Reset Keys',
reset_wallet_keys_desc:
'Reset the API keys for this wallet. This will invalidate the current keys and generate new ones.',
view_list: 'View wallets as list',
view_column: 'View wallets as rows',
filter_payments: 'Filter payments',
filter_date: 'Filter by date',
websocket_example: 'Websocket example'
select_all: 'Select All'
}
+17 -204
View File
@@ -2,10 +2,8 @@ window.localisation.es = {
confirm: 'Sí',
server: 'Servidor',
theme: 'Tema',
site_customisation: 'Personalización del sitio',
funding: 'Financiación',
users: 'Usuarios',
audit: 'Auditoría',
apps: 'Aplicaciones',
channels: 'Canales',
transactions: 'Transacciones',
@@ -28,11 +26,9 @@ window.localisation.es = {
restart: 'Reiniciar el servidor',
save: 'Guardar',
save_tooltip: 'Guardar cambios',
credit_debit: 'Crédito / Débito',
credit_hint: 'Presione Enter para cargar la cuenta',
credit_label: 'Cargar {denomination}',
credit_ok:
'Éxito al acreditar/debitar fondos virtuales ({amount} sats). Los pagos dependen de los fondos reales en la fuente de financiación.',
topup: 'Recargar',
topup_wallet: 'Recargar billetera',
topup_hint: 'Utilice el ID de billetera para recargar cualquier billetera',
restart_tooltip: 'Reinicie el servidor para aplicar los cambios',
add_funds_tooltip: 'Agregue fondos a una billetera.',
reset_defaults: 'Restablecer',
@@ -40,13 +36,14 @@ window.localisation.es = {
'Borrar todas las configuraciones y restablecer a los valores predeterminados.',
download_backup: 'Descargar copia de seguridad de la base de datos',
name_your_wallet: 'Nombre de su billetera {name}',
wallet_topup_ok:
'Éxito creando fondos virtuales ({amount} sats). Los pagos dependen de los fondos reales en la fuente de financiación.',
paste_invoice_label: 'Pegue la factura aquí',
lnbits_description:
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
export_to_phone: 'Exportar a teléfono con código QR',
export_to_phone_desc:
'Este código QR contiene su URL de billetera con acceso completo. Puede escanearlo desde su teléfono para abrir su billetera allí.',
wallet: 'Billetera:',
wallets: 'Billeteras',
add_wallet: 'Agregar nueva billetera',
delete_wallet: 'Eliminar billetera',
@@ -74,6 +71,8 @@ window.localisation.es = {
api_keys_api_docs: 'URL del nodo, claves de API y documentación de API',
lnbits_version: 'Versión de LNbits',
runs_on: 'Corre en',
credit_hint: 'Presione Enter para cargar la cuenta',
credit_label: 'Cargar {denomination}',
paste: 'Pegar',
paste_from_clipboard: 'Pegar desde el portapapeles',
paste_request: 'Pegar solicitud',
@@ -90,7 +89,7 @@ window.localisation.es = {
pay: 'Pagar',
memo: 'Memo',
date: 'Fecha',
payment_processing: 'Procesando pago ...',
processing_payment: 'Procesando pago ...',
not_enough_funds: '¡No hay suficientes fondos!',
search_by_tag_memo_amount: 'Buscar por etiqueta, memo, cantidad',
invoice_waiting: 'Factura esperando pago',
@@ -109,16 +108,10 @@ window.localisation.es = {
'La funcionalidad de inicio de sesión se lanzará en una actualización futura, por ahora, asegúrese de guardar esta página como marcador para acceder a su billetera en el futuro. Este servicio está en BETA y no asumimos ninguna responsabilidad por personas que pierdan el acceso a sus fondos.',
no_transactions: 'No hay transacciones todavía',
manage: 'Administrar',
exchanges: 'Intercambios',
extensions: 'Extensiones',
no_extensions: 'No tienes extensiones instaladas :(',
created: 'Creado',
search_extensions: 'Extensiones de búsqueda',
extension_sources: 'Fuentes de extensión',
ext_sources_hint:
'Repositorios desde donde se pueden descargar las extensiones',
ext_sources_label:
'URL de origen (utilice solo la fuente oficial de la extensión LNbits y fuentes en las que pueda confiar)',
warning: 'Advertencia',
repository: 'Repositorio',
confirm_continue: '¿Está seguro de que desea continuar?',
@@ -127,10 +120,8 @@ window.localisation.es = {
uninstall: 'Desinstalar',
drop_db: 'Eliminar datos',
enable: 'Habilitar',
pay_to_enable: 'Pagar para habilitar',
enable_extension_details: 'Habilitar extensión para el usuario actual',
disable: 'Deshabilitar',
delete: 'Eliminar',
installed: 'Instalado',
activated: 'Activado',
deactivated: 'Desactivado',
@@ -155,19 +146,15 @@ window.localisation.es = {
'Está a punto de eliminar todos los datos para la extensión. Por favor, escriba el nombre de la extensión para continuar:',
extension_required_lnbits_version:
'Esta versión requiere al menos una versión de LNbits',
min_version: 'Mínimo (incluido)',
max_version: 'Máximo (excluido)',
payment_hash: 'Hash de pago',
fee: 'Cuota',
amount: 'Cantidad',
amount_sats: 'Cantidad (sats)',
tag: 'Etiqueta',
unit: 'Unidad',
description: 'Descripción',
expiry: 'Expiración',
webhook: 'Webhook',
payment_proof: 'Prueba de pago',
update: 'Actualizar',
update_available: '¡Actualización {version} disponible!',
latest_update: 'Usted está en la última versión {version}.',
notifications: 'Notificaciones',
@@ -177,7 +164,13 @@ window.localisation.es = {
enable_notifications: 'Activar notificaciones',
enable_notifications_desc:
'Si está activado, buscará las últimas actualizaciones del estado de LNbits, como incidentes de seguridad y actualizaciones.',
enable_killswitch: 'Activar Killswitch',
enable_killswitch_desc:
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si LNbits envía una señal de parada de emergencia. Necesitará activarlo manualmente después de una actualización.',
killswitch_interval: 'Intervalo de Killswitch',
killswitch_interval_desc:
'Con qué frecuencia la tarea en segundo plano debe verificar la señal de interruptor de emergencia de LNbits desde la fuente de estado (en minutos).',
enable_watchdog: 'Activar Watchdog',
enable_watchdog_desc:
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si su saldo es inferior al saldo de LNbits. Tendrá que activarlo manualmente después de una actualización.',
watchdog_interval: 'Intervalo de vigilancia',
@@ -193,6 +186,7 @@ window.localisation.es = {
more: 'más',
less: 'menos',
releases: 'Lanzamientos',
killswitch: 'Interruptor de apagado',
watchdog: 'Perro guardián',
server_logs: 'Registros del Servidor',
ip_blocker: 'Bloqueador de IP',
@@ -226,7 +220,6 @@ window.localisation.es = {
login_to_account: 'Inicie sesión en su cuenta',
create_account: 'Crear cuenta',
account_settings: 'Configuración de la cuenta',
signin_with_nostr: 'Continuar con Nostr',
signin_with_google: 'Inicia sesión con Google',
signin_with_github: 'Inicia sesión con GitHub',
signin_with_keycloak: 'Iniciar sesión con Keycloak',
@@ -235,14 +228,11 @@ window.localisation.es = {
password_config: 'Configuración de Contraseña',
password_repeat: 'Repetición de contraseña',
change_password: 'Cambiar contraseña',
update_credentials: 'Actualizar credenciales',
update_pubkey: 'Actualizar clave pública',
set_password: 'Establecer contraseña',
invalid_password: 'La contraseña debe tener al menos 8 caracteres.',
login: 'Iniciar sesión',
register: 'Registrarse',
username: 'Nombre de usuario',
pubkey: 'Clave pública',
user_id: 'Identificación de usuario',
email: 'Correo electrónico',
first_name: 'Nombre de pila',
@@ -257,191 +247,14 @@ window.localisation.es = {
back: 'Atrás',
logout: 'Cerrar sesión',
look_and_feel: 'Apariencia',
toggle_gradient: 'Alternar degradado',
gradient_background: 'Fondo de gradiente',
language: 'Idioma',
color_scheme: 'Esquema de colores',
admin_settings: 'Configuración del administrador',
extension_cost: 'Esta versión requiere un pago mínimo de {cost} sats.',
extension_paid_sats: 'Ya has pagado {paid_sats} sats.',
release_details_error: 'No se pueden obtener los detalles de la versión.',
pay_from_wallet: 'Pagar desde la billetera',
wallet_required: 'Billetera *',
show_qr: 'Mostrar QR',
retry_install: 'Reintentar Instalación',
new_payment: 'Realizar nuevo pago',
update_payment: 'Actualizar Pago',
already_paid_question: '¿Ya has pagado?',
sell: 'Vender',
sell_require: 'Solicitar pago para habilitar la extensión',
sell_info:
'La extensión {name} requiere un pago mínimo de {amount} sats para habilitar.',
hide_empty_wallets: 'Ocultar billeteras vacías',
recheck: 'Revisar de nuevo',
contributors: 'Colaboradores',
license: 'Licencia',
reset_key: 'Restablecer clave',
reset_password: 'Restablecer contraseña',
border_choices: 'Opciones de Borde',
select_all: 'Seleccionar todo',
nfc_supported: 'Compatible con NFC',
nfc_not_supported: 'NFC no compatible',
expire_date: 'Fecha de vencimiento:',
hash: 'Hash:',
welcome_lnbits: 'Bienvenido a LNbits',
setup_su_account: 'Configura la cuenta de Superusuario a continuación.',
create_ticker_converter: 'Crear Convertidor de Ticker de Moneda',
enable_audit: 'Habilitar auditoría',
recommended: 'Recomendado',
audit_desc:
'Registrar solicitudes HTTP de acuerdo con los filtros especificados',
audit_record_req: 'Registrar cuerpo de solicitud',
audit_record_warning: 'Advertencia:',
audit_record_req_warning_1:
'los datos confidenciales (como las contraseñas) serán registrados.',
audit_record_req_warning_2:
'el cuerpo de la solicitud puede tener un tamaño grande.',
audit_record_use: 'Úsalo con precaución.',
audit_ip: 'Registrar Dirección IP',
audit_ip_desc: 'Registra la dirección IP del cliente',
audit_path_params: 'Registrar parámetros de ruta',
audit_query_params: 'Registrar parámetros de consulta',
audit_http_methods: 'Incluye métodos HTTP',
audit_http_methods_hint:
'Lista de métodos HTTP a incluir. Las listas vacías significan todos.',
audit_http_methods_label: 'Métodos HTTP',
audit_resp_codes: 'Incluir Códigos de Respuesta HTTP',
audit_resp_codes_hint:
'Lista de códigos HTTP a incluir (coincidencia regex). Listas vacías significan todos. Ej: 4.*, 5.*',
audit_resp_codes_label: 'Código de respuesta HTTP (regex)',
audit_paths: 'Incluir rutas',
audit_paths_hint:
'Lista de rutas a incluir (coincidencia de expresión regular). Lista vacía significa todas.',
audit_paths_label: 'Ruta HTTP (regex)',
audit_paths_exclude: 'Excluir rutas',
audit_paths_exclude_hint:
'Lista de rutas a excluir (coincidencia de expresiones regulares). Lista vacía significa ninguna.',
audit_paths_exclude_label: 'Ruta HTTP (regex)',
exchange_providers: 'Proveedores de intercambio',
admin_extensions: 'Extensiones de Administración',
admin_extensions_label: 'Extensiones de administración',
admin_extensions_hint:
'Solo los usuarios con privilegios de administrador pueden usar extensiones.',
user_default_extensions: 'Extensiones predeterminadas del usuario',
user_default_extensions_label: 'Extensiones de usuario',
user_default_extensions_hint:
'Extensiones que estarán habilitadas de forma predeterminada para los usuarios.',
miscellanous: 'Misceláneo',
misc_disable_extensions: 'Desactivar extensiones',
misc_disable_extensions_label: 'Desactivar todas las extensiones',
misc_hide_api: 'Ocultar API',
misc_hide_api_label:
'Oculta la API de la billetera, las extensiones pueden optar por respetar',
wallets_management: 'Gestión de Carteras',
funding_source_info: 'Información sobre la Fuente de Financiamiento',
funding_source: 'Fuente de financiamiento: {wallet_class}',
node_balance: 'Balance de Nodo: {balance} sats',
lnbits_balance: 'Saldo de LNbits: {balance} sats',
funding_reserve_percent: 'Reserve Porcentaje: {percent} %',
node_managment: 'Gestión de nodos',
node_management_not_supported:
'La gestión de nodos no es compatible con la fuente de financiación activa',
toggle_node_ui: 'Interfaz de usuario de nodo',
toggle_public_node_ui: 'Interfaz Pública de Nodo',
toggle_transactions_node_ui:
'Pestaña de transacciones (desactivar en nodos CLN grandes)',
invoice_expiry: 'Vencimiento de la Factura',
invoice_expiry_label: 'Expiración de la factura (segundos)',
fee_reserve: 'Reserva de tarifa',
fee_reserve_msats: 'Cuota de reserva en msats',
fee_reserve_percent: 'Tasa de reserva en porcentaje',
server_management: 'Gestión del Servidor',
base_url: 'URL base',
base_url_label: 'URL base estática para el servidor',
authentication: 'Autenticación',
auth_token_expiry_label: 'Minutos de vencimiento del token',
auth_token_expiry_hint: 'Tiempo en minutos hasta que el token expire',
auth_allowed_methods_label: 'Métodos de autorización permitidos',
auth_allowed_methods_hint: 'Seleccione métodos de autorización',
auth_nostr_label: 'URL de solicitud Nostr',
auth_nostr_hint:
'URL absoluto que los clientes utilizarán para iniciar sesión.',
auth_google_ci_label: 'ID de cliente de Google',
auth_google_ci_hint:
'Asegúrate de que los URIs de redirección autorizados contengan https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Secreto del Cliente de Google',
auth_gh_client_id_label: 'ID de cliente de GitHub',
auth_gh_client_id_hint:
'Asegúrate de que la URL de devolución de llamada de autorización esté configurada en https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'Secreto del cliente de GitHub',
auth_keycloak_label: 'URL de descubrimiento de Keycloak',
auth_keycloak_ci_label: 'ID de cliente de Keycloak',
auth_keycloak_ci_hint:
'Asegúrate de que la URL de devolución de llamada de autorización esté configurada en https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Secreto del Cliente de Keycloak',
currency_settings: 'Configuración de moneda',
allowed_currencies: 'Monedas permitidas',
allowed_currencies_hint:
'Limite el número de monedas fiduciarias disponibles',
default_account_currency: 'Moneda predeterminada de la cuenta',
default_account_currency_hint: 'Moneda predeterminada para contabilidad',
service_fee_label: 'Tarifa de servicio (%)',
service_fee_hint: 'Tarifa cobrada por tx (%)',
service_fee_max_label: 'Tarifa de servicio máx (sats)',
service_fee_max_hint: 'Tarifa máxima por servicio a cobrar en (sats)',
fee_wallet: 'Billetera de Tarifas',
fee_wallet_label: 'Billetera de tarifas (ID de billetera)',
fee_wallet_hint: 'ID de la billetera a la que enviar fondos',
disable_fee: 'Desactivar tarifa',
disable_fee_internal: 'Desactivar tarifa de servicio para pagos internos',
disable_fee_internal_desc:
'Desactivar tarifa de servicio para pagos internos Lightning',
ui_management: 'Gestión de la interfaz de usuario',
ui_site_title: 'Título del Sitio',
ui_site_tagline: 'Lema del sitio',
ui_elements_enable: 'Habilitar elementos en la página de inicio',
ui_elements_disable: 'Desactivar elementos en la página de inicio',
ui_toggle_elements_tip:
"Eliminar elementos de la página de inicio como 'funciona en', etc.",
ui_site_description: 'Descripción del sitio',
ui_site_description_hint:
'Usa texto sin formato, Markdown o HTML sin procesar',
ui_default_wallet_name: 'Nombre predeterminado de la billetera',
lnbits_wallet: 'Cartera LNbits',
denomination: 'Denominación',
denomination_hint: 'El nombre para el token FakeWallet',
ui_qr_code_logo: 'Logo de código QR',
ui_qr_code_logo_hint: 'URL a la imagen del logo en el código QR',
ui_custom_badge: 'Insignia personalizada',
ui_custom_badge_label:
"Insignia personalizada 'USAR CON PRECAUCIÓN - La billetera LNbits aún está en BETA'",
ui_custom_badge_color_label: 'Color personalizado de insignia',
themes: 'Temas',
themes_hint: 'Elige los temas disponibles para los usuarios',
custom_logo: 'Logotipo personalizado',
custom_logo_hint: 'URL a la imagen del logo',
ad_space_title: 'Título del Espacio Publicitario',
ad_space_title_label: 'Respaldado por',
ad_slots: 'Espacios publicitarios',
ad_slots_hint:
'URL de anuncio y rutas de archivo de imagen en formato CSV, las extensiones pueden optar por respetar',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Anuncios habilitados',
ads_disabled: 'Anuncios desactivados',
user_management: 'Gestión de Usuarios',
admin_users: 'Usuarios Administradores',
admin_users_hint: 'Usuarios con privilegios de administrador',
admin_users_label: 'ID de usuario',
allowed_users: 'Usuarios Permitidos',
allowed_users_hint: 'Solo estos usuarios pueden usar LNbits',
allowed_users_label: 'ID de usuario',
allow_creation_user: 'Permitir la creación de nuevos usuarios',
allow_creation_user_desc:
'Permitir la creación de nuevos usuarios en la página de índice',
components: 'Componentes',
long_running_endpoints: 'Principales 5 puntos de conexión de larga duración',
http_request_methods: 'Métodos de solicitud HTTP',
http_response_codes: 'Códigos de Respuesta HTTP',
request_details: 'Detalles de la solicitud',
http_request_details: 'Detalles de la Solicitud HTTP'
hide_empty_wallets: 'Ocultar billeteras vacías'
}
+50 -373
View File
@@ -2,11 +2,8 @@ window.localisation.fi = {
confirm: 'Kyllä',
server: 'Palvelin',
theme: 'Teema',
site_customisation: 'Sivuston kustomointi',
funding: 'Rahoitus',
users: 'Käyttäjät',
audit: 'Seuranta',
api_watch: 'API-seuranta',
apps: 'Sovellukset',
channels: 'Kanavat',
transactions: 'Tapahtumat',
@@ -27,30 +24,27 @@ window.localisation.fi = {
close_channel: 'Sulje kanava',
close: 'Sulje',
restart: 'Palvelimen uudelleen käynnistys',
image_library: 'Kuvakirjasto',
save: 'Tallenna',
save_tooltip: 'Tallenna muutokset',
credit_debit: 'Hyvitä / Veloita',
credit_hint:
'Hyväksy painamalla Enter (negatiivisetkin arvot ovat sallittuja)',
credit_label: 'Hyvitä / Veloita tilille {denomination}-varoja',
credit_ok:
'Virtuaalivarojen ({amount} sat) hyvitys-/veloitustapahtuma onnistui. Maksukyky riippuuu rahoituslähteen todellisista varoista.',
topup: 'Topup',
topup_wallet: 'Lisää varoja lompakkoon',
topup_hint: 'Lisää varoja lompakkoon sen ID:n perusteella',
restart_tooltip: 'Uudelleenkäynnistä palvelu muutosten käyttöönottamiseksi',
add_funds_tooltip: 'Lisää varoja lompakkoon',
reset_defaults: 'Palauta oletusasetukset',
reset_defaults: 'Peruuta muutokset',
reset_defaults_tooltip:
'Poista kaikki asetusten muutokset ja palauta järjestelmän oletusasetukset.',
download_backup: 'Lataa tietokannan varmuuskopio',
name_your_wallet: 'Nimeä lompakkosi {name}',
paste_invoice_label: 'Liita lasku, maksupyyntö tai LNURL-koodi*',
name_your_wallet: 'Anna {name}-lompakollesi nimi',
wallet_topup_ok:
'Virtuaalisten varojen luominen onnistui ({amount} sats). Maksut riippuvat rahoituslähteen todellisista varoista.',
paste_invoice_label:
'Liitä lasku, maksupyyntö, lnurl-koodi tai Lightning Address *',
lnbits_description:
'Kevyt ja helppokäyttöinen LNbits voi käyttää rahoituslähteinään mitä vain Lightning-palveluita ja jopa LNbits-palvelua! Voit käyttää sitä itsenäisesti ja helposti tarjota erilaisia Lightning-palveluita. Pystyt luomaan sillä salamaverkkolompakoita eikä niiden määrää ole rajoitettu. Jokaiselle lompakolle saat yksilölliset API-avaimet. Varojen osittaminen tekee siitä erittäin kätevän varojen hallinnassa sekä myös ohjelmistokehityksen työkalun. Laajennukset lisäävät LNbits:in toiminnallisuuksia. Näinpä voit helposti testailla useita erilaisia ja viimeisimpiä salamaverkon teknologioita. Laajennuksien kehittämisen olemme pyrkineet tekemään mahdollisimman helpoksi pitämällä LNbits:in ilmaisena OpenSource-projektina. Kannustamme kaikkia kehittämään ja jakelemaan omia laajennuksia!',
'Kevyt ja helppokäyttöinen LNbits voi käyttää rahoituslähteinään erilaisia palveluita, ja jopa LNbits itseään! Voit käyttää sitä itsenäisesti ja helposti tarjota erilaisia Lightning-palveluita. Pystyt luomaan sillä salamaverkkolompakoita eikä niiden määrää ole rajoitettu. Jokaiselle lompakolle saat yksilölliset API-avaimet. Varojen osittaminen tekee siitä erittäin kätevän varojen hallinnassa sekä myös ohjelmistokehityksen työkalun. Laajennukset lisäävät LNbits:in toiminnallisuuksia. Näinpä voit helposti testailla useita erilaisia ja viimeisimpiä salamaverkon teknologioita. Laajennuksien kehittämisen olemme pyrkineet tekemään mahdollisimman helpoksi pitämällä LNbits:in ilmaisena OpenSource-projektina. Kannustamme kaikkia kehittämään ja jakelemaan omia laajennuksia!',
export_to_phone: 'Käytä puhelimessa lukemalla QR-koodi',
export_to_phone_desc:
'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voit lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
access_wallet_on_mobile: 'Mobiili käyttö',
wallet: 'Lompakko:',
'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voi lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
wallets: 'Lompakot',
add_wallet: 'Lisää lompakko',
delete_wallet: 'Poista lompakko',
@@ -62,13 +56,14 @@ window.localisation.fi = {
currency: 'Valuutta',
update_currency: 'Tallenna',
press_to_claim: 'Lunasta varat painamalla tästä',
claim_desc:
'Näyttää että sinulla on lunastamattomia bitcoin varoja, mutta sinulla ei vielä ole lompakkoa. Lunasta varat allaolevaa nappia painamalla, ja sinulle luodaan lompakko.',
donate: 'Lahjoita',
view_github: 'Näytä GitHub:ssa',
voidwallet_active: 'VoidWallet on aktiivinen. Se ei tue maksutapahtumia!',
voidwallet_active:
'Maksutapahtumat ovat poissa käytöstä, koska VoidWallet on aktiivinen!',
use_with_caution:
'KÄYTÄ VAROEN - BETA-ohjelmisto on käytössä palvelussa: {name}',
service_fee: 'Palvelumaksu: {amount} % tapahtumasta',
service_fee_max: 'Palvelumaksu: {amount} % tapahtumasta (enintään {max} sat)',
service_fee_tooltip:
'LNbits palvelimen ylläpitäjä veloittaa lähtevästä maksusta palvelumaksun.',
toggle_darkmode: 'Tumma näkymä',
@@ -78,13 +73,14 @@ window.localisation.fi = {
api_keys_api_docs: 'Solmun URL, API-avaimet ja -dokumentaatio',
lnbits_version: 'LNbits versio',
runs_on: 'Mukana menossa',
credit_hint: 'Hyväksy painamalla Enter',
credit_label: 'Lisää tilille varoja {denomination}',
paste: 'Liitä',
paste_from_clipboard: 'Liitä leikepöydältä',
paste_request: 'Liitä pyyntö',
create_invoice: 'Laskuta',
camera_tooltip: 'Kuvaa lasku tai QR-koodi',
export_csv: 'Vie CSV-tiedostoon',
export_csv_details: 'Vie CSV-tiedostoon lisätietoineen',
chart_tooltip: 'Näytä kaaviokuva',
pending: 'Odottaa',
copy_invoice: 'Kopioi lasku',
@@ -92,18 +88,15 @@ window.localisation.fi = {
cancel: 'Peruuta',
scan: 'Scannaa',
read: 'Lue',
write: 'Kirjoita',
pay: 'Maksa',
memo: 'Kuvaus',
date: 'Päiväys',
path: 'Path',
payment_processing: 'Maksua käsitellään...',
processing_payment: 'Maksua käsitellään...',
not_enough_funds: 'Varat eivät riitä!',
search_by_tag_memo_amount: 'Etsi tunnisteella, muistiolla tai määrällä',
invoice_waiting: 'Lasku odottaa maksua',
invoice_waiting: 'Lasku osottaa maksamista',
payment_received: 'Maksu vastaanotettu',
payment_sent: 'Maksu lähetetty',
payment_failed: 'Maksu epäonnistui',
receive: 'vastaanota',
send: 'lähetä',
outgoing_payment_pending: 'Lähtevä maksu odottaa',
@@ -114,18 +107,13 @@ window.localisation.fi = {
copy_wallet_url: 'Kopioi lompakon URL',
disclaimer_dialog_title: 'Tärkeää!',
disclaimer_dialog:
'Sinun *PITÄÄ TALLETTAA* kirjautumistietosi turvallisesta ja helposti saataville, jotta pääset jatkossa kirjautumaan lompakkoosi! Löydät kirjautumistiedot Tilin asetukset -sivulta. Kukaan ei ota mitään vastuuta varojen säilymisestä tai niiden käytettävyyden takaamisesta.',
'Muistathan tallettaa kirjautumistietosi turvallisesta ja helposti saataville, jotta pääset jatkossakin kirjautumaan lompakkoosi! Tutustu myös Tilin asetukset -sivuun. Tämä palvelu on kokeiluvaiheessa (eli BETA), ja niinpä kukaan ei ota mitään vastuuta varojen säilymisestä tai niiden käytettävyyden takaamisesta.',
no_transactions: 'Lompakossa ei ole yhtään tapahtumaa',
manage: 'Hallinnointi',
exchanges: 'Vaihtokurssit',
extensions: 'Laajennukset',
no_extensions: 'Laajennuksia ei ole asennettu :(',
created: 'Luotu',
search_extensions: 'Etsi laajennuksia',
extension_sources: 'Laajennuslähteet',
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
ext_sources_label:
'Lähde-URL (käytä vain virallista LNbits tai muuta luotettaa laajennuslähdettä)',
warning: 'Varoitus',
repository: 'Laajennuksien lähde',
confirm_continue: 'Haluatko varmasti jatkaa?',
@@ -134,10 +122,8 @@ window.localisation.fi = {
uninstall: 'Poista',
drop_db: 'Poista tiedot',
enable: 'Ota käyttöön',
pay_to_enable: 'Maksa ottaaksesi käyttöön',
enable_extension_details: 'Ota laajennus käyttöön tälle käyttäjälle',
disable: 'Poista käytöstä',
delete: 'Poista',
installed: 'Asennettu',
activated: 'Käytössä',
deactivated: 'Poissa käytöstä',
@@ -159,96 +145,32 @@ window.localisation.fi = {
extension_db_drop_warning:
'Olet tuhoamassa laajennuksen tallettamat tiedot. Vahvista poisto kirjoittamalla viivalle seuraavassa näkyvä laajennuksen nimi:',
extension_required_lnbits_version:
'Tämä laajennus vaatii vähintään LNbits-version',
min_version: 'Minimi (sisältyy)',
max_version: 'Enimmäismäärä (ei sisälly)',
'Tämä julkaisu vaatii vähintään LNbits-version',
payment_hash: 'Maksun tiiviste',
fee: 'Kulu',
amount: 'Määrä',
amount_sats: 'Määrä (sat)',
tag: 'Tunniste',
unit: 'Yksikkö',
description: 'Kuvaus',
expiry: 'Vanhenee',
expiry: 'Vanheneminen',
webhook: 'Webhook',
payment_proof: 'Maksun varmenne',
update: 'Päivitä',
update_available: 'Saatavilla on päivitys {version}-versioon!',
update_available: 'Rahoituslähteet',
update_available: 'Saatavilla on päivitys versioon {version}!',
latest_update:
'Käytössä oleva versio {version}, on viimeisin saatavilla oleva.',
notifications: 'Tiedotteet',
notifications_configure: 'Määritä tiedotukset',
notifications_nostr_config: 'Nostr-määritykset',
notifications_enable_nostr: 'Kaytä Nostr:ia',
notifications_enable_nostr_desc: 'Lähetä tietodukset Nostr:in kautta',
notifications_nostr_private_key: 'Nostr-yksityisavain',
notifications_nostr_private_key_desc:
'Yksityinen avain (hex tai nsec) Nostr-viestien lähettämisen allekirjoitukseen',
notifications_nostr_identifiers: 'Nostr-tunnisteet',
notifications_nostr_identifiers_desc:
'Lista tunnisteista kenelle tiedotukset lähetetään',
notifications_telegram_config: 'Telegram-määritykset',
notifications_enable_telegram: 'Käytä Telegram:ia',
notifications_enable_telegram_desc: 'Lähetä tietodukset Telegram:in kautta',
notifications_telegram_access_token: 'Access Token',
notifications_telegram_access_token_desc: 'Telegram botin Access token',
notifications_chat_id: 'Keskustelun tunnus',
notifications_chat_id_desc: 'Keskustelun tunnus minne tiedotukset lähetetään',
notifications_email_config: 'Sähköposti määritykset',
notifications_enable_email: 'Käytä sähköpostia',
notifications_enable_email_desc: 'Lähetä tiedotteet sähköpostilla',
notifications_send_test_email: 'Lähetä testiposti',
notifications_send_email: 'Lähetä sähköpostiosoitteella',
notifications_send_email_desc: 'Lähettäjänä näkyvä sähköpostiosoite',
notifications_send_email_username: 'Käyttäjätunnus',
notifications_send_email_username_desc:
'Käyttäjätunnus, mikäli tyhjä, käytetään sähköpostiosoitetta',
notifications_send_email_password: 'Lähtevän sähköpostin salasana',
notifications_send_email_password_desc: 'Salasana lähettävälle sähköpostille',
notifications_send_email_server_port: 'Lähtevän sähköpostin SMTP-portti',
notifications_send_email_server_port_desc: 'SMTP-palvelimen portti',
notifications_send_email_server: 'Lähtevän sähköpostin SMTP-palvelin',
notifications_send_email_server_desc:
'SMTP-palvelin jonka kautta sähköpostit lähetetään',
notifications_send_to_emails: 'Sähköpostien vastaanottaja',
notifications_send_to_emails_desc: 'Kenelle sähköpostit lähetetään',
notification_settings_update: 'Asetuksia päivitetty',
notification_settings_update_desc:
'Tiedota kun palvelimen asetuksia on päivitetty',
notification_server_start_stop: 'Palvelimen Käynnystys/Sammutus',
notification_server_start_stop_desc:
'Tiedota kun palvelin on käynnistetty tai sammutettu',
notification_watchdog_limit: 'Watchdog-raja -tiedote',
notification_watchdog_limit_desc:
'Tiedota kun watchdog-raja on saavutettu (ei vaikuta rahoituslähteeseen)',
notification_server_status: 'Palvelimen tila',
notification_server_status_desc:
'Lähetä säännölliset tiedotteet palvelimen tilasta (anna tiedotusväli tunteina)',
notification_incoming_payment: 'Saapuvat maksut',
notification_incoming_payment_desc:
'Tiedota kun lompakon vastaanottaman ja saapuvan maksun määrä ylittää rajan (sat)',
notification_outgoing_payment: 'Lähtevät maksut',
notification_outgoing_payment_desc:
'Tiedota kun lompakon lähettävän ja maksettavan maksun määrä ylittää rajan (sat)',
notification_credit_debit: 'Hyvitys / Veloitus',
notification_credit_debit_desc:
'Tiedota kun Superuser tekee lompakon hyvitys- tai veloitustapahtumia',
notification_balance_delta_changed: 'Saldon määrän muutos',
notification_balance_delta_changed_desc:
'Tiedota kun solmun ja LNbits saldojen eri poikkeaa edes yhden satoshin. Tämä tarkastus tehdään joka minuuttu.',
enable_watchdog: 'Watchdog-kytkin',
no_notifications: 'Ei tiedotteita',
notifications_disabled: 'LNbits-tilatiedotteet on poistettu käytöstä.',
enable_notifications: 'Ota tiedotteet käyttöön',
enable_notifications_desc:
'Tämän ollessa valittuna, noudetaan LNbits-tilatiedotteet. Niitä ovat esimerkiksi turvallisuuteen liittyvät tapahtumatiedotteet ja tiedot tämän ohjelmiston päivityksistä.',
enable_killswitch: 'Ota Killswitch käyttöön',
enable_killswitch_desc:
'Jos LNbits antaa killswitch-komennon, niin rahoituslähteeksi valitaan automaattisesti heti VoidWallet. Päivityksen jälkeen tämä asetus pitää tarkastaa uudelleen.',
killswitch_interval: 'Killswitch-aikaväli',
killswitch_interval_desc:
'Tällä määritetään kuinka usein taustatoiminto tarkistaa killswitch-signaalin tilatiedotteiden lähteestä. Hakujen väli ilmoitetaan minuutteina.',
enable_watchdog: 'Ota Watchdog käyttöön',
enable_watchdog_desc:
'Tämän ollessa käytössä, ja solmun varojen laskiessa alle LNbits-varojen määrän, otetaan automaattisesti käyttöön VoidWallet. Päivityksen jälkeen tämä asetus pitää tarkastaa uudelleen.',
watchdog_interval: 'Watchdog-aikaväli',
@@ -256,57 +178,46 @@ window.localisation.fi = {
'Tällä määritetään kuinka usein taustatoiminto tarkistaa varojen Delta-muutokset [node_balance - lnbits_balance] killswitch-signaalille. Hakujen väli ilmoitetaan minuutteina.',
watchdog_delta: 'Watchdog Delta',
watchdog_delta_desc:
'Mikäli rahoituslähteen saldo laskee alle LNbits kokonaissaldon, muutetaan rahoituslähteeksi heti VoidWallet. Päivittämisen jälkeen asetus pitää päivittää manuaalisestsi.',
'Saldomuutoksen raja-arvo jolloin killswitch-muuttaa rahoituslähteeksi VoidWallet:in [lnbits_balance - node_balance > delta]',
status: 'Tilanne',
notification_source: 'Tiedotteiden lähde',
notification_source_label:
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
more: 'näytä lisää',
less: 'supista',
more: 'enemmän',
less: 'vähemmän',
releases: 'Julkaisut',
killswitch: 'Killswitch',
watchdog: 'Watchdog',
server_logs: 'Palvelimen lokit',
ip_blocker: 'Palvelimen suojaus IP-osoitesuodattimella',
ip_blocker: 'IP-suodatin',
security: 'Turvallisuus',
security_tools: 'Turvallisuus työkalut',
block_access_hint: 'Estä pääsy IP-osoitteen perusteella',
allow_access_hint: 'Salli pääsy IP-osoitteen perusteella (ohittaa estot)',
enter_ip: 'Anna IP ja paina +',
rate_limiter: 'Toiston rajoitin',
callback_url_rules: 'Callback URL -säännöt',
enter_callback_url_rule: 'Anna URL-sääntö regex-muodossa ja paina enter',
callback_url_rule_hint:
'Callback URL:it (kuten LNURL) tarkistetaan näiden sääntöjen mukaisesti. Jos sääntöjä ei ole määritetty, kaikki URL:it ovat sallittuja.',
wallet_limiter: 'Lompakon käyttörajoitin',
wallet_config: 'Wallet Config',
wallet_charts: 'Wallet Charts',
wallet_limiter: 'Lompakon Rajoitin',
wallet_limit_max_withdraw_per_day:
'Päivittäin nostettavissa sat maksimi (0 poistaa käytöstä)',
wallet_max_ballance: 'Maksimisaldo (sat) (0 poistaa käytöstä)',
'Maksimi päivittäinen lompakon nosto sateissa (0 poistaa käytöstä)',
wallet_max_ballance: 'Lompakon maksimisaldo satosheina (0 poistaa käytöstä)',
wallet_limit_secs_between_trans:
'Tapahtumien välinen minimi (sec) (0 poistaa käytöstä)',
only_incoming_payments_allowed: 'Vain saapuvat maksut sallittuna',
disable_outgoing_payments: 'Poista lähtevät maksut käytöstä',
'Min sekuntia transaktioiden välillä lompakkoa kohden (0 poistaa käytöstä)',
number_of_requests: 'Pyyntöjen lukumäärä',
time_unit: 'aikayksikkö',
minute: 'minuutti',
settings: 'Asetukset',
second: 'sekunti',
hour: 'tunti',
disable_server_log: 'Piilota palvelimen loki',
enable_server_log: 'Näytä palvelimen loki',
disable_server_log: 'Poista palvelimen loki käytöstä',
enable_server_log: 'Ota palvelimen loki käyttöön',
coming_soon: 'Ominaisuus on tulossa pian',
session_has_expired: 'Käyttämätön sessio on vanhentunut. Kirjaudu uudelleen.',
instant_access_question: 'perinteinen kirjautuminen',
instant_access_question: 'Kirjaudu aikaisemmin luodulla tiedolla',
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
or: 'tai',
create_new_wallet: 'Avaa uusi lompakko',
login_to_account: 'Kirjaudu käyttäjänimellä',
create_account: 'Luo tili',
account_settings: 'Tilin asetukset',
signin_with_oauth: 'Login with',
signin_with_oauth_or: 'or Login with',
signin_with_nostr: 'Kirjaudu Nostr:lla',
signin_with_google: 'Kirjaudu Google-tunnuksella',
signin_with_github: 'Kirjaudu GitHub-tunnuksella',
signin_with_keycloak: 'Kirjaudu Keycloak-tunnuksella',
@@ -314,19 +225,12 @@ window.localisation.fi = {
password: 'Anna uusi salasana',
password_config: 'Salasanan määritys',
password_repeat: 'Toista uusi salasana',
update_password: 'Päivitä salasana',
change_password: 'Vaihda salasana',
update_credentials: 'Päivitä käyttöoikeustiedot',
update_pubkey: 'Päivitä julkinen avain',
set_password: 'Aseta salasana',
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
invalid_password_repeat: 'Salasanat eivät täsmää',
reset_key_generated: 'Salasanan vaihtoavain on luotu.',
reset_key_copy: 'Kopioi vaihto-URL leikepöydälle painamalla OK.',
login: 'Kirjaudu',
register: 'Rekisteröidy',
username: 'Käyttäjänimi',
pubkey: 'Julkinen avain',
user_id: 'Käyttäjä ID',
email: 'Sähköposti',
first_name: 'Etunimi',
@@ -338,244 +242,17 @@ window.localisation.fi = {
invalid_username: 'Virheellinen käyttäjänimi',
auth_provider: 'Tunnistamisen toimittaja',
my_account: 'Tilini',
existing_account_question: 'Onkohan sinulla jo tili?',
background_image: 'Taustakuva',
back: 'Takaisin',
logout: 'Poistu',
look_and_feel: 'Kieli ja värit',
api_token: 'API Token',
api_tokens: 'API Tokens',
access_control_list: 'Access Control List',
access_control_list_admin_warning:
'This is an admin account. The generated tokens will have admin privileges.',
new_api_acl: 'New Access Control List',
api_token_id: 'Token Id',
toggle_gradient: 'Toggle Gradient',
gradient_background: 'Gradient Background',
language: 'Kieli',
color_scheme: 'Väriteema',
admin_settings: 'Pääkäyttäjän asetukset',
extension_cost: 'Tämä laajennus edellyttää vähintään {cost} sat maksua.',
extension_cost: 'Tämä julkaisu edellyttää vähintään {cost} satsin maksua.',
extension_paid_sats: 'Olet jo maksanut {paid_sats} satsia.',
release_details_error: 'Ei voi hakea julkaisun tietoja.',
pay_from_wallet: 'Maksa lompakosta',
wallet_required: 'Lompakko *',
show_qr: 'Näytä QR',
retry_install: 'Yritä asennusta uudelleen',
new_payment: 'Luo uusi maksu',
update_payment: 'Päivitä maksu',
already_paid_question: 'Kenties maksoit jo?',
sell: 'Myy',
sell_require: 'Pyydä maksua laajennuksen käytöstä',
sell_info:
'{name} -laajennuksen aktivointi edellyttää vähintään {amount} sat maksua.',
hide_empty_wallets: 'Piilota tyhjät lompakot',
recheck: 'Tarkista uudelleen',
contributors: 'Avustajat',
license: 'Lisenssi',
reset_key: 'Vaihda avain',
reset_password: 'Vaihda salasana',
border_choices: 'Reunuksen vaihtoehdot',
select_all: 'Valitse kaikki',
nfc_supported: 'NFC on tuettu',
nfc_not_supported: 'NFC:tä ei tueta',
expire_date: 'Vanhenemispäivämäärä:',
hash: 'Tiiviste:',
welcome_lnbits: 'Tervetuloa LNbits-palveluun',
setup_su_account: 'Määritä Superuser-tili alta.',
create_ticker_converter: 'Luo valuuttamuuntimen Ticker',
enable_audit: 'Ota seuranta käyttöön',
recommended: 'Suositeltu',
audit_desc: 'Tallenna HTTP-pyyntöjä seuraavien suodattimien mukaisesti',
audit_record_req: 'Tallenna pyynnön Body',
audit_record_warning: 'Varoitus:',
audit_record_req_warning_1:
'Luottamukselliset tiedot (kuten salasanat) tallennetaan.',
audit_record_req_warning_2: 'Body-datamäätä voi olla iso.',
audit_record_use: 'Käytä varoen!',
audit_ip: 'Tallenna IP-osoite',
audit_ip_desc: 'Tallenna asiakkaan IP-osoite',
audit_path_params: 'Tallenna Path-parametrit',
audit_query_params: 'Tallenna Query-parametrit',
audit_http_methods: 'Tallenna HTTP-menetelmät',
audit_http_methods_hint:
'Luettelo mukaan otettavista HTTP-menetelmistä. Tyhjä luettelo tallettaa kaikki.',
audit_http_methods_label: 'HTTP-metodit',
audit_resp_codes: 'Tallenna HTTP-vastauskoodit',
audit_resp_codes_hint:
'HTTP-koodien lista, jotka sisällytetään (regex-match). Tyhjä luettelo tallettaa kaikki. Esim: 4.*, 5.*',
audit_resp_codes_label: 'HTTP-vastauskoodi (säännöllinen lauseke)',
audit_paths: 'Sisällytä polut',
audit_paths_hint:
'Luettelo poluista, jotka sisällytetään (regex-vastaavuus). Tyhjä luettelo tarkoittaa kaikkia.',
audit_paths_label: 'HTTP-polku (regex)',
audit_paths_exclude: 'Ohita polut',
audit_paths_exclude_hint:
'Lista poluista, jotka jätetään pois (regex-vastaavuus). Tyhjällä listalla mitään ei jätetä pois.',
audit_paths_exclude_label: 'HTTP-polku (regex)',
exchange_providers: 'Vaihtokurssin tarjoajat',
admin_extensions: 'Pääkäyttäjän laajennukset',
admin_extensions_label: 'Pääkäyttäjän laajennukset',
admin_extensions_hint:
'Laajennuksia voi käyttää vain käyttäjä, jolla on pääkäyttäjäoikeudet',
user_default_extensions: 'Käyttäjän oletuslaajennukset',
user_default_extensions_label: 'Käyttäjän laajennukset',
user_default_extensions_hint:
'Laajennukset, jotka otetaan oletusarvoisesti käyttöön kaikille käyttäjille.',
miscellanous: 'Sekalaiset',
misc_disable_extensions: 'Poista laajennukset käytöstä',
misc_disable_extensions_label: 'Poista kaikki laajennukset käytöstä',
misc_hide_api: 'Piilota API',
misc_hide_api_label:
'Piilottaa lompakon rajapinnan, laajennukset voivat valita välittävätkö tästä asetuksesta',
wallets_management: 'Lompakoiden hallinta',
funding_source_info: 'Rahoituslähteen tiedot',
funding_source: 'Rahoituslähde: {wallet_class}',
node_balance: 'Solmun saldo: {balance} sats',
lnbits_balance: 'LNbits-saldo: {balance} sat',
funding_reserve_percent: 'Omavaraisuusaste: {percent} %',
node_managment: 'Solmun hallinta',
node_management_not_supported:
'Solmun hallinta ei ole mahdollista valitun rahoituslähteen kanssa.',
toggle_node_ui: 'Solmun käyttöliittymä',
toggle_public_node_ui: 'Julkinen näkymä solmun tietoihin',
toggle_transactions_node_ui:
'Tapahtumat-välilehti (Poista käytöstä suurilla CLN-solmuilla)',
invoice_expiry: 'Laskun vanhenemisaika',
invoice_expiry_label: 'Laskun vanhentuminen (sekunteina)',
fee_reserve: 'Kuluvaraus',
fee_reserve_percent: 'Kuluvaraus prosentteina',
fee_reserve_msats: 'Kuluvaraus milli-sat',
reserve_fee_in_percent: 'Kuluvaraus prosentteina',
payment_wait_time: 'Maksun odotusaika',
payment_wait_time_desc:
'Kuinka pitkään maksua odotetaan saapuvaksi ennenkuin se merkitään Odotetaan -tilaan. Aseta pidemmäksi käytettäessä HODL-laskuja, Boltz-palvelua tms',
server_management: 'Palvelimen hallinta',
base_url: 'Palvelimen URL-osoite',
base_url_label: 'Palvelun staattinen pohja-URL',
authentication: 'Käyttäjän todennus',
auth_token_expiry_label: 'Kirjautumisen vanhentumisaika minuutteina',
auth_token_expiry_hint: 'Aika minuuteissa, jossa kirjautuminen vanhenee',
auth_allowed_methods_label: 'Sallitut kirjautumismenetelmät',
auth_allowed_methods_hint: 'Valitse kirjautumismenetelmät',
auth_nostr_label: 'Nostr kutsujen URL',
auth_nostr_hint:
'Asiakkaiden kirjautumiseen käyttämä absoluuttinen URL-osoite.',
auth_google_ci_label: 'Google-asiakastunnus',
auth_google_ci_hint:
'Varmista, että valtuutetut uudelleenohjaus-URI:t sisältävät https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Google-asiakassalasana',
auth_gh_client_id_label: 'GitHub-asiakastunnus',
auth_gh_client_id_hint:
'Varmista, että valtuutuksen paluuosoite-URL on asetettu osoitteeseen https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'GitHub-asiakassalaisuusavain',
auth_keycloak_label: 'Keycloak-discovery-URL',
auth_keycloak_ci_label: 'Keycloak-asiakastunnus',
auth_keycloak_ci_hint:
'Varmista, että valtuutuksen palautus-URL on asetettu muotoon https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
currency_settings: 'Valuutta-asetukset',
allowed_currencies: 'Käytettävät valuutat',
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
default_account_currency: 'Tilin oletusvaluutta',
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
max_incoming_payment_amount: 'Saapuvan maksun enimmäismäärä',
max_incoming_payment_amount_desc: 'Enimmäismäärä jonka voi laskuttaa',
max_outgoing_payment_amount: 'Lähtevän maksun enimmäismäärä',
max_outgoing_payment_amount_desc: 'Enimmäismäärä jonka voi maksaa',
service_fee: 'Palvelumaksut',
service_fee_label: 'Palvelumaksu (%)',
service_fee_hint: 'Tapahtumastakohtainen palvelumaksu (%)',
service_fee_max: 'Palvelumaksun enimmäismäärä',
service_fee_max_label: 'Palvelumaksu max (sat)',
service_fee_max_hint: 'Suurin veloitettava palvelumaksu (sat)',
fee_wallet: 'Palvelumaksujen lompakko',
fee_wallet_label: 'Palvelumaksujen tilityslompakko (lompakon tunnus)',
fee_wallet_hint: 'Lompakon tunnus, johon palvelumaksut tilitetään',
disable_fee: 'Poista maksu käytöstä',
disable_fee_internal: 'Poista palvelumaksu sisäisiltä maksuilta',
disable_fee_internal_desc: 'Poista palvelumaksu sisäisiltä salamaksuilta',
ui_management: 'Käyttöliittymän hallinta',
ui_site_title: 'Sivuston nimi',
ui_changing_remove_lnbits_elements:
' (tämän muuttamalla LNbits elementit poistuvat kotisivulla ja alareunasta)',
ui_site_tagline: 'Sivuston iskulause',
ui_elements_enable: 'Ota käyttöön elementit etusivulla/alareunassa',
ui_elements_disable: 'Poista elementit käytöstä etusivulla/alareunassa',
ui_toggle_elements_tip: "Poista kotisivuelementit kuten 'toimii' jne.",
ui_site_description: 'Sivuston kuvaus',
ui_site_description_hint:
'Käytä tavallista tekstiä, Markdownia tai puhdasta HTML:ää',
ui_default_wallet_name: 'Oletuslompakon nimi',
ui_default_theme: 'Oletusteema',
lnbits_wallet: 'LNbits-lompakko',
denomination: 'Valuutan nimi',
denomination_hint: 'FakeWallet-lompakon valuutan nimi',
denomination_error: 'Valuutta tunnisssa on oltava 3 merkkiä, tai `sat`',
ui_qr_code_logo: 'QR- ja Favicon-logo',
ui_qr_code_logo_hint:
'Anna QR-koodissa ja Faviconissa käytettävän logo-kuvan URL',
ui_custom_image: 'Yksilöity kuva',
ui_custom_image_label: 'Anna yksilöidyn kuvan URL-osoite',
ui_custom_image_hint:
'Yksilöity kuva näytetään aloitus- ja kirjautumissivuilla',
ui_custom_badge: 'Yksilöity tunnus',
ui_custom_badge_label:
"Yksilöity tunnus 'KÄYTÄ VAROVAISUUTTA - LNbits-lompakko on edelleen BETA-versiossa'",
ui_custom_badge_color_label: 'Kustomoidun tunnuksen väri',
themes: 'Teemat',
themes_hint: 'Valitse käyttäjille saatavilla olevat teemat',
custom_logo: 'Mukautettu logo',
custom_logo_hint: 'Logokuvan sisältävä URL-osoite',
ad_space_title: 'Mainospaikan otsikko',
ad_space_title_label: 'Palvelua tukevat ',
ad_slots: 'Mainospaikat',
ad_slots_hint:
'Mainoslinkit ja kuvatiedostopolut CSV-muodossa, lisäosat voivat valita välittävätkö asetuksesta',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Mainokset käytössä',
ads_disabled: 'Mainokset poistettu käytöstä',
user_management: 'Käyttäjänhallinta',
admin_users: 'Pääkäyttäjät',
admin_users_hint: 'Käyttäjät, joilla on pääkäyttäjän oikeudet',
admin_users_label: 'Käyttäjätunnus',
allowed_users: 'Sallitut käyttäjät',
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
allowed_users_label: 'Käyttäjätunnus',
allow_creation_user: 'Salli uusien käyttäjien luominen',
allow_creation_user_desc: 'Etusivulta on mahdollisuus luoda uusia käyttäjiä',
new_user_not_allowed: 'Tunnusten luonti on estetty.',
components: 'Komponentit',
long_running_endpoints: 'Top 5 pisimpään yhteydessä ollutta päätepistettä',
http_request_methods: 'HTTP-pyynnön menetelmät',
http_response_codes: 'HTTP-vastaukset koodit',
request_details: 'Pyynnön tiedot',
http_request_details: 'HTTP-pyynnön tiedot',
payment_details: 'Maksun yksityiskohdat',
payment_details_desc: 'Yksityiskohtaisen maksun sisältö',
payments: 'Maksut',
payment_show_internal: 'Näytä sisäiset maksut',
payment_chart_flow: 'Kuukausittainen maksuvirta',
payment_chart_status: 'Maksun Tila',
payment_chart_tx_per_wallet:
'Lompakkokohtaiset tapahtumat (saldo/kappaletta)',
payment_details_back: 'Takaisin Maksuihin',
payment_chart_tags: 'Maksut Tag:eittäin',
payments_balance_in_out: 'Saldo Sisään/Ulos',
payments_count_in_out: 'Tapahtumia Sisään/Ulos',
payments_status_chart: 'Tilakaavio',
payments_tag_chart: 'Tag-kaavio',
payments_balance_chart: 'Saldo-kaavio',
payments_wallets_chart: 'Lompakko-kaavio',
payments_balance_in_out_chart: 'Saldo Sisään/Ulos -kaavio',
payments_count_in_out_chart: 'Lukumäärä Sisään/Ulos -kaavio',
reset_wallet_keys: 'Uusi API-avaimet',
reset_wallet_keys_desc:
'Tämän lompakon API-avaimet uusitaan. Edelliset API-avaimet lakkaavat toimimasta ja uudet luodaan niiden tilalle..',
view_list: 'Näytä lompakot allekain',
view_column: 'Näytä lompakot rinnakkain',
filter_payments: 'Suodata maksuja',
filter_date: 'Suodata päiväyksellä',
websocket_example: 'Websocket esimerkki'
new_payment: 'Tee uusi maksu',
hide_empty_wallets: 'Piilota tyhjät lompakot'
}
+17 -203
View File
@@ -2,10 +2,8 @@ window.localisation.fr = {
confirm: 'Oui',
server: 'Serveur',
theme: 'Thème',
site_customisation: 'Personnalisation du site',
funding: 'Financement',
users: 'Utilisateurs',
audit: 'Audit',
apps: 'Applications',
channels: 'Canaux',
transactions: 'Transactions',
@@ -28,11 +26,10 @@ window.localisation.fr = {
restart: 'Redémarrer le serveur',
save: 'Enregistrer',
save_tooltip: 'Enregistrer vos modifications',
credit_debit: 'Crédit / Débit',
credit_hint: 'Appuyez sur Entrée pour créditer le compte',
credit_label: '{denomination} à créditer',
credit_ok:
'Succès du crédit/débit des fonds virtuels ({amount} sats). Les paiements dépendent des fonds réels sur la source de financement.',
topup: 'Renflouer',
topup_wallet: 'Reflouer un portefeuille',
topup_hint:
"Utilisez l'ID du portefeuille pour recharger n'importe quel portefeuille",
restart_tooltip:
'Redémarrez le serveur pour que les changements prennent effet',
add_funds_tooltip: 'Ajouter des fonds à un portefeuille.',
@@ -41,6 +38,8 @@ window.localisation.fr = {
'Supprimer tous les paramètres et les réinitialiser aux valeurs par défaut.',
download_backup: 'Télécharger la sauvegarde de la base de données',
name_your_wallet: 'Nommez votre portefeuille {name}',
wallet_topup_ok:
'Succès de la création de fonds virtuels ({amount} sats). Les paiements dépendent des fonds réels sur la source de financement.',
paste_invoice_label:
'Coller une facture, une demande de paiement ou un code lnurl *',
lnbits_description:
@@ -48,7 +47,6 @@ window.localisation.fr = {
export_to_phone: 'Exporter vers le téléphone avec un code QR',
export_to_phone_desc:
"Ce code QR contient l'URL de votre portefeuille avec un accès complet. Vous pouvez le scanner depuis votre téléphone pour ouvrir votre portefeuille depuis là-bas.",
wallet: 'Portefeuille :',
wallets: 'Portefeuilles',
add_wallet: 'Ajouter un nouveau portefeuille',
delete_wallet: 'Supprimer le portefeuille',
@@ -77,6 +75,8 @@ window.localisation.fr = {
api_keys_api_docs: 'URL du nœud, clés API et documentation API',
lnbits_version: 'Version de LNbits',
runs_on: 'Fonctionne sur',
credit_hint: 'Appuyez sur Entrée pour créditer le compte',
credit_label: '{denomination} à créditer',
paste: 'Coller',
paste_from_clipboard: 'Coller depuis le presse-papiers',
paste_request: 'Coller la requête',
@@ -93,7 +93,7 @@ window.localisation.fr = {
pay: 'Payer',
memo: 'Mémo',
date: 'Date',
payment_processing: 'Traitement du paiement...',
processing_payment: 'Traitement du paiement...',
not_enough_funds: 'Fonds insuffisants !',
search_by_tag_memo_amount: 'Rechercher par tag, mémo, montant',
invoice_waiting: 'Facture en attente de paiement',
@@ -112,16 +112,10 @@ window.localisation.fr = {
"La fonctionnalité de connexion sera publiée dans une future mise à jour, pour l'instant, assurez-vous de mettre cette page en favori pour accéder à votre portefeuille ultérieurement ! Ce service est en BETA, et nous ne sommes pas responsables des personnes qui perdent l'accès à leurs fonds.",
no_transactions: 'Aucune transaction effectuée pour le moment',
manage: 'Gérer',
exchanges: 'Échanges',
extensions: 'Extensions',
no_extensions: "Vous n'avez installé aucune extension :(",
created: 'Créé',
search_extensions: 'Rechercher des extensions',
extension_sources: "Sources d'extension",
ext_sources_hint:
'Dépôts à partir desquels les extensions peuvent être téléchargées',
ext_sources_label:
"URL source (utilisez uniquement la source officielle de l'extension LNbits et des sources fiables)",
warning: 'Avertissement',
repository: 'Référentiel',
confirm_continue: 'Êtes-vous sûr de vouloir continuer ?',
@@ -130,10 +124,8 @@ window.localisation.fr = {
uninstall: 'Désinstaller',
drop_db: 'Supprimer les données',
enable: 'Activer',
pay_to_enable: 'Payer pour activer',
enable_extension_details: "Activer l'extension pour l'utilisateur actuel",
disable: 'Désactiver',
delete: 'Supprimer',
installed: 'Installé',
activated: 'Activé',
deactivated: 'Désactivé',
@@ -158,19 +150,15 @@ window.localisation.fr = {
"Vous êtes sur le point de supprimer toutes les données de l'extension. Veuillez taper le nom de l'extension pour continuer :",
extension_required_lnbits_version:
'Cette version nécessite au moins LNbits version',
min_version: 'Minimum (inclus)',
max_version: 'Maximum (exclu)',
payment_hash: 'Hash de paiement',
fee: 'Frais',
amount: 'Montant',
amount_sats: 'Montant (sats)',
tag: 'Étiqueter',
unit: 'Unité',
description: 'Description',
expiry: 'Expiration',
webhook: 'Webhook',
payment_proof: 'Preuve de paiement',
update: 'Mettre à jour',
update_available: 'Mise à jour {version} disponible !',
latest_update: 'Vous êtes sur la dernière version {version}.',
notifications: 'Notifications',
@@ -180,7 +168,12 @@ window.localisation.fr = {
enable_notifications: 'Activer les notifications',
enable_notifications_desc:
'Si activé, il récupérera les dernières mises à jour du statut LNbits, telles que les incidents de sécurité et les mises à jour.',
enable_killswitch: 'Activer le Killswitch',
enable_killswitch_desc:
'Si activé, il changera automatiquement votre source de financement en VoidWallet si LNbits envoie un signal de coupure. Vous devrez activer manuellement après une mise à jour.',
killswitch_interval: 'Intervalle du Killswitch',
killswitch_interval_desc:
"À quelle fréquence la tâche de fond doit-elle vérifier le signal d'arrêt d'urgence LNbits provenant de la source de statut (en minutes).",
enable_watchdog: 'Activer le Watchdog',
enable_watchdog_desc:
'Si elle est activée, elle changera automatiquement votre source de financement en VoidWallet si votre solde est inférieur au solde LNbits. Vous devrez activer manuellement après une mise à jour.',
@@ -197,6 +190,7 @@ window.localisation.fr = {
more: 'plus',
less: 'moins',
releases: 'Versions',
killswitch: "Interrupteur d'arrêt",
watchdog: 'Chien de garde',
server_logs: 'Journaux du serveur',
ip_blocker: "Bloqueur d'IP",
@@ -230,7 +224,6 @@ window.localisation.fr = {
login_to_account: 'Connectez-vous à votre compte',
create_account: 'Créer un compte',
account_settings: 'Paramètres du compte',
signin_with_nostr: 'Continuer avec Nostr',
signin_with_google: 'Connectez-vous avec Google',
signin_with_github: 'Connectez-vous avec GitHub',
signin_with_keycloak: 'Connectez-vous avec Keycloak',
@@ -239,14 +232,11 @@ window.localisation.fr = {
password_config: 'Configuration du mot de passe',
password_repeat: 'Répétition du mot de passe',
change_password: 'Changer le mot de passe',
update_credentials: "Mettre à jour les informations d'identification",
update_pubkey: 'Mettre à jour la clé publique',
set_password: 'Définir le mot de passe',
invalid_password: 'Le mot de passe doit comporter au moins 8 caractères',
login: 'Connexion',
register: 'Inscrire',
username: "Nom d'utilisateur",
pubkey: 'Clé publique',
user_id: 'Identifiant utilisateur',
email: 'E-mail',
first_name: 'Prénom',
@@ -261,190 +251,14 @@ window.localisation.fr = {
back: 'Retour',
logout: 'Déconnexion',
look_and_feel: 'Apparence',
toggle_gradient: 'Basculer le dégradé',
gradient_background: 'Fond en dégradé',
language: 'Langue',
color_scheme: 'Schéma de couleurs',
admin_settings: 'Paramètres administrateur',
extension_cost: 'Cette version nécessite un paiement minimum de {cost} sats.',
extension_paid_sats: 'Vous avez déjà payé {paid_sats} sats.',
release_details_error: "Impossible d'obtenir les détails de la version.",
pay_from_wallet: 'Payer depuis le portefeuille',
wallet_required: 'Portefeuille *',
show_qr: 'Afficher le QR',
retry_install: "Réessayer l'installation",
new_payment: 'Effectuer un nouveau paiement',
update_payment: 'Mettre à jour le paiement',
already_paid_question: 'Avez-vous déjà payé ?',
sell: 'Vendre',
sell_require: "Demander un paiement pour activer l'extension",
sell_info:
"L'extension {name} nécessite un paiement minimum de {amount} sats pour être activée.",
hide_empty_wallets: 'Masquer les portefeuilles vides',
recheck: 'Revérifier',
contributors: 'Contributeurs',
license: 'Licence',
reset_key: 'Réinitialiser la clé',
reset_password: 'Réinitialiser le mot de passe',
border_choices: 'Choix de bordure',
select_all: 'Sélectionner tout',
nfc_supported: 'NFC pris en charge',
nfc_not_supported: 'NFC non pris en charge',
expire_date: "Date d'expiration :",
hash: 'Hash :',
welcome_lnbits: 'Bienvenue à LNbits',
setup_su_account: 'Configurez le compte Superuser ci-dessous.',
create_ticker_converter: 'Créer un convertisseur de code de devise',
enable_audit: "Activer l'audit",
recommended: 'Recommandé',
audit_desc: 'Enregistrer les requêtes HTTP selon les filtres spécifiés',
audit_record_req: 'Enregistrer le corps de la demande',
audit_record_warning: 'Avertissement :',
audit_record_req_warning_1:
'les données confidentielles (comme les mots de passe) seront enregistrées.',
audit_record_req_warning_2:
'le corps de la requête peut être de grande taille.',
audit_record_use: 'Utilisez-le avec précaution.',
audit_ip: "Enregistrer l'adresse IP",
audit_ip_desc: "Enregistrer l'adresse IP du client",
audit_path_params: 'Enregistrer les paramètres de chemin',
audit_query_params: 'Enregistrer les paramètres de la requête',
audit_http_methods: 'Inclure les méthodes HTTP',
audit_http_methods_hint:
'Liste des méthodes HTTP à inclure. Listes vides signifie toutes.',
audit_http_methods_label: 'Méthodes HTTP',
audit_resp_codes: 'Inclure les codes de réponse HTTP',
audit_resp_codes_hint:
'Liste des codes HTTP à inclure (correspondance regex). Les listes vides signifient tout. Ex : 4.*, 5.*',
audit_resp_codes_label: 'Code de réponse HTTP (regex)',
audit_paths: 'Inclure des chemins',
audit_paths_hint:
'Liste des chemins à inclure (correspondance regex). Liste vide signifie tout.',
audit_paths_label: 'Chemin HTTP (regex)',
audit_paths_exclude: 'Exclure les chemins',
audit_paths_exclude_hint:
'Liste des chemins à exclure (correspondance regex). Liste vide signifie aucun.',
audit_paths_exclude_label: 'Chemin HTTP (regex)',
exchange_providers: "Fournisseurs d'échange",
admin_extensions: "Extensions d'administration",
admin_extensions_label: "Extensions d'administration",
admin_extensions_hint:
"Seuls les utilisateurs avec des privilèges d'administrateur peuvent utiliser les extensions.",
user_default_extensions: "Extensions par défaut de l'utilisateur",
user_default_extensions_label: 'Extensions utilisateur',
user_default_extensions_hint:
'Extensions qui seront activées par défaut pour les utilisateurs.',
miscellanous: 'Divers',
misc_disable_extensions: 'Désactiver les extensions',
misc_disable_extensions_label: 'Désactiver toutes les extensions',
misc_hide_api: "Masquer l'API",
misc_hide_api_label:
"Masque l'API du portefeuille, les extensions peuvent choisir de respecter",
wallets_management: 'Gestion des portefeuilles',
funding_source_info: 'Informations sur la source de financement',
funding_source: 'Source de financement : {wallet_class}',
node_balance: 'Solde du nœud : {balance} sats',
lnbits_balance: 'Solde LNbits : {balance} sats',
funding_reserve_percent: 'Pourcentage de Réserve : {percent} %',
node_managment: 'Gestion des nœuds',
node_management_not_supported:
"La gestion des nœuds n'est pas prise en charge par la source de financement active",
toggle_node_ui: 'Interface utilisateur de nœud',
toggle_public_node_ui: 'Interface utilisateur du nœud public',
toggle_transactions_node_ui:
'Onglet des transactions (Désactiver sur les grands nœuds CLN)',
invoice_expiry: 'Expiration de la facture',
invoice_expiry_label: 'Expiration de la facture (secondes)',
fee_reserve: 'Réserve de frais',
fee_reserve_msats: 'Frais de réservation en msats',
fee_reserve_percent: 'Frais de réservation en pourcentage',
server_management: 'Gestion de serveur',
base_url: 'URL de base',
base_url_label: 'URL statique/de base pour le serveur',
authentication: 'Authentification',
auth_token_expiry_label: "Durée d'expiration du jeton (en minutes)",
auth_token_expiry_hint: "Durée en minutes avant l'expiration du jeton",
auth_allowed_methods_label: "Méthodes d'autorisation autorisées",
auth_allowed_methods_hint: "Sélectionnez les méthodes d'autorisation",
auth_nostr_label: 'URL de requête Nostr',
auth_nostr_hint: 'URL absolue que les clients utiliseront pour se connecter.',
auth_google_ci_label: 'ID Client Google',
auth_google_ci_hint:
'Assurez-vous que les URIs de redirection autorisées contiennent https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Secret client Google',
auth_gh_client_id_label: 'Identifiant client GitHub',
auth_gh_client_id_hint:
"Assurez-vous que l'URL de rappel d'autorisation est définie sur https://{domain}/api/v1/auth/github/token",
auth_gh_client_secret_label: 'Secret Client GitHub',
auth_keycloak_label: 'URL de découverte Keycloak',
auth_keycloak_ci_label: 'ID Client Keycloak',
auth_keycloak_ci_hint:
"Assurez-vous que l'URL de rappel d'autorisation est définie sur https://{domain}/api/v1/auth/keycloak/token",
auth_keycloak_cs_label: 'Secret client Keycloak',
currency_settings: 'Paramètres de devise',
allowed_currencies: 'Devises autorisées',
allowed_currencies_hint:
'Limiter le nombre de devises fiduciaires disponibles',
default_account_currency: 'Devise par défaut du compte',
default_account_currency_hint: 'Devise par défaut pour la comptabilité',
service_fee_label: 'Frais de service (%)',
service_fee_hint: 'Frais facturés par tx (%)',
service_fee_max_label: 'Frais de service max (sats)',
service_fee_max_hint: 'Frais de service maximum à facturer en (sats)',
fee_wallet: 'Portefeuille de frais',
fee_wallet_label: 'Portefeuille de frais (ID de portefeuille)',
fee_wallet_hint: 'Identifiant de portefeuille pour envoyer des fonds à',
disable_fee: 'Désactiver les frais',
disable_fee_internal:
'Désactiver les frais de service pour les paiements internes',
disable_fee_internal_desc:
'Désactiver les frais de service pour les paiements Lightning internes',
ui_management: "Gestion de l'interface utilisateur",
ui_site_title: 'Titre du site',
ui_site_tagline: 'Slogan du site',
ui_elements_enable: "Activer les éléments sur la page d'accueil",
ui_elements_disable: "Désactiver les éléments sur la page d'accueil",
ui_toggle_elements_tip:
"Supprimer les éléments de la page d'accueil comme 'fonctionne avec', etc.",
ui_site_description: 'Description du site',
ui_site_description_hint:
'Utilisez du texte brut, du Markdown ou du HTML brut',
ui_default_wallet_name: 'Nom par Défaut du Portefeuille',
lnbits_wallet: 'Portefeuille LNbits',
denomination: 'Dénomination',
denomination_hint: 'Le nom du jeton FakeWallet',
ui_qr_code_logo: 'Logo de code QR',
ui_qr_code_logo_hint: "URL de l'image du logo dans le code QR",
ui_custom_badge: 'Badge personnalisé',
ui_custom_badge_label:
"Badge personnalisé 'À UTILISER AVEC PRÉCAUTION - Le portefeuille LNbits est encore en BÊTA'",
ui_custom_badge_color_label: 'Couleur de badge personnalisée',
themes: 'Thèmes',
themes_hint: 'Choisissez des thèmes disponibles pour les utilisateurs',
custom_logo: 'Logo personnalisé',
custom_logo_hint: "URL de l'image du logo",
ad_space_title: "Titre de l'espace publicitaire",
ad_space_title_label: 'Soutenu par',
ad_slots: 'Emplacements publicitaires',
ad_slots_hint:
"URL de l'annonce et chemins des fichiers image au format CSV, les extensions peuvent choisir de respecter",
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Annonces activées',
ads_disabled: 'Publicités désactivées',
user_management: 'Gestion des utilisateurs',
admin_users: 'Utilisateurs administrateurs',
admin_users_hint: "Utilisateurs avec des privilèges d'administration",
admin_users_label: 'Identifiant utilisateur',
allowed_users: 'Utilisateurs autorisés',
allowed_users_hint: 'Seuls ces utilisateurs peuvent utiliser LNbits',
allowed_users_label: 'ID utilisateur',
allow_creation_user: 'Autoriser la création de nouveaux utilisateurs',
allow_creation_user_desc:
'Permettre la création de nouveaux utilisateurs sur la page dindex',
components: 'Composants',
long_running_endpoints: 'Top 5 points de terminaison longue durée',
http_request_methods: 'Méthodes de requête HTTP',
http_response_codes: 'Codes de réponse HTTP',
request_details: 'Détails de la demande',
http_request_details: 'Détails de la requête HTTP'
hide_empty_wallets: 'Masquer les portefeuilles vides'
}
+16 -198
View File
@@ -2,10 +2,8 @@ window.localisation.it = {
confirm: 'Sì',
server: 'Server',
theme: 'Tema',
site_customisation: 'Personalizzazione del sito',
funding: 'Funding',
users: 'Utenti',
audit: 'Verifica',
apps: 'Applicazioni',
channels: 'Canali',
transactions: 'Transazioni',
@@ -28,11 +26,9 @@ window.localisation.it = {
restart: 'Riavvia il server',
save: 'Salva',
save_tooltip: 'Salva le modifiche',
credit_debit: 'Credito / Debito',
credit_hint: 'Premere Invio per accreditare i fondi',
credit_label: '{denomination} da accreditare',
credit_ok:
'Credito/addebito riuscito di fondi virtuali ({amount} sats). I pagamenti dipendono dai fondi effettivi sulla fonte di finanziamento.',
topup: 'Ricarica',
topup_wallet: 'Ricarica un portafoglio',
topup_hint: "Usa l'ID del portafoglio per ricaricare qualsiasi portafoglio",
restart_tooltip: 'Riavvia il server affinché le modifiche abbiano effetto',
add_funds_tooltip: 'Aggiungere fondi a un portafoglio',
reset_defaults: 'Ripristina le impostazioni predefinite',
@@ -40,6 +36,8 @@ window.localisation.it = {
'Cancella tutte le impostazioni e ripristina i valori predefiniti',
download_backup: 'Scarica il backup del database',
name_your_wallet: 'Dai un nome al tuo portafoglio {name}',
wallet_topup_ok:
'Operazione riuscita nella creazione di fondi virtuali ({amount} sats). I pagamenti dipendono dai fondi effettivi sulla fonte di finanziamento.',
paste_invoice_label:
'Incolla una fattura, una richiesta di pagamento o un codice lnurl *',
lnbits_description:
@@ -47,7 +45,6 @@ window.localisation.it = {
export_to_phone: 'Esportazione su telefono con codice QR',
export_to_phone_desc:
"Questo codice QR contiene l'URL del portafoglio con accesso da amministratore. È possibile scansionarlo dal telefono per aprire il portafoglio da lì.",
wallet: 'Portafoglio:',
wallets: 'Portafogli',
add_wallet: 'Aggiungi un nuovo portafoglio',
delete_wallet: 'Elimina il portafoglio',
@@ -75,6 +72,8 @@ window.localisation.it = {
api_keys_api_docs: 'URL del nodo, chiavi API e documentazione API',
lnbits_version: 'Versione di LNbits',
runs_on: 'Esegue su',
credit_hint: 'Premere Invio per accreditare i fondi',
credit_label: '{denomination} da accreditare',
paste: 'Incolla',
paste_from_clipboard: 'Incolla dagli appunti',
paste_request: 'Richiesta di pagamento',
@@ -91,7 +90,7 @@ window.localisation.it = {
pay: 'Paga',
memo: 'Memo',
date: 'Dati',
payment_processing: 'Elaborazione pagamento...',
processing_payment: 'Elaborazione pagamento...',
not_enough_funds: 'Non ci sono abbastanza fondi!',
search_by_tag_memo_amount: 'Cerca per tag, memo, importo...',
invoice_waiting: 'Fattura in attesa di pagamento',
@@ -110,15 +109,10 @@ window.localisation.it = {
"La funzionalità di login sarà rilasciata in un futuro aggiornamento; per ora, assicuratevi di salvare tra i preferiti questa pagina per accedere nuovamente in futuro a questo portafoglio! Questo servizio è in fase BETA e non ci assumiamo alcuna responsabilità per la perdita all'accesso dei fondi",
no_transactions: 'Nessuna transazione effettuata',
manage: 'Gestisci',
exchanges: 'Scambi',
extensions: 'Estensioni',
no_extensions: 'Non ci sono estensioni installate :(',
created: 'Creato',
search_extensions: 'Estensioni di ricerca',
extension_sources: 'Fonti di estensione',
ext_sources_hint: 'Repository da cui è possibile scaricare le estensioni',
ext_sources_label:
"URL di origine (utilizzare solo la fonte ufficiale dell'estensione LNbits e fonti affidabili)",
warning: 'Attenzione',
repository: 'Deposito',
confirm_continue: 'Sei sicuro di voler continuare?',
@@ -127,10 +121,8 @@ window.localisation.it = {
uninstall: 'Disinstalla',
drop_db: 'Rimuovi Dati',
enable: 'Abilita',
pay_to_enable: 'Paga per abilitare',
enable_extension_details: "Attiva l'estensione per l'utente corrente",
disable: 'Disabilita',
delete: 'Elimina',
installed: 'Installato',
activated: 'Attivato',
deactivated: 'Disattivato',
@@ -155,19 +147,15 @@ window.localisation.it = {
"Stai per rimuovere tutti i dati per l'estensione. Digita il nome dell'estensione per continuare:",
extension_required_lnbits_version:
'Questa versione richiede almeno la versione LNbits',
min_version: 'Minimo (incluso)',
max_version: 'Massimo (escluso)',
payment_hash: 'Hash del pagamento',
fee: 'Tariffa',
amount: 'Importo',
amount_sats: 'Importo (sats)',
tag: 'Etichetta',
unit: 'Unità',
description: 'Descrizione',
expiry: 'Scadenza',
webhook: 'Webhook',
payment_proof: 'Prova di pagamento',
update: 'Aggiorna',
update_available: 'Aggiornamento {version} disponibile!',
latest_update: 'Sei sulla versione più recente {version}.',
notifications: 'Notifiche',
@@ -176,6 +164,12 @@ window.localisation.it = {
enable_notifications: 'Attiva le notifiche',
enable_notifications_desc:
'Se attivato, recupererà gli ultimi aggiornamenti sullo stato di LNbits, come incidenti di sicurezza e aggiornamenti.',
enable_killswitch: 'Attiva Killswitch',
enable_killswitch_desc:
'Se attivato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se LNbits invia un segnale di killswitch. Dovrai attivare manualmente dopo un aggiornamento.',
killswitch_interval: 'Intervallo Killswitch',
killswitch_interval_desc:
'Quanto spesso il compito in background dovrebbe controllare il segnale di killswitch LNbits dalla fonte di stato (in minuti).',
enable_watchdog: 'Attiva Watchdog',
enable_watchdog_desc:
'Se abilitato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se il tuo saldo è inferiore al saldo LNbits. Dovrai abilitarlo manualmente dopo un aggiornamento.',
@@ -192,6 +186,7 @@ window.localisation.it = {
more: 'più',
less: 'meno',
releases: 'Pubblicazioni',
killswitch: 'Interruttore di spegnimento',
watchdog: 'Cane da guardia',
server_logs: 'Registri del server',
ip_blocker: 'Blocco IP',
@@ -226,7 +221,6 @@ window.localisation.it = {
login_to_account: 'Accedi al tuo account',
create_account: 'Crea un account',
account_settings: "Impostazioni dell'account",
signin_with_nostr: 'Continua con Nostr',
signin_with_google: 'Accedi con Google',
signin_with_github: 'Accedi con GitHub',
signin_with_keycloak: 'Accedi con Keycloak',
@@ -235,14 +229,11 @@ window.localisation.it = {
password_config: 'Configurazione della password',
password_repeat: 'Ripeti la password',
change_password: 'Cambia Password',
update_credentials: 'Aggiorna credenziali',
update_pubkey: 'Aggiorna chiave pubblica',
set_password: 'Imposta password',
invalid_password: 'La password deve contenere almeno 8 caratteri',
login: 'Accesso',
register: 'Registrati',
username: 'Nome utente',
pubkey: 'Chiave pubblica',
user_id: 'ID utente',
email: 'Email',
first_name: 'Nome',
@@ -257,188 +248,15 @@ window.localisation.it = {
back: 'Indietro',
logout: 'Esci',
look_and_feel: 'Aspetto e Comportamento',
toggle_gradient: 'Attiva/disattiva gradiente',
gradient_background: 'Sfondo sfumato',
language: 'Lingua',
color_scheme: 'Schema dei colori',
admin_settings: 'Impostazioni di amministrazione',
extension_cost:
'Questa versione richiede un pagamento minimo di {cost} satoshi.',
extension_paid_sats: 'Hai già pagato {paid_sats} sats.',
release_details_error: 'Impossibile ottenere i dettagli della versione.',
pay_from_wallet: 'Paga dal Portafoglio',
wallet_required: 'Portafoglio *',
show_qr: 'Mostra QR',
retry_install: 'Riprova Installazione',
new_payment: 'Effettua Nuovo Pagamento',
update_payment: 'Aggiorna Pagamento',
already_paid_question: 'Hai già pagato?',
sell: 'Vendi',
sell_require: "Chiedi il pagamento per abilitare l'estensione",
sell_info:
"L'estensione {name} richiede un pagamento minimo di {amount} sats per essere abilitata.",
hide_empty_wallets: 'Nascondi portafogli vuoti',
recheck: 'Ricontrolla',
contributors: 'Contributori',
license: 'Licenza',
reset_key: 'Reimposta Chiave',
reset_password: 'Reimposta password',
border_choices: 'Scelte del bordo',
select_all: 'Seleziona tutto',
nfc_supported: 'Supportato NFC',
nfc_not_supported: 'NFC non supportato',
expire_date: 'Data di scadenza:',
hash: 'Hash:',
welcome_lnbits: 'Benvenuto in LNbits',
setup_su_account: "Configura l'account Superuser qui sotto.",
create_ticker_converter: 'Crea Convertitore di Simboli di Valuta',
enable_audit: 'Abilita controllo',
recommended: 'Consigliato',
audit_desc: 'Registrare le richieste HTTP secondo i filtri specificati',
audit_record_req: 'Registra il corpo della richiesta',
audit_record_warning: 'Avvertimento:',
audit_record_req_warning_1:
'I dati riservati (come le password) verranno registrati.',
audit_record_req_warning_2:
'il corpo della richiesta può avere grandi dimensioni.',
audit_record_use: 'Usalo con cautela.',
audit_ip: "Registrare l'indirizzo IP",
audit_ip_desc: "Registra l'indirizzo IP del cliente",
audit_path_params: 'Registra i parametri del percorso',
audit_query_params: 'Registrare i parametri di query',
audit_http_methods: 'Includi i metodi HTTP',
audit_http_methods_hint:
'Elenco di metodi HTTP da includere. Liste vuote significano tutti.',
audit_http_methods_label: 'Metodi HTTP',
audit_resp_codes: 'Includere codici di risposta HTTP',
audit_resp_codes_hint:
'Elenco dei codici HTTP da includere (corrispondenza regex). Liste vuote significano tutto. Ad esempio: 4.*, 5.*',
audit_resp_codes_label: 'Codice di risposta HTTP (regex)',
audit_paths: 'Includi percorsi',
audit_paths_hint:
'Elenco dei percorsi da includere (corrispondenza regex). Elenco vuoto significa tutto.',
audit_paths_label: 'Percorso HTTP (regex)',
audit_paths_exclude: 'Escludi percorsi',
audit_paths_exclude_hint:
'Elenco dei percorsi da escludere (corrispondenza regex). Un elenco vuoto significa nessuno.',
audit_paths_exclude_label: 'Percorso HTTP (regex)',
exchange_providers: 'Fornitori di scambio',
admin_extensions: 'Estensioni Admin',
admin_extensions_label: 'Estensioni amministrative',
admin_extensions_hint:
'Solo un utente con privilegi di amministratore può utilizzare le estensioni.',
user_default_extensions: "Estensioni predefinite dell'utente",
user_default_extensions_label: 'Estensioni utente',
user_default_extensions_hint:
'Estensioni che saranno abilitate di default per gli utenti.',
miscellanous: 'Varie',
misc_disable_extensions: 'Disabilita estensioni',
misc_disable_extensions_label: 'Disabilita tutte le estensioni',
misc_hide_api: 'Nascondi API',
misc_hide_api_label:
"Nasconde l'api del portafoglio, le estensioni possono scegliere di onorare",
wallets_management: 'Gestione dei portafogli',
funding_source_info: 'Informazioni sulla fonte di finanziamento',
funding_source: 'Fonte di finanziamento: {wallet_class}',
node_balance: 'Saldo Nodo: {balance} sats',
lnbits_balance: 'Saldo LNbits: {balance} sats',
funding_reserve_percent: 'Riserva Percentuale: {percent} %',
node_managment: 'Gestione dei nodi',
node_management_not_supported:
'La gestione dei nodi non è supportata dalla fonte di finanziamento attiva.',
toggle_node_ui: 'Interfaccia utente del nodo',
toggle_public_node_ui: 'Interfaccia Utente Nodo Pubblico',
toggle_transactions_node_ui:
'Scheda Transazioni (Disabilita su nodi CLN grandi)',
invoice_expiry: 'Scadenza fattura',
invoice_expiry_label: 'Scadenza fattura (secondi)',
fee_reserve: 'Riserva delle commissioni',
fee_reserve_msats: 'Tariffa di prenotazione in msats',
fee_reserve_percent: 'Commissione di riserva in percentuale',
server_management: 'Gestione server',
base_url: 'URL di base',
base_url_label: 'URL statica/base per il server',
authentication: 'Autenticazione',
auth_token_expiry_label: 'Minuti di scadenza del token',
auth_token_expiry_hint: 'Tempo in minuti fino alla scadenza del token',
auth_allowed_methods_label: 'Metodi di autorizzazione consentiti',
auth_allowed_methods_hint: 'Seleziona i metodi di autorizzazione',
auth_nostr_label: 'URL richiesta Nostr',
auth_nostr_hint: 'URL assoluto che i clienti utilizzeranno per accedere.',
auth_google_ci_label: 'ID client di Google',
auth_google_ci_hint:
'Assicurati che gli URI di reindirizzamento autorizzati contengano https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Google Client Secret',
auth_gh_client_id_label: 'ID client di GitHub',
auth_gh_client_id_hint:
"Assicurati che l'URL di callback dell'autorizzazione sia impostato su https://{domain}/api/v1/auth/github/token",
auth_gh_client_secret_label: 'Segreto Client GitHub',
auth_keycloak_label: 'URL di individuazione di Keycloak',
auth_keycloak_ci_label: 'ID client di Keycloak',
auth_keycloak_ci_hint:
"Assicurati che l'URL di callback dell'autorizzazione sia impostato su https://{domain}/api/v1/auth/keycloak/token",
auth_keycloak_cs_label: 'Keycloak Client Secret',
currency_settings: 'Impostazioni valuta',
allowed_currencies: 'Valute consentite',
allowed_currencies_hint: 'Limita il numero di valute fiat disponibili',
default_account_currency: 'Valuta predefinita del conto',
default_account_currency_hint: 'Valuta predefinita per la contabilità',
service_fee_label: 'Tassa di servizio (%)',
service_fee_hint: 'Tariffa addebitata per transazione (%)',
service_fee_max_label: 'Commissione di servizio max (sats)',
service_fee_max_hint: 'Commissione massima da addebitare in (sats)',
fee_wallet: 'Portafoglio delle commissioni',
fee_wallet_label: 'Portafoglio delle commissioni (ID portafoglio)',
fee_wallet_hint: 'ID portafoglio a cui inviare fondi',
disable_fee: 'Disabilita Commissione',
disable_fee_internal:
'Disabilita la commissione di servizio per i pagamenti interni',
disable_fee_internal_desc:
'Disabilita la commissione di servizio per i pagamenti Lightning interni',
ui_management: "Gestione dell'interfaccia utente",
ui_site_title: 'Titolo del sito',
ui_site_tagline: 'Slogan del sito',
ui_elements_enable: 'Abilita elementi sulla homepage',
ui_elements_disable: 'Disabilita elementi sulla homepage',
ui_toggle_elements_tip: "Rimuovi elementi della homepage come 'runs on' ecc.",
ui_site_description: 'Descrizione del sito',
ui_site_description_hint: 'Usa testo normale, Markdown o HTML grezzo',
ui_default_wallet_name: 'Nome predefinito del portafoglio',
lnbits_wallet: 'Portafoglio LNbits',
denomination: 'Denominazione',
denomination_hint: 'Il nome per il token FakeWallet',
ui_qr_code_logo: 'Logo del codice QR',
ui_qr_code_logo_hint: "URL all'immagine del logo nel codice QR",
ui_custom_badge: 'Badge personalizzato',
ui_custom_badge_label:
"Badge personalizzato 'USARE CON CAUTELA - Il portafoglio LNbits è ancora in BETA'",
ui_custom_badge_color_label: 'Colore distintivo personalizzato',
themes: 'Temi',
themes_hint: 'Scegli i temi disponibili per gli utenti',
custom_logo: 'Logo personalizzato',
custom_logo_hint: "URL all'immagine del logo",
ad_space_title: 'Titolo Spazio Pubblicitario',
ad_space_title_label: 'Supportato da',
ad_slots: 'Spazi pubblicitari',
ad_slots_hint:
"Percorso dell'URL e dell'immagine in formato CSV, le estensioni possono scegliere di rispettare",
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Annunci abilitati',
ads_disabled: 'Annunci disabilitati',
user_management: 'Gestione utenti',
admin_users: 'Utenti amministratori',
admin_users_hint: 'Utenti con privilegi di amministratore',
admin_users_label: 'ID utente',
allowed_users: 'Utenti consentiti',
allowed_users_hint: 'Solo questi utenti possono usare LNbits',
allowed_users_label: 'ID utente',
allow_creation_user: 'Consenti la creazione di nuovi utenti',
allow_creation_user_desc:
'Consenti la creazione di nuovi utenti nella pagina indice',
components: 'Componenti',
long_running_endpoints: 'I primi 5 endpoint a lunga esecuzione',
http_request_methods: 'Metodi di richiesta HTTP',
http_response_codes: 'Codici di risposta HTTP',
request_details: 'Dettagli della richiesta',
http_request_details: 'Dettagli della richiesta HTTP'
hide_empty_wallets: 'Nascondi portafogli vuoti'
}
+17 -195
View File
@@ -2,10 +2,8 @@ window.localisation.jp = {
confirm: 'はい',
server: 'サーバー',
theme: 'テーマ',
site_customisation: 'サイトカスタマイズ',
funding: '資金調達',
users: 'ユーザー',
audit: '監査',
apps: 'アプリ',
channels: 'チャンネル',
transactions: 'トランザクション',
@@ -28,23 +26,23 @@ window.localisation.jp = {
restart: 'サーバーを再起動する',
save: '保存',
save_tooltip: '変更を保存する',
credit_debit: 'クレジット / デビット',
credit_hint:
'クレジットカードを使用して資金を追加するには、LNbitsを使用してください。',
credit_label: '{denomination} をクレジットに',
topup: 'トップアップ',
topup_wallet: 'ウォレットをトップアップする',
topup_hint: 'ウォレットIDを使用して、任意のウォレットをトップアップできます',
restart_tooltip: 'サーバーを再起動して変更を適用します',
add_funds_tooltip: 'ウォレットに資金を追加します。',
reset_defaults: 'リセット',
reset_defaults_tooltip: 'すべての設定を削除してデフォルトに戻します。',
download_backup: 'データベースのバックアップをダウンロードする',
name_your_wallet: 'あなたのウォレットの名前 {name}',
wallet_topup_ok:
'仮想資金の作成に成功しました({amount} sats)。支払いは資金ソースの実際の資金に依存します。',
paste_invoice_label: '請求書を貼り付けてください',
lnbits_description:
'簡単にインストールでき、軽量なLNbitsは、あらゆるライトニングネットワークの資金源と、LNbits自身でさえも実行できます!LNbitsを個人で実行することも、他人に対してカストディアンソリューションをで実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
export_to_phone: '電話にエクスポート',
export_to_phone_desc:
'ウォレットを電話にエクスポートすると、ウォレットを削除する前にウォレットを復元できます。ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
wallet: 'ウォレット:',
wallets: 'ウォレット',
add_wallet: 'ウォレットを追加',
delete_wallet: 'ウォレットを削除',
@@ -71,6 +69,9 @@ window.localisation.jp = {
api_keys_api_docs: 'ノードURL、APIキー、APIドキュメント',
lnbits_version: 'LNbits バージョン',
runs_on: 'で実行',
credit_hint:
'クレジットカードを使用して資金を追加するには、LNbitsを使用してください。',
credit_label: '{denomination} をクレジットに',
paste: '貼り付け',
paste_from_clipboard: 'クリップボードから貼り付け',
paste_request: 'リクエストを貼り付ける',
@@ -87,7 +88,7 @@ window.localisation.jp = {
pay: '支払う',
memo: 'メモ',
date: '日付',
payment_processing: '支払い処理中',
processing_payment: '支払い処理中',
not_enough_funds: '資金が不足しています',
search_by_tag_memo_amount: 'タグ、メモ、金額で検索',
invoice_waiting: '請求書を待っています',
@@ -105,15 +106,10 @@ window.localisation.jp = {
'ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。ウォレットを削除する前に、ウォレットをエクスポートしてください。',
no_transactions: 'トランザクションはありません',
manage: '管理',
exchanges: '取引所',
extensions: '拡張機能',
no_extensions: '拡張機能はありません',
created: '作成済み',
search_extensions: '検索拡張機能',
extension_sources: '拡張ソース',
ext_sources_hint: '拡張機能をダウンロードできるリポジトリ',
ext_sources_label:
'ソースURL(公式のLNbits拡張ソースおよび信頼できるソースのみを使用してください)',
warning: '警告',
repository: 'リポジトリ',
confirm_continue: '続行してもよろしいですか?',
@@ -122,10 +118,8 @@ window.localisation.jp = {
uninstall: 'アンインストール',
drop_db: 'データを削除',
enable: '有効',
pay_to_enable: '有効にするために支払う',
enable_extension_details: '現在のユーザーの拡張機能を有効にする',
disable: '無効',
delete: '削除',
installed: 'インストール済み',
activated: '有効化',
deactivated: '無効化',
@@ -150,19 +144,15 @@ window.localisation.jp = {
'エクステンションのすべてのデータを削除しようとしています。続行するには、エクステンションの名前を入力してください:',
extension_required_lnbits_version:
'このリリースには少なくとも LNbits バージョンが必要です',
min_version: '最小値(含む)',
max_version: '最大(除外)',
payment_hash: '支払いハッシュ',
fee: '料金',
amount: '量',
amount_sats: '金額 (サッツ)',
tag: 'タグ',
unit: '単位',
description: '説明',
expiry: '有効期限',
webhook: 'ウェブフック',
payment_proof: '支払い証明',
update: '更新',
update_available: 'アップデート{version}が利用可能です!',
latest_update: 'あなたは最新バージョン{version}を使用しています。',
notifications: '通知',
@@ -171,6 +161,12 @@ window.localisation.jp = {
enable_notifications: '通知を有効にする',
enable_notifications_desc:
'有効にすると、セキュリティインシデントやアップデートのような最新のLNbitsステータス更新を取得します。',
enable_killswitch: 'キルスイッチを有効にする',
enable_killswitch_desc:
'有効にすると、LNbitsからキルスイッチ信号が送信された場合に自動的に資金源をVoidWalletに切り替えます。更新後には手動で有効にする必要があります。',
killswitch_interval: 'キルスイッチ間隔',
killswitch_interval_desc:
'バックグラウンドタスクがステータスソースからLNbitsキルスイッチ信号を確認する頻度(分単位)。',
enable_watchdog: 'ウォッチドッグを有効にする',
enable_watchdog_desc:
'有効にすると、残高がLNbitsの残高より少ない場合に、資金源を自動的にVoidWalletに変更します。アップデート後は手動で有効にする必要があります。',
@@ -187,6 +183,7 @@ window.localisation.jp = {
more: 'より多くの',
less: '少ない',
releases: 'リリース',
killswitch: 'キルスイッチ',
watchdog: 'ウォッチドッグ',
server_logs: 'サーバーログ',
ip_blocker: 'IPブロッカー',
@@ -220,7 +217,6 @@ window.localisation.jp = {
login_to_account: 'アカウントにログインしてください',
create_account: 'アカウントを作成',
account_settings: 'アカウント設定',
signin_with_nostr: 'Nostrで続行',
signin_with_google: 'Googleでサインイン',
signin_with_github: 'GitHubでサインイン',
signin_with_keycloak: 'Keycloakでサインイン',
@@ -229,14 +225,11 @@ window.localisation.jp = {
password_config: 'パスワード設定',
password_repeat: 'パスワードの再入力',
change_password: 'パスワードを変更',
update_credentials: '資格情報を更新する',
update_pubkey: '公開鍵を更新',
set_password: 'パスワードを設定',
invalid_password: 'パスワードは少なくとも8文字必要です',
login: 'ログイン',
register: '登録',
username: 'ユーザー名',
pubkey: '公開鍵',
user_id: 'ユーザーID',
email: 'メール',
first_name: '名',
@@ -251,185 +244,14 @@ window.localisation.jp = {
back: '戻る',
logout: 'ログアウト',
look_and_feel: 'ルック・アンド・フィール',
toggle_gradient: 'グラデーションを切り替える',
gradient_background: 'グラデーション背景',
language: '言語',
color_scheme: 'カラースキーム',
admin_settings: '管理設定',
extension_cost: 'このリリースには最低 {cost} サトシの支払いが必要です。',
extension_paid_sats: 'すでに{paid_sats} satsを支払いました。',
release_details_error: 'リリースの詳細を取得できません。',
pay_from_wallet: 'ウォレットから支払う',
wallet_required: 'ウォレット *',
show_qr: 'QRを表示',
retry_install: '再試行インストール',
new_payment: '新しい支払いを作成する',
update_payment: '支払いを更新する',
already_paid_question: 'すでに支払いましたか?',
sell: '販売する',
sell_require: '拡張を有効にするために支払いを求める',
sell_info:
'{name}拡張機能を有効にするには、最小{amount}サツの支払いが必要です。',
hide_empty_wallets: '空のウォレットを非表示にする',
recheck: '再確認',
contributors: '貢献者',
license: 'ライセンス',
reset_key: 'リセットキー',
reset_password: 'パスワードをリセットする',
border_choices: '境界の選択肢',
select_all: 'すべて選択',
nfc_supported: 'NFC対応',
nfc_not_supported: 'NFCがサポートされていません',
expire_date: '有効期限日:',
hash: 'ハッシュ:',
welcome_lnbits: 'LNbitsへようこそ',
setup_su_account: 'スーパーアカウントを以下に設定してください。',
create_ticker_converter: '通貨ティッカーコンバーターを作成',
enable_audit: '監査を有効にする',
recommended: '推奨',
audit_desc: '指定されたフィルターに従ってHTTPリクエストを記録する',
audit_record_req: 'リクエストボディの記録',
audit_record_warning: '警告:',
audit_record_req_warning_1: 'パスワードなどの機密データが記録されます。',
audit_record_req_warning_2:
'リクエストボディは大きなサイズになる可能性があります。',
audit_record_use: '注意して使用してください。',
audit_ip: 'IPアドレスを記録する',
audit_ip_desc: 'クライアントのIPアドレスを記録する',
audit_path_params: 'パスパラメータを記録',
audit_query_params: 'クエリパラメータを記録する',
audit_http_methods: 'HTTPメソッドを含める',
audit_http_methods_hint:
'含めるHTTPメソッドのリスト。空のリストはすべてを意味します。',
audit_http_methods_label: 'HTTPメソッド',
audit_resp_codes: 'HTTPレスポンスコードを含める',
audit_resp_codes_hint:
'含めるHTTPコードの一覧(正規表現で一致)。空のリストはすべてを意味します。例: 4.*, 5.*',
audit_resp_codes_label: 'HTTPレスポンスコード(正規表現)',
audit_paths: 'パスを含める',
audit_paths_hint:
'含めるパスのリスト(正規表現マッチ)。空のリストはすべてを意味します。',
audit_paths_label: 'HTTP パス (正規表現)',
audit_paths_exclude: 'パスを除外',
audit_paths_exclude_hint:
'除外するパスの一覧(正規表現の一致)。空のリストは対象がないことを意味します。',
audit_paths_exclude_label: 'HTTP パス (正規表現)',
exchange_providers: '取引所プロバイダー',
admin_extensions: '管理拡張機能',
admin_extensions_label: '管理者拡張機能',
admin_extensions_hint: '拡張機能は管理者権限を持つユーザーのみが使用できます',
user_default_extensions: 'ユーザーデフォルト拡張機能',
user_default_extensions_label: 'ユーザー拡張機能',
user_default_extensions_hint:
'ユーザーに対してデフォルトで有効化される拡張機能。',
miscellanous: 'その他',
misc_disable_extensions: '拡張機能を無効にする',
misc_disable_extensions_label: 'すべての拡張機能を無効にする',
misc_hide_api: 'APIを非表示',
misc_hide_api_label:
'ウォレットAPIを隠すことができ、拡張機能は尊重することを選ぶことができます。',
wallets_management: 'ウォレット管理',
funding_source_info: '資金源情報',
funding_source: '資金源: {wallet_class}',
node_balance: 'ノード残高: {balance} サッツ',
lnbits_balance: 'LNbits残高: {balance} sats',
funding_reserve_percent: '予約パーセント: {percent} %',
node_managment: 'ノード管理',
node_management_not_supported:
'アクティブな資金源ではノード管理がサポートされていません',
toggle_node_ui: 'ノードUI',
toggle_public_node_ui: 'パブリックノードUI',
toggle_transactions_node_ui:
'トランザクションタブ(大規模なCLNノードで無効化)',
invoice_expiry: 'インボイスの有効期限',
invoice_expiry_label: 'インボイスの有効期限(秒)',
fee_reserve: '料金予約',
fee_reserve_msats: 'ミリサトシでの予約手数料',
fee_reserve_percent: 'パーセンテージの予約料',
server_management: 'サーバー管理',
base_url: 'ベースURL',
base_url_label: 'サーバーの静的/基本URL',
authentication: '認証',
auth_token_expiry_label: 'トークン有効期限(分)',
auth_token_expiry_hint: 'トークンが失効するまでの時間(分)',
auth_allowed_methods_label: '許可された認証方法',
auth_allowed_methods_hint: '認証方法を選択',
auth_nostr_label: 'Nostr リクエスト URL',
auth_nostr_hint: 'クライアントがログインするために使用する絶対URL。',
auth_google_ci_label: 'Google クライアントID',
auth_google_ci_hint:
'認可されたリダイレクトURIにhttps://{domain}/api/v1/auth/google/tokenが含まれていることを確認してください',
auth_google_cs_label: 'Google クライアントシークレット',
auth_gh_client_id_label: 'GitHub クライアントID',
auth_gh_client_id_hint:
'認証コールバックURLがhttps://{domain}/api/v1/auth/github/tokenに設定されていることを確認してください。',
auth_gh_client_secret_label: 'GitHub クライアントシークレット',
auth_keycloak_label: 'キーコーク ディスカバリー URL',
auth_keycloak_ci_label: 'Keycloak クライアント ID',
auth_keycloak_ci_hint:
'認証コールバックURLが https://{domain}/api/v1/auth/keycloak/token に設定されていることを確認してください。',
auth_keycloak_cs_label: 'キークローククライアントシークレット',
currency_settings: '通貨設定',
allowed_currencies: '許可されている通貨',
allowed_currencies_hint: '利用可能な法定通貨の数を制限する',
default_account_currency: 'デフォルト口座通貨',
default_account_currency_hint: '会計のデフォルト通貨',
service_fee_label: 'サービス料 (%)',
service_fee_hint: '1 取引あたりの手数料 (%)',
service_fee_max_label: 'サービス料最大 (sats)',
service_fee_max_hint: '(サット)での最大サービス料金',
fee_wallet: '手数料ウォレット',
fee_wallet_label: '手数料ウォレット (ウォレットID)',
fee_wallet_hint: '送金先のウォレットID',
disable_fee: '手数料を無効にする',
disable_fee_internal: '内部支払に対するサービス手数料を無効にする',
disable_fee_internal_desc:
'内部のライトニングペイメントのサービス料金を無効にする',
ui_management: 'UI管理',
ui_site_title: 'サイトのタイトル',
ui_site_tagline: 'サイトのタグライン',
ui_elements_enable: 'ホームページの要素を有効にする',
ui_elements_disable: 'ホームページの要素を無効にする',
ui_toggle_elements_tip: '「runs on」などのホームページ要素を削除します。',
ui_site_description: 'サイトの説明',
ui_site_description_hint:
'プレーンテキスト、Markdown、または生のHTMLを使用してください。',
ui_default_wallet_name: 'デフォルトウォレット名',
lnbits_wallet: 'LNbitsウォレット',
denomination: '額面',
denomination_hint: 'FakeWalletトークンの名前',
ui_qr_code_logo: 'QRコードロゴ',
ui_qr_code_logo_hint: 'QRコードのロゴ画像のURL',
ui_custom_badge: 'カスタムバッジ',
ui_custom_badge_label:
'カスタムバッジ「使用に注意 - LNbitsウォレットはまだベータ版です」',
ui_custom_badge_color_label: 'カスタムバッジカラー',
themes: 'テーマ',
themes_hint: 'ユーザーが利用可能なテーマを選択してください',
custom_logo: 'カスタムロゴ',
custom_logo_hint: 'ロゴ画像へのURL',
ad_space_title: '広告スペースのタイトル',
ad_space_title_label: 'サポートされています',
ad_slots: '広告スロット',
ad_slots_hint:
'CSV形式の広告URLと画像ファイルパス、拡張機能は遵守することを選択できます',
ad_slots_label: 'URL;img_light_url;img_dark_url、URL...',
ads_enabled: '広告が有効になっています',
ads_disabled: '広告が無効になっています',
user_management: 'ユーザー管理',
admin_users: '管理者ユーザー',
admin_users_hint: '管理者権限を持つユーザー',
admin_users_label: 'ユーザーID',
allowed_users: '許可されたユーザー',
allowed_users_hint: 'これらのユーザーのみがLNbitsを使用できます。',
allowed_users_label: 'ユーザーID',
allow_creation_user: '新しいユーザーの作成を許可',
allow_creation_user_desc:
'インデックスページで新しいユーザーの作成を許可する',
components: 'コンポーネント',
long_running_endpoints: 'トップ5の長時間実行エンドポイント',
http_request_methods: 'HTTPリクエストメソッド',
http_response_codes: 'HTTPレスポンスコード',
request_details: 'リクエストの詳細',
http_request_details: 'HTTPリクエストの詳細'
hide_empty_wallets: '空のウォレットを非表示にする'
}

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