Compare commits
159
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5714848434 | ||
|
|
f87e5d4372 | ||
|
|
a0502f1676 | ||
|
|
f6c8a308dc | ||
|
|
6df3933c1b | ||
|
|
ffef085fa7 | ||
|
|
913991c72f | ||
|
|
6f611461f2 | ||
|
|
681385e2a2 | ||
|
|
883d52c303 | ||
|
|
1058461882 | ||
|
|
3a7a88857a | ||
|
|
6834b5e00f | ||
|
|
10093bb465 | ||
|
|
1323a2005b | ||
|
|
bafb4ddf75 | ||
|
|
aa050eaf49 | ||
|
|
0c76efa2b9 | ||
|
|
681730a4af | ||
|
|
63adcb6780 | ||
|
|
35f7821183 | ||
|
|
b185d9585c | ||
|
|
30a8d88ada | ||
|
|
f7e984198b | ||
|
|
eb09e95e88 | ||
|
|
a883e2c7d8 | ||
|
|
e06abb52ce | ||
|
|
9f64b5349d | ||
|
|
a6bbddce41 | ||
|
|
adb9d24673 | ||
|
|
dc85f26964 | ||
|
|
14153b4a8f | ||
|
|
b8a5f3942c | ||
|
|
475ae5736e | ||
|
|
d7f0f4da0c | ||
|
|
f7b3444be4 | ||
|
|
28e32a54a4 | ||
|
|
1833adc0a1 | ||
|
|
b759ec7468 | ||
|
|
2661d12ff1 | ||
|
|
ed419f27ac | ||
|
|
c3efd48108 | ||
|
|
4a28c22d79 | ||
|
|
0d0eb36de4 | ||
|
|
2aa73bfe75 | ||
|
|
ebd080192c | ||
|
|
41abe63f18 | ||
|
|
f61471b0f2 | ||
|
|
9bd037b6e7 | ||
|
|
a4ca88b6a4 | ||
|
|
f2b9aafc51 | ||
|
|
ec77a00f49 | ||
|
|
991e0db50b | ||
|
|
fe9b62e8a8 | ||
|
|
c5964436b5 | ||
|
|
4ce14e2312 | ||
|
|
7021add193 | ||
|
|
4be01a405c | ||
|
|
ebe9ff700f | ||
|
|
a3619d40c6 | ||
|
|
048aff3db4 | ||
|
|
89d85a0d7e | ||
|
|
5aa1f9b0f8 | ||
|
|
5dc1705fa6 | ||
|
|
0fc657c55c | ||
|
|
8641b34a3d | ||
|
|
30cf6913af | ||
|
|
10682cf736 | ||
|
|
ee725846c5 | ||
|
|
69b31b1480 | ||
|
|
73a57ecf97 | ||
|
|
ab0f52d811 | ||
|
|
8c59f3f4ae | ||
|
|
1bfb09ddab | ||
|
|
2af5ac9678 | ||
|
|
4511891297 | ||
|
|
eeca7de10d | ||
|
|
b06c16ed57 | ||
|
|
2f2a3b1092 | ||
|
|
9fe63b49d6 | ||
|
|
6754ffb9dc | ||
|
|
4afe6dbd6d | ||
|
|
0cd94e654e | ||
|
|
d1692a18dd | ||
|
|
0305d740ee | ||
|
|
05777ca953 | ||
|
|
1e083856ed | ||
|
|
c08623277e | ||
|
|
60de308c8f | ||
|
|
2aebe72df8 | ||
|
|
2bd7600bcf | ||
|
|
e318f7679e | ||
|
|
0912d7d1c8 | ||
|
|
bfa23568e3 | ||
|
|
b76d8b5458 | ||
|
|
1cef614349 | ||
|
|
16fbfd1c6f | ||
|
|
15ba3c33a3 | ||
|
|
b33a1903e8 | ||
|
|
2d41a1bed3 | ||
|
|
4d490506f3 | ||
|
|
ed3a54e57a | ||
|
|
61e8d0059b | ||
|
|
4a640dc123 | ||
|
|
1f4160fda5 | ||
|
|
dc91fbea9b | ||
|
|
7eb021f43c | ||
|
|
69350849b1 | ||
|
|
828261d696 | ||
|
|
90dd3cc73a | ||
|
|
10c60291d2 | ||
|
|
9d2e52f694 | ||
|
|
503a599341 | ||
|
|
a8809ed5a4 | ||
|
|
e134c5c7b9 | ||
|
|
0efb52664e | ||
|
|
53b00c6d41 | ||
|
|
9a1d707f54 | ||
|
|
736699af94 | ||
|
|
432b3a0fe0 | ||
|
|
34a959f0bc | ||
|
|
c1d26bb274 | ||
|
|
b24a65cf2a | ||
|
|
960c58db87 | ||
|
|
dd9b217fdf | ||
|
|
7af9da4a58 | ||
|
|
c23eae9e08 | ||
|
|
66b2ca2ac4 | ||
|
|
25e5423511 | ||
|
|
4c7567a552 | ||
|
|
73e716085b | ||
|
|
cc33a49b12 | ||
|
|
eeebaffbed | ||
|
|
8633891485 | ||
|
|
cb5cc1e5e8 | ||
|
|
f845bfe651 | ||
|
|
b6bdf50ed7 | ||
|
|
56a4b702f3 | ||
|
|
273ab9781f | ||
|
|
8a759c8fc1 | ||
|
|
c7ebe09463 | ||
|
|
5d4d22dcdd | ||
|
|
b164317121 | ||
|
|
f415a92914 | ||
|
|
16c78f1901 | ||
|
|
6a08d20fe8 | ||
|
|
5abfbdd07a | ||
|
|
bba7614012 | ||
|
|
2ee7180aa0 | ||
|
|
982732cbd4 | ||
|
|
dfdb34852c | ||
|
|
8fa5c81088 | ||
|
|
91748fc891 | ||
|
|
0574837950 | ||
|
|
7e27b2c6f0 | ||
|
|
6f3d0edfbd | ||
|
|
3900d2871d | ||
|
|
0c5b909c7a | ||
|
|
78437eaf94 |
+15
-12
@@ -1,5 +1,17 @@
|
||||
#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 ###########
|
||||
######################################
|
||||
@@ -14,7 +26,7 @@ LNBITS_ADMIN_UI=true
|
||||
|
||||
# Change theme
|
||||
LNBITS_SITE_TITLE="LNbits"
|
||||
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
|
||||
LNBITS_SITE_TAGLINE="Open Source Lightning Payments Platform"
|
||||
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"
|
||||
@@ -133,17 +145,8 @@ 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
|
||||
@@ -183,7 +186,7 @@ LNBITS_ADMIN_USERS=""
|
||||
# SUPER_USER=""
|
||||
|
||||
# Extensions only admin can access
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok, nostrclient"
|
||||
# Extensions enabled by default when a user is created
|
||||
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ inputs:
|
||||
default: "3.10"
|
||||
poetry-version:
|
||||
description: "Poetry Version"
|
||||
default: "1.7.0"
|
||||
default: "1.8.5"
|
||||
node-version:
|
||||
description: "Node Version"
|
||||
default: "20.x"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Name=LNbits
|
||||
Exec=lnbits
|
||||
Icon=lnbits
|
||||
Type=Application
|
||||
Categories=X-Bitcoin;X-LightningNetwork;X-Finance;Network;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,72 @@
|
||||
name: Build LNbits AppImage DMG
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
|
||||
build-linux-package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Step 1: Checkout the repository
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Step 2: Install Dependencies
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
shell: bash
|
||||
|
||||
# Step 3: Clone LNbits Repository
|
||||
- name: Clone LNbits
|
||||
run: |
|
||||
mv .github/packaging packaging
|
||||
mkdir -p packaging/linux/AppDir/usr
|
||||
git clone https://github.com/lnbits/lnbits.git packaging/linux/AppDir/usr/lnbits
|
||||
shell: bash
|
||||
|
||||
# Step 4: Make the AppImage Asset
|
||||
- name: Make Asset
|
||||
run: |
|
||||
cd packaging/linux/AppDir/usr/lnbits
|
||||
poetry install
|
||||
poetry run pip install pyinstaller
|
||||
|
||||
# Build the LNbits binary
|
||||
poetry run pyinstaller --onefile --name lnbits --hidden-import=embit --collect-all embit --collect-all lnbits --collect-all sqlalchemy --collect-all aiosqlite --hidden-import=passlib.handlers.bcrypt $(poetry run which lnbits)
|
||||
cd ../../../../..
|
||||
chmod +x packaging/linux/AppDir/AppRun
|
||||
chmod +x packaging/linux/AppDir/lnbits.desktop
|
||||
chmod +x packaging/linux/AppDir/usr/lnbits/dist/lnbits
|
||||
|
||||
find packaging/linux/AppDir/usr/lnbits -mindepth 1 -maxdepth 1 \
|
||||
! -name 'dist' \
|
||||
! -name 'lnbits' \
|
||||
-exec rm -rf {} +
|
||||
|
||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
TAG_NAME=${{ github.event.release.tag_name }}
|
||||
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
|
||||
./appimagetool-x86_64.AppImage --updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" packaging/linux/AppDir "$APPIMAGE_NAME"
|
||||
chmod +x "$APPIMAGE_NAME"
|
||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
# Step 5: Upload Linux Release Asset
|
||||
- name: Upload Linux Release Asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: ${{ env.APPIMAGE_NAME }}
|
||||
asset_name: ${{ env.APPIMAGE_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -63,6 +63,8 @@ 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
|
||||
|
||||
@@ -3,7 +3,7 @@ name: release-rc
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*-rc[0-9]"
|
||||
- "*-rc[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
+3
-1
@@ -39,6 +39,8 @@ 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
|
||||
@@ -51,7 +53,7 @@ lnbits-backup.zip
|
||||
|
||||
# Ignore extensions (post installable extension PR)
|
||||
/lnbits/extensions
|
||||
/upgrades/
|
||||
/lnbits/upgrades/
|
||||
|
||||
# builded python package
|
||||
dist
|
||||
|
||||
+2
-2
@@ -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 -
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
@@ -32,7 +32,7 @@ RUN apt-get update && apt-get -y upgrade && \
|
||||
apt-get -y install postgresql-client-14 postgresql-client-common && \
|
||||
apt-get clean all && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
ENV POETRY_NO_INTERACTION=1 \
|
||||
|
||||
+26
-12
@@ -6,30 +6,44 @@ 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 (recommended): Poetry
|
||||
## 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)
|
||||
|
||||
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
|
||||
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"
|
||||
# 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"
|
||||
```
|
||||
|
||||
### install LNbits
|
||||
@@ -81,7 +95,7 @@ chmod +x lnbits.sh &&
|
||||
|
||||
Now visit `0.0.0.0:5000` to make a super-user account.
|
||||
|
||||
`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).
|
||||
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `poetry run lnbits` (see previous option).
|
||||
|
||||
## Option 3: Nix
|
||||
|
||||
@@ -226,7 +240,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.9-dev gcc build-essential
|
||||
sudo apt install python3.10-dev gcc build-essential
|
||||
|
||||
# if the secp256k1 build fails:
|
||||
# if you used poetry
|
||||
|
||||
Generated
+3
-3
@@ -41,11 +41,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1723938990,
|
||||
"narHash": "sha256-9tUadhnZQbWIiYVXH8ncfGXGvkNq3Hag4RCBEMUk7MI=",
|
||||
"lastModified": 1735563628,
|
||||
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c42fcfbdfeae23e68fc520f9182dde9f38ad1890",
|
||||
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -34,6 +34,16 @@
|
||||
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
|
||||
];
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -42,9 +42,6 @@ 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
|
||||
|
||||
+39
-16
@@ -24,14 +24,17 @@ 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.tasks import ( # watchdog_task
|
||||
from lnbits.core.services.notifications import enqueue_notification
|
||||
from lnbits.core.tasks import (
|
||||
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
|
||||
@@ -78,6 +81,7 @@ 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
|
||||
@@ -102,9 +106,24 @@ 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
|
||||
@@ -144,9 +163,13 @@ def create_app() -> FastAPI:
|
||||
core_app_extra.register_new_ratelimiter = register_new_ratelimiter(app)
|
||||
|
||||
# register static files
|
||||
static_path = Path("lnbits", "static")
|
||||
static = StaticFiles(directory=static_path)
|
||||
app.mount("/static", static, name="static")
|
||||
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",
|
||||
)
|
||||
|
||||
g().base_url = f"http://{settings.host}:{settings.port}"
|
||||
|
||||
@@ -265,14 +288,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_all_extensions_ids = {e.id for e in installed_extensions}
|
||||
settings.lnbits_installed_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_all_extensions_ids:
|
||||
if ext_id in settings.lnbits_installed_extensions_ids:
|
||||
continue
|
||||
ext_info = InstallableExtension.from_ext_dir(ext_id)
|
||||
if not ext_info:
|
||||
@@ -287,7 +310,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_all_extensions_ids:
|
||||
if ext_id in settings.lnbits_installed_extensions_ids:
|
||||
continue
|
||||
|
||||
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
|
||||
@@ -348,6 +371,11 @@ 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")
|
||||
@@ -366,11 +394,6 @@ 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.
|
||||
@@ -444,6 +467,8 @@ 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)
|
||||
@@ -454,9 +479,7 @@ def register_async_tasks():
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
|
||||
# TODO: implement watchdog properly
|
||||
# create_permanent_task(watchdog_task)
|
||||
create_permanent_task(killswitch_task)
|
||||
create_permanent_task(run_by_the_minute_tasks)
|
||||
create_permanent_task(purge_audit_data)
|
||||
create_permanent_task(collect_exchange_rates_data)
|
||||
|
||||
|
||||
+38
-15
@@ -5,6 +5,7 @@ import time
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
import click
|
||||
import httpx
|
||||
@@ -27,13 +28,13 @@ from lnbits.core.crud import (
|
||||
update_payment,
|
||||
)
|
||||
from lnbits.core.helpers import is_valid_url, migrate_databases
|
||||
from lnbits.core.models import Payment, PaymentState
|
||||
from lnbits.core.models import Account, Payment, PaymentState
|
||||
from lnbits.core.models.extensions import (
|
||||
CreateExtension,
|
||||
ExtensionRelease,
|
||||
InstallableExtension,
|
||||
)
|
||||
from lnbits.core.services import check_admin_settings
|
||||
from lnbits.core.services import check_admin_settings, create_user_account_no_ckeck
|
||||
from lnbits.core.views.extension_api import (
|
||||
api_install_extension,
|
||||
api_uninstall_extension,
|
||||
@@ -64,6 +65,13 @@ def db():
|
||||
"""
|
||||
|
||||
|
||||
@lnbits_cli.group()
|
||||
def users():
|
||||
"""
|
||||
Users related commands
|
||||
"""
|
||||
|
||||
|
||||
@lnbits_cli.group()
|
||||
def extensions():
|
||||
"""
|
||||
@@ -109,7 +117,7 @@ async def delete_settings():
|
||||
"""Deletes the settings"""
|
||||
|
||||
async with core_db.connect() as conn:
|
||||
await conn.execute("DELETE from settings")
|
||||
await conn.execute("DELETE from system_settings")
|
||||
|
||||
|
||||
@db.command("migrate")
|
||||
@@ -180,17 +188,6 @@ 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.")
|
||||
@@ -271,6 +268,32 @@ 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():
|
||||
@@ -339,7 +362,7 @@ async def extensions_update(
|
||||
if not await _can_run_operation(url):
|
||||
return
|
||||
|
||||
upgrades_dir = Path(settings.lnbits_extensions_path, "upgrades")
|
||||
upgrades_dir = settings.lnbits_extensions_upgrade_path
|
||||
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
|
||||
sys.path.append(str(upgrades_dir))
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ from .users import (
|
||||
get_account_by_username_or_email,
|
||||
get_accounts,
|
||||
get_user,
|
||||
get_user_access_control_lists,
|
||||
get_user_from_account,
|
||||
update_account,
|
||||
)
|
||||
@@ -145,6 +146,7 @@ __all__ = [
|
||||
"get_accounts",
|
||||
"get_user",
|
||||
"get_user_from_account",
|
||||
"get_user_access_control_lists",
|
||||
"update_account",
|
||||
# wallets
|
||||
"create_wallet",
|
||||
|
||||
+142
-20
@@ -1,25 +1,23 @@
|
||||
from time import time
|
||||
from typing import Literal, Optional
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
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 DB_TYPE, SQLITE, Connection, Filters, Page
|
||||
from lnbits.db import Connection, DateTrunc, 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
|
||||
@@ -87,7 +85,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 {limit}
|
||||
ORDER BY time DESC LIMIT {int(limit)}
|
||||
""",
|
||||
{"ext_name": f"%{ext_name}%", "ext_id": f"%{ext_id}%"},
|
||||
Payment,
|
||||
@@ -99,6 +97,7 @@ async def get_payments_paginated(
|
||||
wallet_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 +106,11 @@ async def get_payments_paginated(
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Page[Payment]:
|
||||
"""
|
||||
Filters payments to be returned by complete | pending | outgoing | incoming.
|
||||
Filters payments to be returned by:
|
||||
- complete | pending | failed | outgoing | incoming.
|
||||
"""
|
||||
|
||||
values: dict = {
|
||||
"wallet_id": wallet_id,
|
||||
values: dict[str, Any] = {
|
||||
"time": since,
|
||||
}
|
||||
clause: list[str] = []
|
||||
@@ -120,6 +119,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")
|
||||
|
||||
if complete and pending:
|
||||
@@ -137,6 +137,8 @@ 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
|
||||
@@ -200,6 +202,21 @@ 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:
|
||||
@@ -234,6 +251,7 @@ 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,
|
||||
@@ -247,7 +265,8 @@ async def create_payment(
|
||||
expiry=data.expiry,
|
||||
webhook=data.webhook,
|
||||
fee=data.fee,
|
||||
extra=data.extra or {},
|
||||
tag=extra.get("tag", None),
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
await (conn or db).insert("apipayments", payment)
|
||||
@@ -284,12 +303,7 @@ async def get_payments_history(
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
|
||||
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}")
|
||||
date_trunc = db.datetime_grouping(group)
|
||||
|
||||
values = {
|
||||
"wallet_id": wallet_id,
|
||||
@@ -341,6 +355,114 @@ 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:
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 (
|
||||
@@ -93,6 +94,8 @@ 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},
|
||||
@@ -122,6 +125,8 @@ 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},
|
||||
@@ -142,6 +147,8 @@ 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},
|
||||
@@ -185,3 +192,20 @@ 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)
|
||||
|
||||
@@ -23,6 +23,7 @@ async def create_wallet(
|
||||
user=user_id,
|
||||
adminkey=uuid4().hex,
|
||||
inkey=uuid4().hex,
|
||||
currency=settings.lnbits_default_accounting_currency or "USD",
|
||||
)
|
||||
await (conn or db).insert("wallets", wallet)
|
||||
return wallet
|
||||
@@ -134,6 +135,12 @@ async def get_wallets(
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
@@ -152,6 +159,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) FROM balances")
|
||||
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
||||
row = result.mappings().first()
|
||||
return row.get("balance", 0)
|
||||
return row.get("balance", 0) or 0
|
||||
|
||||
+48
-21
@@ -602,34 +602,46 @@ 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")
|
||||
result = await db.execute("SELECT * FROM apipayments LIMIT 100")
|
||||
except Exception as exc:
|
||||
logger.warning("Could not select, trying again after cache cleared.")
|
||||
logger.debug(exc)
|
||||
await db.execute("COMMIT")
|
||||
|
||||
result = await db.execute("SELECT * FROM apipayments")
|
||||
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}")
|
||||
|
||||
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"),
|
||||
},
|
||||
result = await db.execute(
|
||||
f"SELECT * FROM apipayments ORDER BY time LIMIT {limit} OFFSET {offset}"
|
||||
)
|
||||
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):
|
||||
@@ -684,3 +696,18 @@ 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")
|
||||
|
||||
@@ -14,10 +14,15 @@ from .payments import (
|
||||
DecodePayment,
|
||||
PayInvoice,
|
||||
Payment,
|
||||
PaymentCountField,
|
||||
PaymentCountStat,
|
||||
PaymentDailyStats,
|
||||
PaymentExtra,
|
||||
PaymentFilters,
|
||||
PaymentHistoryPoint,
|
||||
PaymentsStatusCount,
|
||||
PaymentState,
|
||||
PaymentWalletStats,
|
||||
)
|
||||
from .tinyurl import TinyURL
|
||||
from .users import (
|
||||
@@ -36,6 +41,7 @@ from .users import (
|
||||
UpdateUserPassword,
|
||||
UpdateUserPubkey,
|
||||
User,
|
||||
UserAcls,
|
||||
UserExtra,
|
||||
)
|
||||
from .wallets import BaseWallet, CreateWallet, KeyType, Wallet, WalletTypeInfo
|
||||
@@ -62,6 +68,11 @@ __all__ = [
|
||||
"DecodePayment",
|
||||
"PayInvoice",
|
||||
"Payment",
|
||||
"PaymentCountField",
|
||||
"PaymentCountStat",
|
||||
"PaymentDailyStats",
|
||||
"PaymentsStatusCount",
|
||||
"PaymentWalletStats",
|
||||
"PaymentExtra",
|
||||
"PaymentFilters",
|
||||
"PaymentHistoryPoint",
|
||||
@@ -73,6 +84,7 @@ __all__ = [
|
||||
"Account",
|
||||
"AccountFilters",
|
||||
"AccountOverview",
|
||||
"UserAcls",
|
||||
"CreateUser",
|
||||
"RegisterUser",
|
||||
"LoginUsernamePassword",
|
||||
|
||||
+15
-16
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -10,16 +9,16 @@ from lnbits.settings import settings
|
||||
|
||||
|
||||
class AuditEntry(BaseModel):
|
||||
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
|
||||
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
|
||||
duration: float
|
||||
delete_at: Optional[datetime] = None
|
||||
delete_at: datetime | None = None
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
def __init__(self, **data):
|
||||
@@ -42,12 +41,12 @@ class AuditFilters(FilterModel):
|
||||
"duration",
|
||||
]
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
class AuditCountStat(BaseModel):
|
||||
|
||||
@@ -7,7 +7,7 @@ import os
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -29,17 +29,17 @@ class ExplicitRelease(BaseModel):
|
||||
archive: str
|
||||
hash: str
|
||||
dependencies: list[str] = []
|
||||
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]
|
||||
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
|
||||
|
||||
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: Optional[str] = ""
|
||||
min_lnbits_version: Optional[str]
|
||||
max_lnbits_version: Optional[str]
|
||||
warning: str | None = ""
|
||||
min_lnbits_version: str | None
|
||||
max_lnbits_version: str | None
|
||||
|
||||
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
|
||||
) -> Optional[ExtensionConfig]:
|
||||
) -> ExtensionConfig | None:
|
||||
config_url = (
|
||||
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
|
||||
)
|
||||
@@ -97,28 +97,28 @@ class ExtensionConfig(BaseModel):
|
||||
|
||||
|
||||
class ReleasePaymentInfo(BaseModel):
|
||||
amount: Optional[int] = None
|
||||
pay_link: Optional[str] = None
|
||||
payment_hash: Optional[str] = None
|
||||
payment_request: Optional[str] = None
|
||||
amount: int | None = None
|
||||
pay_link: str | None = None
|
||||
payment_hash: str | None = None
|
||||
payment_request: str | None = None
|
||||
|
||||
|
||||
class PayToEnableInfo(BaseModel):
|
||||
amount: int
|
||||
amount: int = 0
|
||||
required: bool = False
|
||||
wallet: Optional[str] = None
|
||||
wallet: str | None = None
|
||||
|
||||
|
||||
class UserExtensionInfo(BaseModel):
|
||||
paid_to_enable: Optional[bool] = False
|
||||
payment_hash_to_enable: Optional[str] = None
|
||||
paid_to_enable: bool | None = False
|
||||
payment_hash_to_enable: str | None = None
|
||||
|
||||
|
||||
class UserExtension(BaseModel):
|
||||
user: str
|
||||
extension: str
|
||||
active: bool
|
||||
extra: Optional[UserExtensionInfo] = None
|
||||
extra: UserExtensionInfo | None = None
|
||||
|
||||
@property
|
||||
def is_paid(self) -> bool:
|
||||
@@ -140,16 +140,14 @@ class UserExtension(BaseModel):
|
||||
class Extension(BaseModel):
|
||||
code: str
|
||||
is_valid: bool
|
||||
name: Optional[str] = None
|
||||
short_description: Optional[str] = None
|
||||
tile: Optional[str] = None
|
||||
upgrade_hash: Optional[str] = ""
|
||||
name: str | None = None
|
||||
short_description: str | None = None
|
||||
tile: str | None = None
|
||||
upgrade_hash: str | None = ""
|
||||
|
||||
@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:
|
||||
@@ -178,21 +176,21 @@ class ExtensionRelease(BaseModel):
|
||||
archive: str
|
||||
source_repo: str
|
||||
is_github_release: bool = False
|
||||
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
|
||||
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
|
||||
|
||||
pay_link: Optional[str] = None
|
||||
cost_sats: Optional[int] = None
|
||||
paid_sats: Optional[int] = 0
|
||||
payment_hash: Optional[str] = None
|
||||
pay_link: str | None = None
|
||||
cost_sats: int | None = None
|
||||
paid_sats: int | None = 0
|
||||
payment_hash: str | None = None
|
||||
|
||||
@property
|
||||
def archive_url(self) -> str:
|
||||
@@ -210,8 +208,8 @@ class ExtensionRelease(BaseModel):
|
||||
self.cost_sats = payment_info.amount if payment_info else None
|
||||
|
||||
async def fetch_release_payment_info(
|
||||
self, amount: Optional[int] = None
|
||||
) -> Optional[ReleasePaymentInfo]:
|
||||
self, amount: int | None = None
|
||||
) -> ReleasePaymentInfo | None:
|
||||
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
|
||||
assert url, "Missing URL for payment info."
|
||||
try:
|
||||
@@ -283,7 +281,7 @@ class ExtensionRelease(BaseModel):
|
||||
return [GitHubRepoRelease.parse_obj(r) for r in releases]
|
||||
|
||||
@classmethod
|
||||
async def fetch_release_details(cls, details_link: str) -> Optional[dict]:
|
||||
async def fetch_release_details(cls, details_link: str) -> dict | None:
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
@@ -302,12 +300,12 @@ class ExtensionRelease(BaseModel):
|
||||
|
||||
|
||||
class ExtensionMeta(BaseModel):
|
||||
installed_release: Optional[ExtensionRelease] = None
|
||||
latest_release: Optional[ExtensionRelease] = None
|
||||
pay_to_enable: Optional[PayToEnableInfo] = None
|
||||
installed_release: ExtensionRelease | None = None
|
||||
latest_release: ExtensionRelease | None = None
|
||||
pay_to_enable: PayToEnableInfo | None = None
|
||||
payments: list[ReleasePaymentInfo] = []
|
||||
dependencies: list[str] = []
|
||||
archive: Optional[str] = None
|
||||
archive: str | None = None
|
||||
featured: bool = False
|
||||
|
||||
|
||||
@@ -315,11 +313,11 @@ class InstallableExtension(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
version: str
|
||||
active: Optional[bool] = False
|
||||
short_description: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
active: bool | None = False
|
||||
short_description: str | None = None
|
||||
icon: str | None = None
|
||||
stars: int = 0
|
||||
meta: Optional[ExtensionMeta] = None
|
||||
meta: ExtensionMeta | None = None
|
||||
|
||||
@property
|
||||
def hash(self) -> str:
|
||||
@@ -343,15 +341,11 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
@property
|
||||
def ext_upgrade_dir(self) -> Path:
|
||||
return Path(
|
||||
settings.lnbits_extensions_path, "upgrades", f"{self.id}-{self.hash}"
|
||||
)
|
||||
return Path(settings.lnbits_extensions_upgrade_path, 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:
|
||||
@@ -411,9 +405,7 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
def extract_archive(self):
|
||||
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
|
||||
Path(settings.lnbits_extensions_path, "upgrades").mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
Path(settings.lnbits_extensions_upgrade_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tmp_dir = Path(settings.lnbits_data_folder, "unzip-temp", self.hash)
|
||||
shutil.rmtree(tmp_dir, True)
|
||||
@@ -460,7 +452,7 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
shutil.rmtree(self.ext_upgrade_dir, True)
|
||||
|
||||
def check_latest_version(self, release: Optional[ExtensionRelease]):
|
||||
def check_latest_version(self, release: ExtensionRelease | None):
|
||||
if not release:
|
||||
return
|
||||
if not self.meta or not self.meta.latest_release:
|
||||
@@ -473,9 +465,7 @@ class InstallableExtension(BaseModel):
|
||||
):
|
||||
self.meta.latest_release = release
|
||||
|
||||
def find_existing_payment(
|
||||
self, pay_link: Optional[str]
|
||||
) -> Optional[ReleasePaymentInfo]:
|
||||
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
|
||||
if not pay_link or not self.meta or not self.meta.payments:
|
||||
return None
|
||||
return next(
|
||||
@@ -515,7 +505,7 @@ class InstallableExtension(BaseModel):
|
||||
@classmethod
|
||||
async def from_github_release(
|
||||
cls, github_release: GitHubRelease
|
||||
) -> Optional[InstallableExtension]:
|
||||
) -> InstallableExtension | None:
|
||||
try:
|
||||
repo, latest_release, config = await cls.fetch_github_repo_info(
|
||||
github_release.organisation, github_release.repository
|
||||
@@ -554,7 +544,7 @@ class InstallableExtension(BaseModel):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_ext_dir(cls, ext_id: str) -> Optional[InstallableExtension]:
|
||||
def from_ext_dir(cls, ext_id: str) -> InstallableExtension | None:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
@@ -594,7 +584,6 @@ class InstallableExtension(BaseModel):
|
||||
cls,
|
||||
) -> list[InstallableExtension]:
|
||||
extension_list: list[InstallableExtension] = []
|
||||
extension_id_list: list[str] = []
|
||||
|
||||
for url in settings.lnbits_extensions_manifests:
|
||||
try:
|
||||
@@ -615,7 +604,6 @@ 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)
|
||||
@@ -631,10 +619,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
|
||||
@@ -667,7 +655,7 @@ class InstallableExtension(BaseModel):
|
||||
@classmethod
|
||||
async def get_extension_release(
|
||||
cls, ext_id: str, source_repo: str, archive: str, version: str
|
||||
) -> Optional[ExtensionRelease]:
|
||||
) -> ExtensionRelease | None:
|
||||
all_releases: list[ExtensionRelease] = (
|
||||
await InstallableExtension.get_extension_releases(ext_id)
|
||||
)
|
||||
@@ -718,8 +706,8 @@ class CreateExtension(BaseModel):
|
||||
archive: str
|
||||
source_repo: str
|
||||
version: str
|
||||
cost_sats: Optional[int] = 0
|
||||
payment_hash: Optional[str] = None
|
||||
cost_sats: int | None = 0
|
||||
payment_hash: str | None = None
|
||||
|
||||
|
||||
class ExtensionDetailsRequest(BaseModel):
|
||||
@@ -728,7 +716,7 @@ class ExtensionDetailsRequest(BaseModel):
|
||||
version: str
|
||||
|
||||
|
||||
async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
|
||||
async def github_api_get(url: str, error_msg: str | None) -> Any:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
if settings.lnbits_ext_github_token:
|
||||
headers["Authorization"] = f"Bearer {settings.lnbits_ext_github_token}"
|
||||
@@ -740,7 +728,7 @@ async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
|
||||
return resp.json()
|
||||
|
||||
|
||||
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
|
||||
def icon_to_github_url(source_repo: str, path: str | None) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
_, _, *rest = path.split("/")
|
||||
|
||||
@@ -25,12 +25,12 @@ class Callback(BaseModel):
|
||||
|
||||
|
||||
class BalanceDelta(BaseModel):
|
||||
lnbits_balance_msats: int
|
||||
node_balance_msats: int
|
||||
lnbits_balance_sats: int
|
||||
node_balance_sats: int
|
||||
|
||||
@property
|
||||
def delta_msats(self):
|
||||
return self.node_balance_msats - self.lnbits_balance_msats
|
||||
def delta_sats(self) -> int:
|
||||
return int(self.lnbits_balance_sats - self.node_balance_sats)
|
||||
|
||||
|
||||
class SimpleStatus(BaseModel):
|
||||
@@ -38,6 +38,16 @@ 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"
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
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}).
|
||||
""",
|
||||
}
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, Field, validator
|
||||
@@ -28,16 +28,16 @@ class PaymentState(str, Enum):
|
||||
|
||||
|
||||
class PaymentExtra(BaseModel):
|
||||
comment: Optional[str] = None
|
||||
success_action: Optional[str] = None
|
||||
lnurl_response: Optional[str] = None
|
||||
comment: str | None = None
|
||||
success_action: str | None = None
|
||||
lnurl_response: str | None = None
|
||||
|
||||
|
||||
class PayInvoice(BaseModel):
|
||||
payment_request: str
|
||||
description: Optional[str] = None
|
||||
max_sat: Optional[int] = None
|
||||
extra: Optional[dict] = {}
|
||||
description: str | None = None
|
||||
max_sat: int | None = None
|
||||
extra: dict | None = {}
|
||||
|
||||
|
||||
class CreatePayment(BaseModel):
|
||||
@@ -46,10 +46,10 @@ class CreatePayment(BaseModel):
|
||||
bolt11: str
|
||||
amount_msat: int
|
||||
memo: str
|
||||
extra: Optional[dict] = {}
|
||||
preimage: Optional[str] = None
|
||||
expiry: Optional[datetime] = None
|
||||
webhook: Optional[str] = None
|
||||
extra: dict | None = {}
|
||||
preimage: str | None = None
|
||||
expiry: datetime | None = None
|
||||
webhook: str | None = None
|
||||
fee: int = 0
|
||||
|
||||
|
||||
@@ -61,13 +61,13 @@ class Payment(BaseModel):
|
||||
fee: int
|
||||
bolt11: str
|
||||
status: str = PaymentState.PENDING
|
||||
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
|
||||
memo: str | None = None
|
||||
expiry: datetime | None = None
|
||||
webhook: str | None = None
|
||||
webhook_status: int | None = None
|
||||
preimage: str | None = None
|
||||
tag: str | None = None
|
||||
extension: str | None = 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,22 +125,60 @@ class Payment(BaseModel):
|
||||
|
||||
|
||||
class PaymentFilters(FilterModel):
|
||||
__search_fields__ = ["memo", "amount"]
|
||||
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
|
||||
|
||||
status: str
|
||||
checking_id: str
|
||||
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
||||
|
||||
status: str | None
|
||||
tag: str | None
|
||||
checking_id: str | None
|
||||
amount: int
|
||||
fee: int
|
||||
memo: Optional[str]
|
||||
memo: str | None
|
||||
time: datetime
|
||||
bolt11: str
|
||||
preimage: str
|
||||
payment_hash: str
|
||||
expiry: Optional[datetime]
|
||||
extra: dict = {}
|
||||
wallet_id: str
|
||||
webhook: Optional[str]
|
||||
webhook_status: Optional[int]
|
||||
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
|
||||
|
||||
|
||||
class PaymentHistoryPoint(BaseModel):
|
||||
@@ -152,7 +190,7 @@ class PaymentHistoryPoint(BaseModel):
|
||||
|
||||
class DecodePayment(BaseModel):
|
||||
data: str
|
||||
filter_fields: Optional[list[str]] = []
|
||||
filter_fields: list[str] | None = []
|
||||
|
||||
|
||||
class CreateInvoice(BaseModel):
|
||||
@@ -160,14 +198,14 @@ class CreateInvoice(BaseModel):
|
||||
internal: bool = False
|
||||
out: bool = True
|
||||
amount: float = Query(None, ge=0)
|
||||
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
|
||||
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
|
||||
|
||||
@validator("unit")
|
||||
@classmethod
|
||||
@@ -175,3 +213,10 @@ 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
|
||||
|
||||
+128
-39
@@ -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,25 +16,86 @@ from .wallets import Wallet
|
||||
|
||||
|
||||
class UserExtra(BaseModel):
|
||||
email_verified: Optional[bool] = False
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
display_name: Optional[str] = None
|
||||
picture: Optional[str] = None
|
||||
email_verified: bool | None = False
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
display_name: str | None = None
|
||||
picture: str | None = 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: Optional[str] = "lnbits" # auth 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
|
||||
|
||||
|
||||
class Account(BaseModel):
|
||||
id: str
|
||||
username: Optional[str] = None
|
||||
password_hash: Optional[str] = None
|
||||
pubkey: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
username: str | None = None
|
||||
password_hash: str | None = None
|
||||
pubkey: str | None = None
|
||||
email: str | None = 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))
|
||||
|
||||
@@ -72,10 +133,10 @@ class Account(BaseModel):
|
||||
|
||||
|
||||
class AccountOverview(Account):
|
||||
transaction_count: Optional[int] = 0
|
||||
wallet_count: Optional[int] = 0
|
||||
balance_msat: Optional[int] = 0
|
||||
last_payment: Optional[datetime] = None
|
||||
transaction_count: int | None = 0
|
||||
wallet_count: int | None = 0
|
||||
balance_msat: int | None = 0
|
||||
last_payment: datetime | None = None
|
||||
|
||||
|
||||
class AccountFilters(FilterModel):
|
||||
@@ -89,20 +150,20 @@ class AccountFilters(FilterModel):
|
||||
"last_payment",
|
||||
]
|
||||
|
||||
email: Optional[str] = None
|
||||
user: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
pubkey: Optional[str] = None
|
||||
wallet_id: Optional[str] = None
|
||||
email: str | None = None
|
||||
user: str | None = None
|
||||
username: str | None = None
|
||||
pubkey: str | None = None
|
||||
wallet_id: str | None = None
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
email: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
pubkey: Optional[str] = None
|
||||
email: str | None = None
|
||||
username: str | None = None
|
||||
pubkey: str | None = None
|
||||
extensions: list[str] = []
|
||||
wallets: list[Wallet] = []
|
||||
admin: bool = False
|
||||
@@ -114,7 +175,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) -> Optional[Wallet]:
|
||||
def get_wallet(self, wallet_id: str) -> Wallet | None:
|
||||
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
||||
return w[0] if w else None
|
||||
|
||||
@@ -130,33 +191,33 @@ class User(BaseModel):
|
||||
|
||||
|
||||
class RegisterUser(BaseModel):
|
||||
email: Optional[str] = Query(default=None)
|
||||
email: str | None = 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: 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)
|
||||
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)
|
||||
pubkey: str = Query(default=None, max_length=64)
|
||||
extensions: Optional[list[str]] = None
|
||||
extra: Optional[UserExtra] = None
|
||||
extensions: list[str] | None = None
|
||||
extra: UserExtra | None = None
|
||||
|
||||
|
||||
class UpdateUser(BaseModel):
|
||||
user_id: str
|
||||
email: Optional[str] = Query(default=None)
|
||||
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
|
||||
extra: Optional[UserExtra] = None
|
||||
email: str | None = Query(default=None)
|
||||
username: str | None = Query(default=..., min_length=2, max_length=20)
|
||||
extra: UserExtra | None = None
|
||||
|
||||
|
||||
class UpdateUserPassword(BaseModel):
|
||||
user_id: str
|
||||
password_old: Optional[str] = None
|
||||
password_old: str | None = 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)
|
||||
@@ -190,11 +251,39 @@ class LoginUsernamePassword(BaseModel):
|
||||
|
||||
class AccessTokenPayload(BaseModel):
|
||||
sub: str
|
||||
usr: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
auth_time: Optional[int] = 0
|
||||
usr: str | None = None
|
||||
email: str | None = None
|
||||
auth_time: int | None = 0
|
||||
api_token_id: str | None = None
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
@@ -23,6 +22,11 @@ class BaseWallet(BaseModel):
|
||||
balance_msat: int
|
||||
|
||||
|
||||
class WalletExtra(BaseModel):
|
||||
icon: str = "flash_on"
|
||||
color: str = "primary"
|
||||
|
||||
|
||||
class Wallet(BaseModel):
|
||||
id: str
|
||||
user: str
|
||||
@@ -32,8 +36,9 @@ 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: Optional[str] = None
|
||||
currency: str | None = None
|
||||
balance_msat: int = Field(default=0, no_database=True)
|
||||
extra: WalletExtra = WalletExtra()
|
||||
|
||||
@property
|
||||
def balance(self) -> int:
|
||||
@@ -61,7 +66,7 @@ class Wallet(BaseModel):
|
||||
|
||||
|
||||
class CreateWallet(BaseModel):
|
||||
name: Optional[str] = None
|
||||
name: str | None = None
|
||||
|
||||
|
||||
class KeyType(Enum):
|
||||
|
||||
@@ -3,6 +3,7 @@ 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,
|
||||
@@ -11,7 +12,6 @@ from .payments import (
|
||||
fee_reserve,
|
||||
fee_reserve_total,
|
||||
pay_invoice,
|
||||
send_payment_notification,
|
||||
service_fee,
|
||||
update_pending_payments,
|
||||
update_wallet_balance,
|
||||
@@ -24,7 +24,6 @@ from .users import (
|
||||
check_admin_settings,
|
||||
create_user_account,
|
||||
create_user_account_no_ckeck,
|
||||
init_admin_settings,
|
||||
update_user_account,
|
||||
update_user_extensions,
|
||||
)
|
||||
@@ -37,6 +36,9 @@ __all__ = [
|
||||
# lnurl
|
||||
"redeem_lnurl_withdraw",
|
||||
"perform_lnurlauth",
|
||||
# notifications
|
||||
"enqueue_notification",
|
||||
"send_payment_notification",
|
||||
# payments
|
||||
"calculate_fiat_amounts",
|
||||
"check_transaction_status",
|
||||
@@ -45,7 +47,6 @@ __all__ = [
|
||||
"fee_reserve",
|
||||
"fee_reserve_total",
|
||||
"pay_invoice",
|
||||
"send_payment_notification",
|
||||
"service_fee",
|
||||
"update_pending_payments",
|
||||
"update_wallet_balance",
|
||||
@@ -56,7 +57,6 @@ __all__ = [
|
||||
"check_admin_settings",
|
||||
"create_user_account",
|
||||
"create_user_account_no_ckeck",
|
||||
"init_admin_settings",
|
||||
"update_user_account",
|
||||
"update_user_extensions",
|
||||
# websockets
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -18,6 +22,62 @@ async def get_balance_delta() -> BalanceDelta:
|
||||
status = await funding_source.status()
|
||||
lnbits_balance = await get_total_balance()
|
||||
return BalanceDelta(
|
||||
lnbits_balance_msats=lnbits_balance,
|
||||
node_balance_msats=status.balance_msat,
|
||||
lnbits_balance_sats=int(lnbits_balance) // 1000,
|
||||
node_balance_sats=status.balance_msat // 1000,
|
||||
)
|
||||
|
||||
|
||||
async def check_server_balance_against_node():
|
||||
"""
|
||||
Watchdog will check lnbits balance and nodebalance
|
||||
and will switch to VoidWallet if the watchdog delta is reached.
|
||||
"""
|
||||
if (
|
||||
not settings.lnbits_watchdog_switch_to_voidwallet
|
||||
and not settings.lnbits_notification_watchdog
|
||||
):
|
||||
return
|
||||
|
||||
funding_source = get_funding_source()
|
||||
if funding_source.__class__.__name__ == "VoidWallet":
|
||||
return
|
||||
|
||||
status = await get_balance_delta()
|
||||
if status.delta_sats < settings.lnbits_watchdog_delta:
|
||||
return
|
||||
|
||||
use_voidwallet = settings.lnbits_watchdog_switch_to_voidwallet
|
||||
logger.warning(
|
||||
f"Balance delta reached: {status.delta_sats} sats."
|
||||
f" Switch to void wallet: {use_voidwallet}."
|
||||
)
|
||||
enqueue_notification(
|
||||
NotificationType.watchdog_check,
|
||||
{
|
||||
"delta_sats": status.delta_sats,
|
||||
"lnbits_balance_sats": status.lnbits_balance_sats,
|
||||
"node_balance_sats": status.node_balance_sats,
|
||||
"switch_to_void_wallet": use_voidwallet,
|
||||
},
|
||||
)
|
||||
if use_voidwallet:
|
||||
logger.error(f"Switching to VoidWallet. Delta: {status.delta_sats} sats.")
|
||||
await switch_to_voidwallet()
|
||||
|
||||
|
||||
async def check_balance_delta_changed():
|
||||
status = await get_balance_delta()
|
||||
if settings.latest_balance_delta_sats is None:
|
||||
settings.latest_balance_delta_sats = status.delta_sats
|
||||
return
|
||||
if status.delta_sats != settings.latest_balance_delta_sats:
|
||||
enqueue_notification(
|
||||
NotificationType.balance_delta,
|
||||
{
|
||||
"delta_sats": status.delta_sats,
|
||||
"old_delta_sats": settings.latest_balance_delta_sats,
|
||||
"lnbits_balance_sats": status.lnbits_balance_sats,
|
||||
"node_balance_sats": status.node_balance_sats,
|
||||
},
|
||||
)
|
||||
settings.latest_balance_delta_sats = status.delta_sats
|
||||
|
||||
@@ -13,7 +13,7 @@ from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
require_admin_key,
|
||||
)
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.helpers import check_callback_url, url_for
|
||||
from lnbits.lnurl import LnurlErrorResponse
|
||||
from lnbits.lnurl import decode as decode_lnurl
|
||||
from lnbits.settings import settings
|
||||
@@ -37,6 +37,7 @@ 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()
|
||||
|
||||
@@ -72,6 +73,7 @@ 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
|
||||
@@ -135,6 +137,7 @@ 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={
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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
|
||||
@@ -0,0 +1,330 @@
|
||||
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:
|
||||
data = payment.dict()
|
||||
try:
|
||||
check_callback_url(payment.webhook)
|
||||
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_notification(wallet: Wallet, payment: Payment):
|
||||
try:
|
||||
await send_ws_payment_notification(wallet, payment)
|
||||
except Exception as e:
|
||||
logger.error("Error sending websocket payment notification", e)
|
||||
try:
|
||||
send_chat_payment_notification(wallet, payment)
|
||||
except Exception as e:
|
||||
logger.error("Error sending chat payment notification", e)
|
||||
try:
|
||||
await send_payment_push_notification(wallet, payment)
|
||||
except Exception as e:
|
||||
logger.error("Error sending push payment notification", e)
|
||||
|
||||
if payment.webhook and not payment.webhook_status:
|
||||
await dispatch_webhook(payment)
|
||||
|
||||
|
||||
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}"
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from bolt11 import Bolt11, MilliSatoshi, Tags
|
||||
@@ -7,11 +8,14 @@ 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.db import Connection
|
||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
||||
from lnbits.db import Connection, Filters
|
||||
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
|
||||
@@ -38,7 +42,7 @@ from ..models import (
|
||||
PaymentState,
|
||||
Wallet,
|
||||
)
|
||||
from .websockets import websocket_manager
|
||||
from .notifications import send_payment_notification
|
||||
|
||||
|
||||
async def pay_invoice(
|
||||
@@ -51,14 +55,16 @@ 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 conn:
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||
amount_msat = invoice.amount_msat
|
||||
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, conn)
|
||||
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
|
||||
|
||||
if await is_internal_status_success(invoice.payment_hash, conn):
|
||||
if await is_internal_status_success(invoice.payment_hash, new_conn):
|
||||
raise PaymentError("Internal invoice already paid.", status="failed")
|
||||
|
||||
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
|
||||
@@ -73,8 +79,10 @@ async def pay_invoice(
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
payment = await _pay_invoice(wallet, create_payment_model, conn)
|
||||
await _credit_service_fee_wallet(payment, conn)
|
||||
payment = await _pay_invoice(wallet, create_payment_model, conn)
|
||||
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||
await _credit_service_fee_wallet(payment, new_conn)
|
||||
|
||||
return payment
|
||||
|
||||
@@ -100,7 +108,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
|
||||
invoice_memo = None if description_hash else memo[:640]
|
||||
|
||||
# use the fake wallet if the invoice is for internal use only
|
||||
funding_source = fake_wallet if internal else get_funding_source()
|
||||
@@ -109,6 +117,12 @@ 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
|
||||
):
|
||||
@@ -164,13 +178,20 @@ async def update_pending_payments(wallet_id: str):
|
||||
exclude_uncheckable=True,
|
||||
)
|
||||
for payment in pending_payments:
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
||||
@@ -261,27 +282,6 @@ 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
|
||||
):
|
||||
@@ -354,17 +354,22 @@ 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:
|
||||
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
|
||||
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}")
|
||||
|
||||
logger.debug(
|
||||
f"Calculated fiat amounts {wallet.id=} {amount=} {currency=}: {fiat_amounts=}"
|
||||
@@ -388,6 +393,50 @@ 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] = []
|
||||
|
||||
start_date = min(data_in[0].date, data_out[0].date)
|
||||
end_date = max(data_in[-1].date, data_out[-1].date)
|
||||
delta = timedelta(days=1)
|
||||
while start_date <= end_date:
|
||||
|
||||
data_in_point = next((x for x in data_in if x.date == start_date), _none)
|
||||
data_out_point = next((x for x in data_out if x.date == start_date), _none)
|
||||
|
||||
balance_total += data_in_point.balance + data_out_point.balance
|
||||
data.append(
|
||||
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, create_payment_model, conn):
|
||||
payment = await _pay_internal_invoice(wallet, create_payment_model, conn)
|
||||
if not payment:
|
||||
@@ -491,13 +540,19 @@ async def _pay_external_invoice(
|
||||
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
|
||||
service_fee_msat = service_fee(amount_msat, internal=False)
|
||||
|
||||
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
|
||||
task = create_task(
|
||||
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
|
||||
)
|
||||
logger.debug(f"backend: pay_invoice finished {checking_id}, {payment_response}")
|
||||
|
||||
# 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
|
||||
|
||||
if payment_response.checking_id and payment_response.checking_id != checking_id:
|
||||
logger.warning(
|
||||
f"backend sent unexpected checking_id (expected: {checking_id} got:"
|
||||
@@ -533,9 +588,22 @@ 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:
|
||||
@@ -595,8 +663,14 @@ def _validate_payment_request(
|
||||
if not invoice.amount_msat or not invoice.amount_msat > 0:
|
||||
raise PaymentError("Amountless invoices not supported.", status="failed")
|
||||
|
||||
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||
raise PaymentError("Amount in invoice is too high.", 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",
|
||||
)
|
||||
|
||||
return invoice
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from py_vapid.utils import b64urlencode
|
||||
from lnbits.db import dict_to_model
|
||||
from lnbits.settings import (
|
||||
EditableSettings,
|
||||
UpdateSettings,
|
||||
readonly_variables,
|
||||
settings,
|
||||
)
|
||||
@@ -37,8 +38,12 @@ 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_model(sets_dict, EditableSettings)
|
||||
editable_settings = dict_to_settings(sets_dict)
|
||||
for key in sets_dict.keys():
|
||||
if key in readonly_variables:
|
||||
continue
|
||||
|
||||
@@ -8,7 +8,6 @@ from lnbits.core.models.extensions import UserExtension
|
||||
from lnbits.settings import (
|
||||
EditableSettings,
|
||||
SuperSettings,
|
||||
send_admin_user_to_saas,
|
||||
settings,
|
||||
)
|
||||
|
||||
@@ -47,7 +46,9 @@ async def create_user_account(
|
||||
|
||||
|
||||
async def create_user_account_no_ckeck(
|
||||
account: Optional[Account] = None, wallet_name: Optional[str] = None
|
||||
account: Optional[Account] = None,
|
||||
wallet_name: Optional[str] = None,
|
||||
default_exts: Optional[list[str]] = None,
|
||||
) -> User:
|
||||
|
||||
if account:
|
||||
@@ -70,9 +71,13 @@ async def create_user_account_no_ckeck(
|
||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||
)
|
||||
|
||||
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_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}")
|
||||
|
||||
user = await get_user_from_account(account)
|
||||
assert user, "Cannot find user for account."
|
||||
@@ -154,14 +159,6 @@ 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
|
||||
|
||||
+91
-112
@@ -1,79 +1,92 @@
|
||||
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.models import AuditEntry, Payment
|
||||
from lnbits.core.services import (
|
||||
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,
|
||||
get_balance_delta,
|
||||
send_payment_notification,
|
||||
switch_to_voidwallet,
|
||||
)
|
||||
from lnbits.settings import get_funding_source, settings
|
||||
from lnbits.tasks import send_push_notification
|
||||
from lnbits.core.services.notifications import (
|
||||
enqueue_notification,
|
||||
process_next_notification,
|
||||
send_payment_notification,
|
||||
)
|
||||
from lnbits.db import Filters
|
||||
from lnbits.settings import settings
|
||||
from lnbits.tasks import create_unique_task
|
||||
from lnbits.utils.exchange_rates import btc_rates
|
||||
|
||||
audit_queue: asyncio.Queue = asyncio.Queue()
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
async def run_by_the_minute_tasks():
|
||||
minute_counter = 0
|
||||
while settings.lnbits_running:
|
||||
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)
|
||||
status_minutes = settings.lnbits_notification_server_status_hours * 60
|
||||
|
||||
|
||||
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"
|
||||
):
|
||||
if settings.notification_balance_delta_changed:
|
||||
try:
|
||||
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)
|
||||
# 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)
|
||||
|
||||
|
||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
@@ -87,61 +100,6 @@ 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():
|
||||
@@ -158,6 +116,16 @@ 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.
|
||||
@@ -194,3 +162,14 @@ 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())
|
||||
|
||||
@@ -15,11 +15,8 @@
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<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-label v-text="$t('enable_audit')"></q-item-label>
|
||||
<q-item-label caption v-text="$t('audit_desc')"></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
@@ -48,14 +45,16 @@
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Record Request Body</q-item-label>
|
||||
<q-item-label caption
|
||||
>Warning:
|
||||
<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 />
|
||||
<ul>
|
||||
<li>confidential data (like passwords) will be logged</li>
|
||||
<li>the request body can have large size.</li>
|
||||
<li><span v-text="$t('audit_record_req_warning_1')"></span></li>
|
||||
<li><span v-text="$t('audit_record_req_warning_2')"></span></li>
|
||||
</ul>
|
||||
Use it with caution.
|
||||
<br />
|
||||
<span v-text="$t('audit_record_use')"></span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
@@ -72,8 +71,12 @@
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Record IP Address</q-item-label>
|
||||
<q-item-label caption>Save the client IP address.</q-item-label>
|
||||
<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-section>
|
||||
</q-item>
|
||||
</div>
|
||||
@@ -89,8 +92,12 @@
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Record Path Parameters</q-item-label>
|
||||
<q-item-label caption>Recommended. </q-item-label>
|
||||
<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-section>
|
||||
</q-item>
|
||||
</div>
|
||||
@@ -106,8 +113,12 @@
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Record Query Parameters</q-item-label>
|
||||
<q-item-label caption>Recommended.</q-item-label>
|
||||
<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-section>
|
||||
</q-item>
|
||||
</div>
|
||||
@@ -115,25 +126,26 @@
|
||||
<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>Include HTTP Methods</p>
|
||||
<p><span v-text="$t('audit_http_methods')"></span></p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_audit_http_methods"
|
||||
multiple
|
||||
hint="List of HTTP methods to be included. Empty lists means all."
|
||||
label="HTTP Methods"
|
||||
:hint="$t('audit_http_methods_hint')"
|
||||
:label="$t('audit_http_methods_label')"
|
||||
:options="['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<p>Include HTTP Response Codes</p>
|
||||
<p><span v-text="$t('audit_resp_codes')"></span></p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formAddIncludeResponseCode"
|
||||
@keydown.enter="addIncludeResponseCode"
|
||||
type="text"
|
||||
label="HTTP Response code (regex)"
|
||||
hint="List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*"
|
||||
:label="$t('audit_resp_codes_label')"
|
||||
:hint="$t('audit_resp_codes_hint')"
|
||||
>
|
||||
<q-btn @click="addIncludeResponseCode" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
@@ -154,14 +166,15 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<p>Include Paths</p>
|
||||
<p><span v-text="$t('audit_paths')"></span></p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formAddIncludePath"
|
||||
@keydown.enter="addIncludePath"
|
||||
type="text"
|
||||
label="HTTP Path (regex)"
|
||||
hint="List of paths to be included (regex match). Empty list means all."
|
||||
:label="$t('audit_paths_label')"
|
||||
:hint="$t('audit_paths_hint')"
|
||||
>
|
||||
<q-btn @click="addIncludePath" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
@@ -180,14 +193,17 @@
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<p>Exclude Paths</p>
|
||||
<p>
|
||||
<span v-text="$t('audit_paths_exclude')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formAddExcludePath"
|
||||
@keydown.enter="addExcludePath"
|
||||
type="text"
|
||||
label="HTTP Path (regex)"
|
||||
hint="List of paths to be excluded (regex match). Empty list means none."
|
||||
:label="$t('audit_paths_exclude_label')"
|
||||
:hint="$t('audit_paths_exclude_hint')"
|
||||
>
|
||||
<q-btn @click="addExcludePath" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<q-tab-panel name="exchange_providers">
|
||||
<h6 class="q-my-none q-mb-sm">Exchange Providers</h6>
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('exchange_providers')"></span>
|
||||
</h6>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
<q-tab-panel name="extensions">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div>
|
||||
<h6 class="q-my-none">Extensions</h6>
|
||||
<h6 class="q-my-none">
|
||||
<span v-text="$t('extensions')"></span>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<p>Extension Sources</p>
|
||||
<div class="col-12 q-mb-md">
|
||||
<p>
|
||||
<span v-text="$t('extension_sources')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formAddExtensionsManifest"
|
||||
@keydown.enter="addExtensionsManifest"
|
||||
type="text"
|
||||
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
|
||||
hint="Repositories from where the extensions can be downloaded"
|
||||
:label="$t('ext_sources_label')"
|
||||
:hint="$t('ext_sources_hint')"
|
||||
>
|
||||
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
@@ -30,34 +35,44 @@
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Admin Extensions</p>
|
||||
<p>
|
||||
<span v-text="$t('admin_extensions')"></span>
|
||||
</p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_admin_extensions"
|
||||
multiple
|
||||
hint="Extensions only user with admin privileges can use"
|
||||
label="Admin extensions"
|
||||
:label="$t('admin_extensions_label')"
|
||||
:hint="$t('admin_extensions_hint')"
|
||||
:options="g.extensions"
|
||||
></q-select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<p>User Default Extensions</p>
|
||||
<p>
|
||||
<span v-text="$t('user_default_extensions')"></span>
|
||||
</p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_user_default_extensions"
|
||||
multiple
|
||||
hint="Extensions that will be enabled by default for the users."
|
||||
label="User extensions"
|
||||
:label="$t('user_default_extensions_label')"
|
||||
:hint="$t('user_default_extensions_hint')"
|
||||
:options="g.extensions"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Miscellaneous</p>
|
||||
<p>
|
||||
<span v-text="$t('miscellaneous')"></span>
|
||||
</p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Disable Extensions</q-item-label>
|
||||
<q-item-label caption>Disables all extensions</q-item-label>
|
||||
<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-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
@@ -71,10 +86,12 @@
|
||||
</q-item>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<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-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-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
|
||||
@@ -1,88 +1,104 @@
|
||||
<q-tab-panel name="funding">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none">Wallets Management</h6>
|
||||
<h6 class="q-my-none">
|
||||
<span v-text="$t('wallets_management')"></span>
|
||||
</h6>
|
||||
<br />
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p>Funding Source Info</p>
|
||||
<p>
|
||||
<span v-text="$t('funding_source_info')"></span>
|
||||
</p>
|
||||
<ul>
|
||||
<li
|
||||
v-text="'Funding Source: '+ settings.lnbits_backend_wallet_class"
|
||||
v-text="$t('funding_source', {wallet_class: settings.lnbits_backend_wallet_class})"
|
||||
></li>
|
||||
<li
|
||||
v-text="'Node Balance: ' + (auditData.node_balance_msats /
|
||||
1000).toLocaleString() + ' sats'"
|
||||
v-text="$t('node_balance', {balance: (auditData.node_balance_sats || 0).toLocaleString()})"
|
||||
></li>
|
||||
<li
|
||||
v-text="'LNbits Balance: ' + (auditData.lnbits_balance_msats /
|
||||
1000).toLocaleString() + ' sats'"
|
||||
v-text="$t('lnbits_balance', {balance: (auditData.lnbits_balance_sats || 0).toLocaleString()})"
|
||||
></li>
|
||||
<li
|
||||
v-text="'Reserve Percent: ' + (auditData.node_balance_msats /
|
||||
auditData.lnbits_balance_msats * 100).toFixed(2) + ' %'"
|
||||
v-text="$t('funding_reserve_percent', {
|
||||
percent: auditData.lnbits_balance_sats > 0
|
||||
? (auditData.node_balance_sats / auditData.lnbits_balance_sats * 100).toFixed(2)
|
||||
: 100
|
||||
})"
|
||||
></li>
|
||||
</ul>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col">
|
||||
{% if LNBITS_NODE_UI_AVAILABLE %}
|
||||
<p>Node Management</p>
|
||||
<p><span v-text="$t('node_management')"></span></p>
|
||||
<q-toggle
|
||||
label="Node UI"
|
||||
:label="$t('toggle_node_ui')"
|
||||
v-model="formData.lnbits_node_ui"
|
||||
></q-toggle>
|
||||
<q-toggle
|
||||
v-if="formData.lnbits_node_ui"
|
||||
label="Public node UI"
|
||||
:label="$t('toggle_public_node_ui')"
|
||||
v-model="formData.lnbits_public_node_ui"
|
||||
></q-toggle>
|
||||
<br />
|
||||
<q-toggle
|
||||
v-if="formData.lnbits_node_ui"
|
||||
label="Transactions Tab (Disable on large CLN nodes)"
|
||||
:label="$t('toggle_transactions_node_ui')"
|
||||
v-model="formData.lnbits_node_ui_transactions"
|
||||
></q-toggle>
|
||||
{% else %}
|
||||
<p>Node Management not supported by active funding source</p>
|
||||
<p><span v-text="$t('node_management_not_supported')"></span></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-4">
|
||||
<p>Invoice Expiry</p>
|
||||
<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>
|
||||
<q-input
|
||||
filled
|
||||
v-model.number="formData.lightning_invoice_expiry"
|
||||
type="number"
|
||||
label="Invoice expiry (seconds)"
|
||||
:label="$t('invoice_expiry_label')"
|
||||
mask="#######"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<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 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>
|
||||
</div>
|
||||
<div v-if="isSuperUser">
|
||||
@@ -91,6 +107,90 @@
|
||||
: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>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<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,140 +1,505 @@
|
||||
<q-tab-panel name="notifications">
|
||||
<q-card-section class="q-pa-none">
|
||||
<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>
|
||||
<br />
|
||||
<div>
|
||||
<p v-text="$t('notification_source')"></p>
|
||||
<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="formData.lnbits_status_manifest"
|
||||
type="text"
|
||||
:label="$t('notification_source_label')"
|
||||
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="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>
|
||||
</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>
|
||||
</q-tab-panel>
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
<q-tab-panel name="security">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none">Server Management</h6>
|
||||
<h6 class="q-my-none"><span v-text="$t('server_management')"></span></h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p>Base URL</p>
|
||||
<p><span v-text="$t('base_url')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model.number="formData.lnbits_baseurl"
|
||||
label="Static/Base url for the server"
|
||||
:label="$t('base_url_label')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<h6 class="q-my-none q-mb-sm">Authentication</h6>
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('authentication')"></span>
|
||||
</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="Token expire minutes"
|
||||
hint="Time in minutes until the token expires"
|
||||
:label="$t('auth_token_expiry_label')"
|
||||
:hint="$t('auth_token_expiry_hint')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
@@ -29,8 +31,8 @@
|
||||
filled
|
||||
v-model="formData.auth_allowed_methods"
|
||||
multiple
|
||||
hint="Allowed authorization methods"
|
||||
label="Select authorization methods"
|
||||
:hint="$t('auth_allowed_methods_hint')"
|
||||
:label="$t('auth_allowed_methods_label')"
|
||||
:options="formData.auth_all_methods"
|
||||
></q-select>
|
||||
</div>
|
||||
@@ -50,8 +52,8 @@
|
||||
v-model="nostrAcceptedUrl"
|
||||
@keydown.enter="addNostrUrl"
|
||||
type="text"
|
||||
label="Nostr Request URL"
|
||||
hint="Absolute URL that the clients will use to login."
|
||||
:label="$t('auth_nostr_label')"
|
||||
:hint="$t('auth_nostr_hint')"
|
||||
>
|
||||
<q-btn @click="addNostrUrl" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
@@ -81,8 +83,8 @@
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.google_client_id"
|
||||
label="Google Client ID"
|
||||
hint="Make sure thant the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token"
|
||||
:label="$t('auth_google_ci_label')"
|
||||
:hint="$t('auth_google_ci_hint')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
@@ -91,7 +93,7 @@
|
||||
filled
|
||||
v-model="formData.google_client_secret"
|
||||
type="password"
|
||||
label="Google Client Secret"
|
||||
:label="$t('auth_google_cs_label')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
@@ -108,8 +110,8 @@
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.github_client_id"
|
||||
label="GitHub Client ID"
|
||||
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/github/token"
|
||||
:label="$t('auth_gh_client_id_label')"
|
||||
:hint="$t('auth_gh_client_id_hint')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
@@ -118,7 +120,7 @@
|
||||
filled
|
||||
v-model="formData.github_client_secret"
|
||||
type="password"
|
||||
label="GitHub Client Secret"
|
||||
:label="$t('auth_gh_client_secret_label')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
@@ -135,7 +137,7 @@
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.keycloak_discovery_url"
|
||||
label="Keycloak Discovey URL"
|
||||
:label="$t('auth_keycloak_label')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
@@ -143,8 +145,8 @@
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.keycloak_client_id"
|
||||
label="Keycloak Client ID"
|
||||
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/keycloak/token"
|
||||
:label="$t('auth_keycloak_ci_label')"
|
||||
:hint="$t('auth_keycloak_ci_hint')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
@@ -153,7 +155,7 @@
|
||||
filled
|
||||
v-model="formData.keycloak_client_secret"
|
||||
type="password"
|
||||
label="Keycloak Client Secret"
|
||||
:label="$t('auth_keycloak_cs_label')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
@@ -281,31 +283,37 @@
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-12">
|
||||
<p v-text="$t('wallet_limiter')"></p>
|
||||
<p v-text="$t('callback_url_rules')"></p>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-4">
|
||||
<div class="col-12">
|
||||
<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-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>
|
||||
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 />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<q-banner v-if="updateAvailable" class="bg-primary text-white">
|
||||
<q-icon size="28px" name="update"></q-icon>
|
||||
|
||||
<span v-text="$t('update_available', {version: statusData.version})"></span>
|
||||
<template v-slot:action>
|
||||
<a
|
||||
class="q-btn"
|
||||
color="white"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/lnbits/lnbits/releases"
|
||||
v-text="$t('releases')"
|
||||
></a>
|
||||
</template>
|
||||
</q-banner>
|
||||
<q-banner v-if="!updateAvailable" class="bg-green text-white">
|
||||
<q-icon size="28px" name="checkmark"></q-icon>
|
||||
<span v-text="$t('latest_update', {version: statusData.version})"></span>
|
||||
</q-banner>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:rows="statusData.notifications"
|
||||
:columns="statusDataTable.columns"
|
||||
:no-data-label="$t('no_notifications')"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width> </q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.label"
|
||||
></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
v-if="props.row.type === 'update'"
|
||||
size="18px"
|
||||
name="update"
|
||||
color="green"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
v-if="props.row.type === 'warning'"
|
||||
size="18px"
|
||||
name="warning"
|
||||
color="red"
|
||||
></q-icon>
|
||||
</q-td>
|
||||
<q-td
|
||||
auto-width
|
||||
key="date"
|
||||
:props="props"
|
||||
v-text="formatDate(props.row.date)"
|
||||
>
|
||||
</q-td>
|
||||
<q-td key="message" :props="props"
|
||||
><span v-text="props.row.message"></span
|
||||
><a
|
||||
v-if="props.row.link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:href="props.row.link"
|
||||
v-text="$t('more')"
|
||||
></a
|
||||
></q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -1,79 +1,144 @@
|
||||
<q-tab-panel name="server">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div>
|
||||
<h6 class="q-my-none">Currency Settings</h6>
|
||||
<h6 class="q-my-none"><span v-text="$t('currency_settings')"></span></h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Allowed currencies</p>
|
||||
<p><span v-text="$t('allowed_currencies')"></span></p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_allowed_currencies"
|
||||
multiple
|
||||
hint="Limit the number of available fiat currencies"
|
||||
label="Allowed currencies"
|
||||
:hint="$t('allowed_currencies_hint')"
|
||||
:label="$t('allowed_currencies')"
|
||||
:options="{{ currencies | safe }}"
|
||||
></q-select>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Default Accounting Currency</p>
|
||||
<p><span v-text="$t('default_account_currency')"></span></p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_default_accounting_currency"
|
||||
clearable
|
||||
hint="Default currency for accounting"
|
||||
label="Currency"
|
||||
:hint="$t('default_account_currency_hint')"
|
||||
:label="$t('currency')"
|
||||
:options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}"
|
||||
></q-select>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<h6 class="q-my-none">Service Fee</h6>
|
||||
<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>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Service Fee</p>
|
||||
<p><span v-text="$t('service_fee')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_service_fee"
|
||||
label="Service fee (%)"
|
||||
:label="$t('service_fee_label')"
|
||||
step="0.1"
|
||||
hint="Fee charged per tx (%)"
|
||||
:hint="$t('service_fee_hint')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Service fee max</p>
|
||||
<p><span v-text="$t('service_fee_max')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_service_fee_max"
|
||||
label="Service fee max (sats)"
|
||||
hint="Max service fee to charge in (sats)"
|
||||
:label="$t('service_fee_max_label')"
|
||||
:hint="$t('service_fee_max_hint')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Fee Wallet</p>
|
||||
<p><span v-text="$t('fee_wallet')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_service_fee_wallet"
|
||||
label="Fee wallet (wallet ID)"
|
||||
hint="Wallet ID to send funds to"
|
||||
:label="$t('fee_wallet_label')"
|
||||
:hint="$t('fee_wallet_hint')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Disable Service Fee for Internal Payments</p>
|
||||
<p><span v-text="$t('disable_fee_internal')"></span></p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Disable Fee</q-item-label>
|
||||
<q-item-label caption
|
||||
>Disable Service Fee for Internal Lightning
|
||||
Payments</q-item-label
|
||||
>
|
||||
<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-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
|
||||
@@ -1,82 +1,72 @@
|
||||
<q-tab-panel name="site_customisation">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none">UI Management</h6>
|
||||
<h6 class="q-my-none"><span v-text="$t('ui_management')"></span></h6>
|
||||
<br />
|
||||
<div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-5">
|
||||
<p>Site Title</p>
|
||||
<p><span v-text="$t('ui_site_title')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_site_title"
|
||||
label="Site title"
|
||||
:label="$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<p>Site Tagline</p>
|
||||
<p><span v-text="$t('ui_site_tagline')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_site_tagline"
|
||||
label="Site tagline"
|
||||
:label="$t('ui_site_tagline')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-2 q-mt-xl">
|
||||
<q-toggle
|
||||
tip="Remove homepage elements like 'runs on' etc"
|
||||
:tip="$t('ui_toggle_elements_tip')"
|
||||
v-model="formData.lnbits_show_home_page_elements"
|
||||
:label="formData.lnbits_show_home_page_elements ? 'Enable elements on homepage' : 'Disable elements on homepage'"
|
||||
:label="formData.lnbits_show_home_page_elements ? $t('ui_elements_enable') : $t('ui_elements_disable')"
|
||||
></q-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Site Description</p>
|
||||
<p><span v-text="$t('ui_site_description')"></span></p>
|
||||
<q-input
|
||||
v-model="formData.lnbits_site_description"
|
||||
filled
|
||||
type="textarea"
|
||||
hint="Use plain text, Markdown, or raw HTML"
|
||||
:hint="$t('ui_site_description_hint')"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-4">
|
||||
<p>Default Wallet Name</p>
|
||||
<p><span v-text="$t('ui_default_wallet_name')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_default_wallet_name"
|
||||
label="LNbits wallet"
|
||||
:label="$t('lnbits_wallet')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<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>
|
||||
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_qr_logo"
|
||||
label="https://example.com/image.svg"
|
||||
hint="URL to logo image in QR code"
|
||||
:hint="$t('ui_qr_code_logo_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<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 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-8">
|
||||
<q-input
|
||||
@@ -84,7 +74,7 @@
|
||||
type="text"
|
||||
tip="Custom Badge"
|
||||
v-model.trim="formData.lnbits_custom_badge"
|
||||
label="Custom Badge 'USE WITH CAUTION - LNbits wallet is still in BETA'"
|
||||
:label="$t('ui_custom_badge_label')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
@@ -92,67 +82,127 @@
|
||||
filled
|
||||
v-model="formData.lnbits_custom_badge_color"
|
||||
:options="colors"
|
||||
label="Custom badge color"
|
||||
:label="$t('ui_custom_badge_color_label')"
|
||||
></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>Themes</p>
|
||||
<p><span v-text="$t('themes')"></span></p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_theme_options"
|
||||
multiple
|
||||
hint="Choose themes available for users"
|
||||
:hint="$t('themes_hint')"
|
||||
:options="lnbits_theme_options"
|
||||
label="Themes"
|
||||
:label="$t('themes')"
|
||||
></q-select>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Custom Logo</p>
|
||||
<p><span v-text="$t('custom_logo')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_custom_logo"
|
||||
label="https://example.com/image.png"
|
||||
hint="URL to logo image"
|
||||
:hint="$t('custom_logo_hint')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Ad Space Title</p>
|
||||
<p><span v-text="$t('ad_space_title')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_ad_space_title"
|
||||
label="Supported by"
|
||||
:label="$t('ad_space_title_label')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Advertisement Slots</p>
|
||||
<p><span v-text="$t('ad_slots')"></span></p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formData.lnbits_ad_space"
|
||||
type="text"
|
||||
label="url;img_light_url;img_dark_url, url..."
|
||||
hint="Ad url and image filepaths in CSV format, extensions can choose to honor"
|
||||
:label="$t('ad_slots_label')"
|
||||
:hint="$t('ad_slots_hint')"
|
||||
>
|
||||
</q-input>
|
||||
<q-toggle
|
||||
v-model="formData.lnbits_ad_space_enabled"
|
||||
:label="formData.lnbits_ad_space_enabled ? 'Ads enabled' : 'Ads disabled'"
|
||||
:label="formData.lnbits_ad_space_enabled ? $t('ads_enabled') : $t('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>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<q-tab-panel name="users">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none q-mb-sm">User Management</h6>
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('user_management')"></span>
|
||||
</h6>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 q-pr-sm">
|
||||
<p>Admin Users</p>
|
||||
<p><span v-text="$t('admin_users')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAddAdmin"
|
||||
@keydown.enter="addAdminUser"
|
||||
type="text"
|
||||
label="User ID"
|
||||
hint="Users with admin privileges"
|
||||
:label="$t('admin_users_label')"
|
||||
:hint="$t('admin_users_hint')"
|
||||
>
|
||||
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
@@ -31,14 +33,14 @@
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Allowed Users</p>
|
||||
<p><span v-text="$t('allowed_users')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAddUser"
|
||||
@keydown.enter="addAllowedUser"
|
||||
type="text"
|
||||
label="User ID"
|
||||
hint="Only these users can use LNbits"
|
||||
:label="$t('allowed_users_label')"
|
||||
:hint="$t('allowed_users_hint')"
|
||||
>
|
||||
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
@@ -58,10 +60,11 @@
|
||||
<br />
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<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-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-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
|
||||
@@ -1,60 +1,75 @@
|
||||
{% 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>
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %} {% from "macros.jinja"
|
||||
import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
||||
{% endblock %} {% block page %}
|
||||
|
||||
<q-badge
|
||||
v-if="checkChanges"
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
style="padding: 6px; border-radius: 6px"
|
||||
/>
|
||||
</q-btn>
|
||||
<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-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="checkChanges"
|
||||
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
|
||||
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 :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
|
||||
<q-badge
|
||||
v-if="needsRestart"
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
style="padding: 6px; border-radius: 6px"
|
||||
/>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,6 +149,14 @@
|
||||
><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"
|
||||
@@ -162,7 +185,8 @@
|
||||
"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_audit.html"%}{% include
|
||||
"admin/_tab_library.html"%}
|
||||
</q-tab-panels>
|
||||
</q-form>
|
||||
</template>
|
||||
@@ -173,7 +197,9 @@
|
||||
|
||||
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
|
||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||
<strong>Create Currecny Ticker Converter</strong>
|
||||
<div class="q-mb-md">
|
||||
<strong v-text="$t('create_ticker_converter')"></strong>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 q-mb-md">
|
||||
<q-select
|
||||
@@ -211,7 +237,4 @@
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,36 +1,63 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
{% 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>
|
||||
|
||||
<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>Components</strong>
|
||||
<strong v-text="$t('components')"></strong>
|
||||
<div style="width: 250px" class="q-pa-sm">
|
||||
<canvas ref="componentUseChart"></canvas>
|
||||
<canvas v-if="chartsReady" 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>To 5 Long Running Endpoints</strong>
|
||||
<strong v-text="$t('long_running_endpoints')"></strong>
|
||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
||||
<canvas ref="longDurationChart"></canvas>
|
||||
<canvas v-if="chartsReady" 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>HTTP Request Methods</strong>
|
||||
<strong v-text="$t('http_request_methods')"></strong>
|
||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
||||
<canvas ref="requestMethodChart"></canvas>
|
||||
<canvas v-if="chartsReady" 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>HTTP Response Codes</strong>
|
||||
<strong v-text="$t('http_response_codes')"></strong>
|
||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
||||
<canvas ref="responseCodeChart"></canvas>
|
||||
<canvas v-if="chartsReady" ref="responseCodeChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
@@ -70,6 +97,7 @@
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-select
|
||||
v-else-if="['component', 'response_code','request_method'].includes(col.name)"
|
||||
v-model="searchData[col.name]"
|
||||
@@ -96,7 +124,9 @@
|
||||
class="cursor-pointer q-mr-xs"
|
||||
@click="showDetailsDialog(props.row)"
|
||||
>
|
||||
<q-tooltip>Request Details</q-tooltip>
|
||||
<q-tooltip
|
||||
><span v-text="$t('request_details')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<span v-text="formatDate(props.row.created_at)"></span>
|
||||
@@ -140,7 +170,7 @@
|
||||
|
||||
<q-dialog v-model="auditDetailsDialog.show" position="top">
|
||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||
<strong>HTTP Request Details</strong>
|
||||
<strong v-text="$t('http_request_details')"></strong>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
@@ -168,6 +198,4 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ static_url_for('static', 'js/audit.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -85,7 +85,12 @@
|
||||
<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="wallet.inkey"></i>"}</code><br />
|
||||
<code
|
||||
>{"X-Api-Key": "<i
|
||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||
></i
|
||||
>"}</code
|
||||
><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
@@ -96,7 +101,8 @@
|
||||
<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="wallet.inkey"></i>"</code
|
||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i
|
||||
>"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -112,7 +118,12 @@
|
||||
<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="wallet.inkey"></i>"}</code><br />
|
||||
<code
|
||||
>{"X-Api-Key": "<i
|
||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||
></i
|
||||
>"}</code
|
||||
><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"out": false, "amount": <int>, "memo": <string>,
|
||||
@@ -130,8 +141,9 @@
|
||||
<code
|
||||
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
||||
'{"out": false, "amount": <int>, "memo": <string>}' -H
|
||||
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
|
||||
application/json"</code
|
||||
"X-Api-Key:
|
||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
||||
"Content-type: application/json"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -218,7 +230,12 @@
|
||||
/api/v1/payments/<payment_hash></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": "{{ wallet.inkey }}"}</code>
|
||||
<code
|
||||
>{"X-Api-Key": "<i
|
||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||
></i
|
||||
>"}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
@@ -227,10 +244,42 @@
|
||||
<code
|
||||
>curl -X GET
|
||||
<span v-text="baseUrl"></span>api/v1/payments/<payment_hash> -H
|
||||
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
|
||||
application/json"</code
|
||||
"X-Api-Key:
|
||||
<i v-text="inkeyHidden ? '****************' : 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/<invoice_key></code
|
||||
>
|
||||
<h5
|
||||
class="text-caption q-mt-sm q-mb-none"
|
||||
v-text="$t('websocket_example')"
|
||||
></h5>
|
||||
<code
|
||||
>wscat -c <span v-text="websocketUrl"></span>/<span
|
||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||
></span
|
||||
></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)/payments
|
||||
</h5>
|
||||
<code>{"balance": <int>, "payment": <object>}</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-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>
|
||||
|
||||
@@ -1,39 +1,51 @@
|
||||
{% extends "base.html" %}
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
||||
<!---->
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ static_url_for('static', 'js/account.js') }}"></script>
|
||||
{% endblock %} {% block page %}
|
||||
{% 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>
|
||||
|
||||
<div class="row q-col-gutter-md">
|
||||
<div v-if="user" class="col-12 col-md-6 q-gutter-y-md">
|
||||
<div v-if="user" class="col-md-12 col-lg-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_config')"></span>
|
||||
<span v-text="$t('password')"></span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col">
|
||||
@@ -90,7 +102,7 @@
|
||||
unelevated
|
||||
color="primary"
|
||||
class="float-right"
|
||||
:label="$t('change_password')"
|
||||
:label="$t('update_password')"
|
||||
>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
@@ -98,7 +110,7 @@
|
||||
<q-card-section>
|
||||
<div class="col q-mb-sm">
|
||||
<h4 class="q-my-none">
|
||||
<span v-text="$t('pubkey')"></span>
|
||||
Nostr <span v-text="$t('pubkey')"></span>
|
||||
</h4>
|
||||
</div>
|
||||
<q-input
|
||||
@@ -129,11 +141,10 @@
|
||||
</q-card-section>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-card-section>
|
||||
<q-card-section v-if="user.extra.picture">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-img
|
||||
v-if="user.extra.picture"
|
||||
style="max-width: 100px"
|
||||
:src="user.extra.picture"
|
||||
class="float-right"
|
||||
@@ -141,7 +152,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-card-section>
|
||||
<q-input
|
||||
@@ -277,7 +287,7 @@
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@click="showUpdateCredentials()"
|
||||
:label="$t('update_credentials')"
|
||||
:label="$t('change_password')"
|
||||
filled
|
||||
color="primary"
|
||||
class="float-right"
|
||||
@@ -286,6 +296,15 @@
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="theme">
|
||||
<q-btn
|
||||
v-if="g.user.admin"
|
||||
class="absolute-top-right"
|
||||
flat
|
||||
round
|
||||
icon="settings"
|
||||
to="/admin#site_customisation"
|
||||
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
||||
></q-btn>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4"><span v-text="$t('language')"></span></div>
|
||||
<div class="col-8">
|
||||
@@ -325,7 +344,7 @@
|
||||
v-if="g.allowedThemes.includes('classic')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('classic')"
|
||||
@click="themeChoiceFunc('classic')"
|
||||
icon="circle"
|
||||
color="deep-purple"
|
||||
size="md"
|
||||
@@ -335,9 +354,9 @@
|
||||
v-if="g.allowedThemes.includes('bitcoin')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('bitcoin')"
|
||||
@click="themeChoiceFunc('bitcoin')"
|
||||
icon="circle"
|
||||
color="orange"
|
||||
color="deep-orange"
|
||||
size="md"
|
||||
><q-tooltip>bitcoin</q-tooltip>
|
||||
</q-btn>
|
||||
@@ -345,7 +364,7 @@
|
||||
v-if="g.allowedThemes.includes('mint')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('mint')"
|
||||
@click="themeChoiceFunc('mint')"
|
||||
icon="circle"
|
||||
color="green"
|
||||
size="md"
|
||||
@@ -354,7 +373,7 @@
|
||||
v-if="g.allowedThemes.includes('autumn')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('autumn')"
|
||||
@click="themeChoiceFunc('autumn')"
|
||||
icon="circle"
|
||||
color="brown"
|
||||
size="md"
|
||||
@@ -364,7 +383,7 @@
|
||||
v-if="g.allowedThemes.includes('monochrome')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('monochrome')"
|
||||
@click="themeChoiceFunc('monochrome')"
|
||||
icon="circle"
|
||||
color="grey"
|
||||
size="md"
|
||||
@@ -374,7 +393,7 @@
|
||||
v-if="g.allowedThemes.includes('salvador')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('salvador')"
|
||||
@click="themeChoiceFunc('salvador')"
|
||||
icon="circle"
|
||||
color="blue-10"
|
||||
size="md"
|
||||
@@ -384,7 +403,7 @@
|
||||
v-if="g.allowedThemes.includes('freedom')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('freedom')"
|
||||
@click="themeChoiceFunc('freedom')"
|
||||
icon="circle"
|
||||
color="pink-13"
|
||||
size="md"
|
||||
@@ -394,7 +413,7 @@
|
||||
v-if="g.allowedThemes.includes('cyber')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('cyber')"
|
||||
@click="themeChoiceFunc('cyber')"
|
||||
icon="circle"
|
||||
color="light-green-9"
|
||||
size="md"
|
||||
@@ -404,7 +423,7 @@
|
||||
v-if="g.allowedThemes.includes('flamingo')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('flamingo')"
|
||||
@click="themeChoiceFunc('flamingo')"
|
||||
icon="circle"
|
||||
color="pink-3"
|
||||
size="md"
|
||||
@@ -412,6 +431,22 @@
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">
|
||||
<span v-text="$t('background_image')"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-input
|
||||
v-model="bgimageSelection"
|
||||
:label="$t('background_image')"
|
||||
@update:model-value="bgimageChoiceFunc"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('background_image')"></span
|
||||
></q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">
|
||||
<span v-text="$t('gradient_background')"></span>
|
||||
@@ -458,9 +493,9 @@
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-select
|
||||
v-model="borderChoice"
|
||||
v-model="borderSelection"
|
||||
:options="borderOptions"
|
||||
label="Reactions"
|
||||
label="Borders"
|
||||
@update:model-value="applyBorder"
|
||||
>
|
||||
<q-tooltip
|
||||
@@ -470,7 +505,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">Notifications</div>
|
||||
<div class="col-4">
|
||||
<span v-text="$t('notifications')"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<lnbits-notifications-btn
|
||||
v-if="g.user"
|
||||
@@ -484,7 +521,7 @@
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-select
|
||||
v-model="reactionChoice"
|
||||
v-model="reactionSelection"
|
||||
:options="reactionOptions"
|
||||
label="Reactions"
|
||||
@update:model-value="reactionChoiceFunc"
|
||||
@@ -496,6 +533,230 @@
|
||||
</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>
|
||||
<code>Authorization</code>
|
||||
|
||||
</strong>
|
||||
<span> header.</span>
|
||||
</q-badge>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<table
|
||||
class="full-width lnbits__table-bordered"
|
||||
style="
|
||||
border-collapse: collapse;
|
||||
background-color: grey;
|
||||
"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="float-left">Header Name</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="float-left">Header Value</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Authorization</strong>
|
||||
</td>
|
||||
<td>
|
||||
<div class="row q-pt-sm">
|
||||
<div class="col-2 q-mt-sm">
|
||||
<strong>Bearer </strong>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<q-input
|
||||
v-model="apiAcl.apiToken"
|
||||
:label="$t('api_token_id')"
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
:type="selectedApiAcl.showId ? 'text': 'password'"
|
||||
class="q-mb-md"
|
||||
>
|
||||
<q-btn
|
||||
@click="selectedApiAcl.showId = !selectedApiAcl.showId"
|
||||
dense
|
||||
flat
|
||||
:icon="selectedApiAcl.showId ? 'visibility_off' : 'visibility'"
|
||||
color="black"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
@click="copyText(apiAcl.apiToken)"
|
||||
icon="content_copy"
|
||||
color="black"
|
||||
flat
|
||||
dense
|
||||
></q-btn>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-badge>
|
||||
<span
|
||||
>Please store this token. It cannot be later
|
||||
retrieved, only revoked.</span
|
||||
>
|
||||
</q-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
row-key="path"
|
||||
:rows="selectedApiAcl.endpoints"
|
||||
:columns="apiAcl.columns"
|
||||
v-model:pagination="apiAcl.pagination"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
<q-toggle
|
||||
v-if="col.name == 'read'"
|
||||
v-model="selectedApiAcl.allRead"
|
||||
@update:model-value="handleAllEndpointsReadAccess()"
|
||||
:label="$t('read')"
|
||||
size="sm"
|
||||
></q-toggle>
|
||||
<q-toggle
|
||||
v-else-if="col.name == 'write'"
|
||||
v-model="selectedApiAcl.allWrite"
|
||||
@update:model-value="handleAllEndpointsWriteAccess()"
|
||||
:label="$t('write')"
|
||||
size="sm"
|
||||
></q-toggle>
|
||||
<span v-else v-text="col.label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td>
|
||||
<span v-text="props.row.name"></span>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<span v-text="props.row.path"></span>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-toggle size="sm" v-model="props.row.read" />
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-toggle size="sm" v-model="props.row.write" />
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
<q-separator></q-separator>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedApiAcl.id" class="row q-mt-md">
|
||||
<div class="col-sm-12 col-md-6">
|
||||
<q-btn
|
||||
@click="askPasswordAndRunFunction('updateApiACLs')"
|
||||
:label="$t('update')"
|
||||
filled
|
||||
color="primary"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-6">
|
||||
<q-btn
|
||||
@click="askPasswordAndRunFunction('deleteApiACL')"
|
||||
:label="$t('delete')"
|
||||
icon="delete"
|
||||
color="negative"
|
||||
class="float-right"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
</div>
|
||||
@@ -510,4 +771,126 @@
|
||||
</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 %}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {{ window_vars(user, extensions) }}{% block page %}
|
||||
{% 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 %}
|
||||
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-12">
|
||||
<q-card>
|
||||
<div class="q-pa-xs">
|
||||
<div>
|
||||
<div class="q-gutter-y-md">
|
||||
<q-tabs
|
||||
:model-value="tab"
|
||||
@@ -49,6 +53,14 @@
|
||||
v-text="$t('new_version') + ` (${updatableExtensions?.length})`"
|
||||
></span>
|
||||
</q-badge>
|
||||
<q-btn
|
||||
v-if="g.user.admin"
|
||||
flat
|
||||
round
|
||||
icon="settings"
|
||||
to="/admin#extensions"
|
||||
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
||||
></q-btn>
|
||||
</q-tabs>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,6 +69,7 @@
|
||||
</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"
|
||||
@@ -105,6 +118,7 @@
|
||||
></q-tooltip>
|
||||
</q-badge>
|
||||
<div
|
||||
v-if="extension.name"
|
||||
class="text-h5"
|
||||
style="cursor: pointer"
|
||||
@click="showExtensionDetails(extension.id, extension.details_link)"
|
||||
@@ -177,7 +191,7 @@
|
||||
<div class="col-10">
|
||||
<div v-if="!extension.inProgress">
|
||||
<q-btn
|
||||
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
|
||||
v-if="g.user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
|
||||
flat
|
||||
color="primary"
|
||||
type="a"
|
||||
@@ -185,20 +199,20 @@
|
||||
:label="$t('open')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
|
||||
v-if="g.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 && !user.admin"
|
||||
v-if="extension.isAdminOnly && !g.user.admin"
|
||||
v-text="$t('admin_only')"
|
||||
>
|
||||
</q-badge>
|
||||
|
||||
<q-btn
|
||||
v-else-if="extension.isInstalled && extension.isActive && !user.extensions.includes(extension.id)"
|
||||
v-else-if="extension.isInstalled && extension.isActive && !g.user.extensions.includes(extension.id)"
|
||||
flat
|
||||
color="primary"
|
||||
@click="enableExtensionForUser(extension)"
|
||||
@@ -221,7 +235,7 @@
|
||||
</q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -323,7 +337,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
@@ -422,7 +436,10 @@
|
||||
:header-class="getReleaseIconColor(release)"
|
||||
>
|
||||
<div v-if="release.inProgress">
|
||||
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||
<q-spinner-bars
|
||||
color="primary"
|
||||
size="2.55em"
|
||||
></q-spinner-bars>
|
||||
</div>
|
||||
<div v-else-if="release.error">
|
||||
<q-icon
|
||||
@@ -433,7 +450,7 @@
|
||||
></q-icon>
|
||||
<span v-text="$t('release_details_error')"></span>
|
||||
</div>
|
||||
<q-card v-else>
|
||||
<q-card class="no-border" v-else>
|
||||
<q-card-section v-if="release.is_version_compatible">
|
||||
<span
|
||||
v-if="release.requiresPayment && !release.paid_sats"
|
||||
@@ -608,7 +625,7 @@
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-spinner v-else color="primary" size="2.55em"></q-spinner>
|
||||
<q-spinner-bars v-else color="primary" size="2.55em"></q-spinner-bars>
|
||||
<div
|
||||
v-if="selectedExtension"
|
||||
v-show="manageExtensionTab === 'sell'"
|
||||
@@ -694,7 +711,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div v-else class="col">
|
||||
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
@@ -942,12 +959,12 @@
|
||||
value="false"
|
||||
:label="item.name + ` (v${item.latestRelease?.version})`"
|
||||
>
|
||||
<q-spinner
|
||||
<q-spinner-bars
|
||||
v-if="item.inProgress"
|
||||
color="primary"
|
||||
size="1.5em"
|
||||
class="q-ml-md"
|
||||
></q-spinner>
|
||||
></q-spinner-bars>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
@@ -971,674 +988,4 @@
|
||||
</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 %}
|
||||
|
||||
@@ -1,139 +1,150 @@
|
||||
{% 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;
|
||||
}
|
||||
{% 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;
|
||||
}
|
||||
|
||||
@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: ''
|
||||
@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;
|
||||
}
|
||||
}
|
||||
},
|
||||
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
|
||||
})
|
||||
</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
|
||||
}
|
||||
)
|
||||
|
||||
window.location.href = '/admin'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
window.location.href = '/admin'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
{% endblock %}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
||||
<!---->
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% 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 %}
|
||||
{% block scripts %} {{ window_vars(user, wallet) }}{% endblock %} {% block page
|
||||
%}
|
||||
<div class="row q-col-gutter-md">
|
||||
{% if HIDE_API and AD_SPACE %}
|
||||
<div class="col-12 col-md-8 q-gutter-y-md">
|
||||
@@ -25,102 +20,131 @@
|
||||
} : ''"
|
||||
>
|
||||
{% endif %}
|
||||
<q-scroll-area
|
||||
v-if="!mobileSimple"
|
||||
style="
|
||||
height: 115px;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
"
|
||||
>
|
||||
<div class="row no-wrap q-gutter-md q-pr-md">
|
||||
<q-card
|
||||
v-for="wallet in g.user.wallets"
|
||||
:key="wallet.id"
|
||||
class="wallet-list-card"
|
||||
bordered
|
||||
tag="a"
|
||||
:href="wallet.url"
|
||||
:style="
|
||||
g.wallet && g.wallet.id === wallet.id
|
||||
? `border: 1px solid ${primaryColor}; width: 250px; text-decoration: none;`
|
||||
: 'width: 250px; text-decoration: none;'
|
||||
"
|
||||
:class="{
|
||||
'active-wallet-card': g.wallet && g.wallet.id === wallet.id
|
||||
}"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="row items-center">
|
||||
<q-avatar
|
||||
size="lg"
|
||||
:color="
|
||||
g.wallet && g.wallet.id === wallet.id
|
||||
? $q.dark.isActive
|
||||
? 'primary'
|
||||
: 'primary'
|
||||
: 'grey-5'
|
||||
"
|
||||
>
|
||||
<q-icon
|
||||
name="flash_on"
|
||||
:size="$q.dark.isActive ? '21px' : '20px'"
|
||||
:color="$q.dark.isActive ? 'black' : 'grey-3'"
|
||||
></q-icon>
|
||||
</q-avatar>
|
||||
<div
|
||||
class="text-h6 q-pl-md"
|
||||
:class="{
|
||||
'text-bold': g.wallet && g.wallet.id === wallet.id
|
||||
}"
|
||||
v-text="wallet.name"
|
||||
></div>
|
||||
</div>
|
||||
<div class="row items-center q-pt-sm">
|
||||
<h6 class="q-my-none text-no-wrap">
|
||||
<strong v-text="wallet.fsat"></strong>
|
||||
<small> {{LNBITS_DENOMINATION}}</small>
|
||||
</h6>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-scroll-area>
|
||||
<q-card
|
||||
:style="$q.screen.lt.md ? {
|
||||
background: $q.screen.lt.md ? 'none !important': ''
|
||||
, boxShadow: $q.screen.lt.md ? 'none !important': ''
|
||||
, margin: $q.screen.lt.md && mobileSimple ? 'auto !important': ''
|
||||
, width: $q.screen.lt.md && mobileSimple ? '90% !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>
|
||||
<h3 class="q-my-none text-no-wrap">
|
||||
<strong v-text="formattedBalance"></strong>
|
||||
<small> {{LNBITS_DENOMINATION}}</small>
|
||||
<lnbits-update-balance
|
||||
:wallet_id="this.g.wallet.id"
|
||||
@credit-value="handleBalanceUpdate"
|
||||
class="q-ml-md"
|
||||
></lnbits-update-balance>
|
||||
</h3>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div v-if="g.wallet.currency">
|
||||
<span
|
||||
class="text-h5 text-italic"
|
||||
v-text="formattedFiatBalance"
|
||||
style="opacity: 0.75"
|
||||
></span>
|
||||
</div>
|
||||
<q-card-section style="height: 130px">
|
||||
<div class="row q-gutter-md">
|
||||
<div
|
||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
|
||||
class="col-1"
|
||||
style="max-width: 30px"
|
||||
>
|
||||
<q-btn
|
||||
v-if="g.fiatTracking"
|
||||
@click="swapBalancePriority"
|
||||
style="height: 50px"
|
||||
class="q-mt-lg"
|
||||
color="primary"
|
||||
flat
|
||||
dense
|
||||
icon="swap_vert"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div
|
||||
class="column"
|
||||
:class="{
|
||||
'q-pt-sm': g.fiatTracking,
|
||||
'q-pt-lg': !g.fiatTracking
|
||||
}"
|
||||
v-if="!isFiatPriority || !g.fiatTracking"
|
||||
style="height: 100px"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<lnbits-update-balance
|
||||
v-if="$q.screen.lt.lg"
|
||||
:wallet_id="this.g.wallet.id"
|
||||
:callback="updateBalanceCallback"
|
||||
:small_btn="true"
|
||||
></lnbits-update-balance>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div v-if="g.fiatTracking">
|
||||
<span
|
||||
class="text-h5 text-italic"
|
||||
v-text="formattedFiatAmount"
|
||||
style="opacity: 0.75"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="column"
|
||||
v-if="isFiatPriority && g.fiatTracking"
|
||||
:class="{
|
||||
'q-pt-sm': g.fiatTracking,
|
||||
'q-pt-lg': !g.fiatTracking
|
||||
}"
|
||||
style="height: 100px"
|
||||
>
|
||||
<div class="col-7">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<div
|
||||
v-if="g.fiatTracking"
|
||||
class="text-h3 q-my-none text-no-wrap"
|
||||
>
|
||||
<strong v-text="formattedFiatAmount"></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<lnbits-update-balance
|
||||
v-if="$q.screen.lt.lg"
|
||||
:wallet_id="this.g.wallet.id"
|
||||
:callback="updateBalanceCallback"
|
||||
:small_btn="true"
|
||||
></lnbits-update-balance>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<span
|
||||
class="text-h5 text-italic"
|
||||
style="opacity: 0.75"
|
||||
v-text="formattedBalance + ' {{LNBITS_DENOMINATION}}'"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute-right q-pa-md"
|
||||
v-if="$q.screen.gt.md && g.fiatTracking && '{{ LNBITS_DENOMINATION }}' == 'sats'"
|
||||
>
|
||||
<div class="text-bold text-italic">BTC Price</div>
|
||||
<span
|
||||
class="text-bold text-italic"
|
||||
v-text="formattedExchange"
|
||||
></span>
|
||||
</div>
|
||||
<q-btn
|
||||
@click="mobileSimple = !mobileSimple"
|
||||
v-if="$q.screen.lt.md"
|
||||
@click="simpleMobile()"
|
||||
color="primary"
|
||||
class="float-right lt-md"
|
||||
class="q-ml-xl absolute-right"
|
||||
dense
|
||||
size="sm"
|
||||
style="height: 20px; margin-top: 75px"
|
||||
flat
|
||||
:icon="mobileSimple ? 'unfold_more' : 'unfold_less'"
|
||||
:label="mobileSimple ? $t('more') : $t('less')"
|
||||
@@ -133,41 +157,44 @@
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
class="full-width"
|
||||
class="q-mr-md"
|
||||
@click="showParseDialog"
|
||||
:label="$t('paste_request')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
class="full-width"
|
||||
class="q-mr-md"
|
||||
@click="showReceiveDialog"
|
||||
:label="$t('create_invoice')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="secondary"
|
||||
icon="photo_camera"
|
||||
icon="qr_code_scanner"
|
||||
@click="showCamera"
|
||||
:label="$t('scan')"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('camera_tooltip')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
<lnbits-update-balance
|
||||
v-if="$q.screen.gt.md"
|
||||
:wallet_id="this.g.wallet.id"
|
||||
:callback="updateBalanceCallback"
|
||||
:small_btn="false"
|
||||
></lnbits-update-balance>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<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' : ''
|
||||
}
|
||||
: ''
|
||||
@@ -175,26 +202,47 @@
|
||||
>
|
||||
<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 || $q.screen.gt.sm"
|
||||
class="col-12 col-md-5 q-gutter-y-md"
|
||||
>
|
||||
<div v-if="!mobileSimple" class="col-12 col-md-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<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 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>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
@@ -221,99 +269,196 @@
|
||||
{% endif %}
|
||||
|
||||
<q-expansion-item
|
||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
|
||||
group="extras"
|
||||
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')"
|
||||
icon="phone_android"
|
||||
:label="$t('access_wallet_on_mobile')"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<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>
|
||||
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>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="attach_money"
|
||||
:label="$t('fiat_tracking')"
|
||||
icon="settings"
|
||||
:label="$t('wallet_config')"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div style="max-width: 360px">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
clearable
|
||||
v-model="update.currency"
|
||||
type="text"
|
||||
:label="$t('currency')"
|
||||
:options="receive.units.filter((u) => u !== 'sat')"
|
||||
></q-select>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<q-input
|
||||
filled
|
||||
v-model.trim="update.name"
|
||||
label="Name"
|
||||
dense
|
||||
/>
|
||||
</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-btn
|
||||
:disable="!update.name.length"
|
||||
unelevated
|
||||
class="q-mt-sm"
|
||||
color="primary"
|
||||
:label="$t('update_currency')"
|
||||
@click="updateWallet({ currency: update.currency || '' })"
|
||||
></q-btn>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
<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-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
@@ -337,7 +482,6 @@
|
||||
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"
|
||||
@@ -353,9 +497,84 @@
|
||||
>{% 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"
|
||||
@@ -373,7 +592,7 @@
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="receive.data.amount"
|
||||
v-model="receive.data.amount"
|
||||
:label="$t('amount')+' ({{LNBITS_DENOMINATION}}) *'"
|
||||
mask="#.##"
|
||||
fill-mask="0"
|
||||
@@ -432,11 +651,11 @@
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<q-spinner
|
||||
<q-spinner-bars
|
||||
v-if="receive.status == 'loading'"
|
||||
color="primary"
|
||||
size="2.55em"
|
||||
></q-spinner>
|
||||
></q-spinner-bars>
|
||||
</q-form>
|
||||
</q-card>
|
||||
<q-card
|
||||
@@ -445,9 +664,7 @@
|
||||
>
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
<div v-html="invoiceQrCode"></div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
@@ -463,9 +680,13 @@
|
||||
color="positive"
|
||||
text-color="white"
|
||||
></q-avatar>
|
||||
NFC supported
|
||||
<span v-text="$t('nfc_supported')"></span>
|
||||
</q-chip>
|
||||
<span v-else class="text-caption text-grey">NFC not supported</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-caption text-grey"
|
||||
v-text="$t('nfc_not_supported')"
|
||||
></span>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
@@ -488,27 +709,131 @@
|
||||
<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">
|
||||
<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 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>
|
||||
<div v-if="canPay" class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
|
||||
@@ -29,7 +29,46 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="openChannelDialog.show" position="top">
|
||||
<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-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input
|
||||
@@ -171,10 +210,41 @@
|
||||
<q-tr :props="props">
|
||||
<div class="q-pb-sm">
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<div
|
||||
class="text-subtitle1 col-grow"
|
||||
v-text="props.row.name"
|
||||
></div>
|
||||
<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-caption" v-if="props.row.short_id">
|
||||
<span v-text="props.row.short_id"></span>
|
||||
<q-btn
|
||||
@@ -201,6 +271,7 @@
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</div>
|
||||
|
||||
<lnbits-channel-balance
|
||||
:balance="props.row.balance"
|
||||
:color="props.row.color"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
||||
<!---->
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
||||
|
||||
<q-dialog v-model="nodeInfoDialog.show" position="top">
|
||||
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
|
||||
@@ -40,336 +43,4 @@
|
||||
</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 %}
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
{% 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,6 +35,7 @@
|
||||
<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
|
||||
@@ -47,12 +48,14 @@
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-td auto-width v-if="g.user.super_user">
|
||||
<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"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
{% 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 justify-center">
|
||||
<div class="col">
|
||||
@@ -11,13 +14,35 @@
|
||||
{%include "users/_createWalletDialog.html" %}
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-btn
|
||||
@click="showAccountPage()"
|
||||
:label="$t('create_account')"
|
||||
color="primary"
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-btn>
|
||||
<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-card class="q-pa-md">
|
||||
<q-table
|
||||
@@ -102,7 +127,17 @@
|
||||
:label="props.row.wallet_count"
|
||||
@click="fetchWallets(props.row.id)"
|
||||
>
|
||||
<q-tooltip>Show Wallets</q-tooltip>
|
||||
</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-btn>
|
||||
</q-td>
|
||||
|
||||
@@ -148,6 +183,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ static_url_for('static', 'js/users.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import os
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from shutil import make_archive
|
||||
from pathlib import Path
|
||||
from shutil import make_archive, move
|
||||
from subprocess import Popen
|
||||
from typing import Optional
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import IO, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
import filetype
|
||||
from fastapi import APIRouter, Depends, File, Header, HTTPException, UploadFile
|
||||
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
|
||||
@@ -23,6 +32,7 @@ from .. import core_app_extra
|
||||
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(
|
||||
@@ -47,6 +57,18 @@ 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),
|
||||
@@ -60,6 +82,7 @@ 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."
|
||||
@@ -68,6 +91,15 @@ 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,
|
||||
@@ -78,12 +110,9 @@ 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,
|
||||
dependencies=[Depends(check_super_user)],
|
||||
)
|
||||
async def api_delete_settings() -> None:
|
||||
@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 delete_admin_settings()
|
||||
server_restart.set()
|
||||
|
||||
@@ -136,3 +165,93 @@ 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")
|
||||
|
||||
@@ -15,7 +15,6 @@ 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,
|
||||
@@ -30,6 +29,7 @@ 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,21 +50,18 @@ api_router = APIRouter(tags=["Core"])
|
||||
async def health() -> dict:
|
||||
return {
|
||||
"server_time": int(time()),
|
||||
"up_time": int(time() - settings.server_startup_time),
|
||||
"up_time": settings.lnbits_server_up_time,
|
||||
}
|
||||
|
||||
|
||||
@api_router.get("/api/v1/status", status_code=HTTPStatus.OK)
|
||||
async def health_check(wallet: WalletTypeInfo = Depends(require_invoice_key)) -> dict:
|
||||
async def health_check(user: User = Depends(check_user_exists)) -> dict:
|
||||
stat: dict[str, Any] = {
|
||||
"server_time": int(time()),
|
||||
"up_time": int(time() - settings.server_startup_time),
|
||||
"up_time": settings.lnbits_server_up_time,
|
||||
"up_time_seconds": int(time() - settings.server_startup_time),
|
||||
}
|
||||
|
||||
user = await get_user(wallet.wallet.user)
|
||||
if not user:
|
||||
return stat
|
||||
|
||||
stat["version"] = settings.version
|
||||
if not user.admin:
|
||||
return stat
|
||||
@@ -132,6 +129,7 @@ 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:
|
||||
@@ -227,7 +225,7 @@ async def api_perform_lnurlauth(
|
||||
|
||||
@api_router.get(
|
||||
"/api/v1/rate/history",
|
||||
dependencies=[Depends(require_invoice_key)],
|
||||
dependencies=[Depends(check_user_exists)],
|
||||
)
|
||||
async def api_exchange_rate_history() -> list[dict]:
|
||||
return settings.lnbits_exchange_rate_history
|
||||
|
||||
@@ -11,12 +11,26 @@ 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,
|
||||
)
|
||||
@@ -44,6 +58,7 @@ from ..models import (
|
||||
UpdateUserPassword,
|
||||
UpdateUserPubkey,
|
||||
User,
|
||||
UserAcls,
|
||||
UserExtra,
|
||||
)
|
||||
|
||||
@@ -95,9 +110,131 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
||||
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.UNAUTHORIZED, "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
|
||||
@@ -370,6 +507,17 @@ def _auth_success_response(
|
||||
return response
|
||||
|
||||
|
||||
def _auth_api_token_response(
|
||||
username: str, api_token_id: str, token_expire_minutes: int
|
||||
):
|
||||
payload = AccessTokenPayload(
|
||||
sub=username, api_token_id=api_token_id, auth_time=int(time())
|
||||
)
|
||||
return create_access_token(
|
||||
data=payload.dict(), token_expire_minutes=token_expire_minutes
|
||||
)
|
||||
|
||||
|
||||
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
||||
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
|
||||
access_token = create_access_token(data=payload.dict())
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Annotated, List, Optional, Union
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
@@ -17,7 +16,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 template_renderer
|
||||
from lnbits.helpers import check_callback_url, template_renderer
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_funding_source
|
||||
|
||||
@@ -37,7 +36,7 @@ generic_router = APIRouter(
|
||||
|
||||
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
||||
async def favicon():
|
||||
return FileResponse(Path("lnbits", "static", "favicon.ico"))
|
||||
return RedirectResponse(settings.lnbits_qr_logo)
|
||||
|
||||
|
||||
@generic_router.get("/", response_class=HTMLResponse)
|
||||
@@ -50,12 +49,9 @@ 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:
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"error.html",
|
||||
{
|
||||
"err": "Super user account has already been configured.",
|
||||
},
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Super user account has already been configured.",
|
||||
)
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
@@ -107,7 +103,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()
|
||||
|
||||
extensions = [
|
||||
extension_data = [
|
||||
{
|
||||
"id": ext.id,
|
||||
"name": ext.name,
|
||||
@@ -152,7 +148,8 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
||||
"core/extensions.html",
|
||||
{
|
||||
"user": user.json(),
|
||||
"extensions": extensions,
|
||||
"extension_data": extension_data,
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -179,25 +176,25 @@ async def wallet(
|
||||
wallet = user.wallets[0]
|
||||
|
||||
if not wallet or wallet.deleted:
|
||||
return template_renderer().TemplateResponse(
|
||||
request, "error.html", {"err": "Wallet not found"}, HTTPStatus.NOT_FOUND
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Wallet not found",
|
||||
)
|
||||
context = {
|
||||
"user": user.json(),
|
||||
"wallet": wallet.json(),
|
||||
"wallet_name": wallet.name,
|
||||
"currencies": allowed_currencies(),
|
||||
"service_fee": settings.lnbits_service_fee,
|
||||
"service_fee_max": settings.lnbits_service_fee_max,
|
||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
||||
}
|
||||
|
||||
resp = template_renderer().TemplateResponse(
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"core/wallet.html",
|
||||
{
|
||||
"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",
|
||||
},
|
||||
{**context, "ajax": _is_ajax_request(request)},
|
||||
)
|
||||
resp.set_cookie("lnbits_last_active_wallet", wallet.id)
|
||||
return resp
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
@@ -214,6 +211,7 @@ async def account(
|
||||
"core/account.html",
|
||||
{
|
||||
"user": user.json(),
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -327,6 +325,7 @@ 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),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -365,6 +364,7 @@ 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,6 +381,7 @@ 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),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -395,6 +396,19 @@ 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),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -411,23 +425,22 @@ async def hex_to_uuid4(hex_value: str):
|
||||
|
||||
|
||||
@generic_router.get("/lnurlwallet", response_class=RedirectResponse)
|
||||
async def lnurlwallet(request: Request):
|
||||
async def lnurlwallet(request: Request, lightning: str = ""):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
lightning_param = request.query_params.get("lightning")
|
||||
if not lightning_param:
|
||||
if not lightning:
|
||||
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_param)
|
||||
lnurl = lnurl_decode(lightning)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
||||
check_callback_url(lnurl)
|
||||
res1 = await client.get(lnurl, timeout=2)
|
||||
res1.raise_for_status()
|
||||
data1 = res1.json()
|
||||
@@ -442,19 +455,23 @@ async def lnurlwallet(request: Request):
|
||||
detail="Invalid lnurl. Expected maxWithdrawable",
|
||||
)
|
||||
account = await create_user_account()
|
||||
wallet = await create_wallet(user_id=account.id)
|
||||
_, payment_request = await create_invoice(
|
||||
wallet = account.wallets[0]
|
||||
payment = 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_request}
|
||||
params = {"k1": data1.get("k1"), "pr": payment.bolt11}
|
||||
callback = url + ("&" if urlparse(url).query else "?") + urlencode(params)
|
||||
|
||||
res2 = await client.get(callback, timeout=2)
|
||||
res2 = await client.get(callback, timeout=5)
|
||||
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"
|
||||
|
||||
@@ -95,6 +95,14 @@ 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),
|
||||
@@ -125,7 +133,17 @@ async def api_delete_channel(
|
||||
)
|
||||
|
||||
|
||||
@node_router.get("/payments", response_model=Page[NodePayment])
|
||||
@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")
|
||||
async def api_get_payments(
|
||||
node: Node = Depends(require_node),
|
||||
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
|
||||
@@ -138,7 +156,7 @@ async def api_get_payments(
|
||||
return await node.get_payments(filters)
|
||||
|
||||
|
||||
@node_router.get("/invoices", response_model=Page[NodeInvoice])
|
||||
@node_router.get("/invoices")
|
||||
async def api_get_invoices(
|
||||
node: Node = Depends(require_node),
|
||||
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
|
||||
@@ -151,7 +169,7 @@ async def api_get_invoices(
|
||||
return await node.get_invoices(filters)
|
||||
|
||||
|
||||
@node_router.get("/peers", response_model=List[NodePeerInfo])
|
||||
@node_router.get("/peers")
|
||||
async def api_get_peers(node: Node = Depends(require_node)) -> List[NodePeerInfo]:
|
||||
return await node.get_peers()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import ssl
|
||||
from http import HTTPStatus
|
||||
from math import ceil
|
||||
from typing import List, Optional
|
||||
@@ -16,6 +17,10 @@ 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,
|
||||
@@ -23,18 +28,33 @@ 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 filter_dict_keys, generate_filter_params_openapi
|
||||
from lnbits.helpers import (
|
||||
check_callback_url,
|
||||
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
|
||||
@@ -93,6 +113,64 @@ 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.UNAUTHORIZED,
|
||||
detail="Missing wallet id.",
|
||||
)
|
||||
wallet_filter = next(
|
||||
(f for f in filters.filters if f.field == "wallet_id"), None
|
||||
)
|
||||
if not wallet_filter:
|
||||
raise exc
|
||||
wallet_id = list((wallet_filter.values or {}).values())
|
||||
if len(wallet_id) == 0:
|
||||
raise exc
|
||||
if not user.get_wallet(wallet_id[0]):
|
||||
raise exc
|
||||
return await get_payments_daily_stats(filters)
|
||||
|
||||
|
||||
@payment_router.get(
|
||||
"/paginated",
|
||||
name="Payment List",
|
||||
@@ -105,11 +183,14 @@ 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
|
||||
|
||||
|
||||
@@ -155,6 +236,7 @@ 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},
|
||||
@@ -175,6 +257,23 @@ 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),
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def api_all_payments_paginated(
|
||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||
):
|
||||
return await get_payments_paginated(
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
|
||||
@payment_router.post(
|
||||
"",
|
||||
summary="Create or pay an invoice",
|
||||
@@ -250,6 +349,7 @@ 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},
|
||||
@@ -258,7 +358,8 @@ async def api_payments_pay_lnurl(
|
||||
if r.is_error:
|
||||
raise httpx.ConnectError("LNURL callback connection error")
|
||||
r.raise_for_status()
|
||||
except (httpx.ConnectError, httpx.RequestError) as exc:
|
||||
except (httpx.HTTPError, ssl.SSLError) as exc:
|
||||
logger.warning(exc)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Failed to connect to {domain}.",
|
||||
@@ -366,7 +467,6 @@ 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
|
||||
@@ -375,6 +475,7 @@ 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(
|
||||
|
||||
@@ -31,9 +31,11 @@ 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,
|
||||
@@ -102,16 +104,24 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
||||
)
|
||||
account.validate_fields()
|
||||
account.hash_password(data.password)
|
||||
user = await create_user_account_no_ckeck(account)
|
||||
user = await create_user_account_no_ckeck(account, default_exts=data.extensions)
|
||||
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) -> CreateUser:
|
||||
async def api_update_user(
|
||||
user_id: str, data: CreateUser, user: User = Depends(check_admin)
|
||||
) -> 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."
|
||||
@@ -253,13 +263,22 @@ 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) -> SimpleStatus:
|
||||
async def api_users_delete_user_wallet(
|
||||
user_id: str, wallet: str, user: User = Depends(check_admin)
|
||||
) -> 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)
|
||||
@@ -277,4 +296,14 @@ 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.")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
@@ -8,13 +9,10 @@ from fastapi import (
|
||||
HTTPException,
|
||||
)
|
||||
|
||||
from lnbits.core.models import (
|
||||
CreateWallet,
|
||||
KeyType,
|
||||
Wallet,
|
||||
)
|
||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
check_user_exists,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
@@ -56,9 +54,25 @@ 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:
|
||||
@@ -66,18 +80,24 @@ 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_router.delete("/{wallet_id}")
|
||||
async def api_delete_wallet(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
wallet_id: str, user: User = Depends(check_user_exists)
|
||||
) -> 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.wallet.user,
|
||||
wallet_id=wallet.wallet.id,
|
||||
user_id=wallet.user,
|
||||
wallet_id=wallet.id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ from lnbits.core.models import (
|
||||
CreateWebPushSubscription,
|
||||
WebPushSubscription,
|
||||
)
|
||||
from lnbits.core.models.users import User
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
require_admin_key,
|
||||
check_user_exists,
|
||||
)
|
||||
|
||||
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,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> WebPushSubscription:
|
||||
try:
|
||||
subscription = json.loads(data.subscription)
|
||||
endpoint = subscription["endpoint"]
|
||||
host = urlparse(str(request.url)).netloc
|
||||
|
||||
subscription = await get_webpush_subscription(endpoint, wallet.wallet.user)
|
||||
subscription = await get_webpush_subscription(endpoint, user.id)
|
||||
if subscription:
|
||||
return subscription
|
||||
else:
|
||||
return await create_webpush_subscription(
|
||||
endpoint,
|
||||
wallet.wallet.user,
|
||||
user.id,
|
||||
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,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
try:
|
||||
endpoint = unquote(
|
||||
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
|
||||
)
|
||||
count = await delete_webpush_subscription(endpoint, wallet.wallet.user)
|
||||
count = await delete_webpush_subscription(endpoint, user.id)
|
||||
return {"count": count}
|
||||
except Exception as exc:
|
||||
logger.debug(exc)
|
||||
|
||||
+59
-41
@@ -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, Optional, TypeVar, Union, get_origin
|
||||
from typing import Any, Generic, Literal, TypeVar, get_origin
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, ValidationError, root_validator
|
||||
@@ -22,6 +22,13 @@ 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://"):
|
||||
@@ -58,8 +65,8 @@ def get_placeholder(model: Any, field: str) -> str:
|
||||
|
||||
|
||||
class Compat:
|
||||
type: Optional[str] = "<inherited>"
|
||||
schema: Optional[str] = "<inherited>"
|
||||
type: str | None = "<inherited>"
|
||||
schema: str | None = "<inherited>"
|
||||
|
||||
def interval_seconds(self, seconds: int) -> str:
|
||||
if self.type in {POSTGRES, COCKROACH}:
|
||||
@@ -75,6 +82,18 @@ 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}:
|
||||
@@ -148,8 +167,8 @@ class Connection(Compat):
|
||||
async def fetchall(
|
||||
self,
|
||||
query: str,
|
||||
values: Optional[dict] = None,
|
||||
model: Optional[type[TModel]] = None,
|
||||
values: dict | None = None,
|
||||
model: type[TModel] | None = None,
|
||||
) -> list[TModel]:
|
||||
params = self.rewrite_values(values) if values else {}
|
||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||
@@ -164,8 +183,8 @@ class Connection(Compat):
|
||||
async def fetchone(
|
||||
self,
|
||||
query: str,
|
||||
values: Optional[dict] = None,
|
||||
model: Optional[type[TModel]] = None,
|
||||
values: dict | None = None,
|
||||
model: type[TModel] | None = None,
|
||||
) -> TModel:
|
||||
params = self.rewrite_values(values) if values else {}
|
||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||
@@ -192,11 +211,11 @@ class Connection(Compat):
|
||||
async def fetch_page(
|
||||
self,
|
||||
query: str,
|
||||
where: Optional[list[str]] = None,
|
||||
values: Optional[dict] = None,
|
||||
filters: Optional[Filters] = None,
|
||||
model: Optional[type[TModel]] = None,
|
||||
group_by: Optional[list[str]] = None,
|
||||
where: list[str] | None = None,
|
||||
values: dict | None = None,
|
||||
filters: Filters | None = None,
|
||||
model: type[TModel] | None = None,
|
||||
group_by: list[str] | None = None,
|
||||
) -> Page[TModel]:
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
@@ -249,7 +268,7 @@ class Connection(Compat):
|
||||
total=count,
|
||||
)
|
||||
|
||||
async def execute(self, query: str, values: Optional[dict] = None):
|
||||
async def execute(self, query: str, values: dict | None = None):
|
||||
params = self.rewrite_values(values) if values else {}
|
||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||
await self.conn.commit()
|
||||
@@ -331,8 +350,8 @@ class Database(Compat):
|
||||
async def fetchall(
|
||||
self,
|
||||
query: str,
|
||||
values: Optional[dict] = None,
|
||||
model: Optional[type[TModel]] = None,
|
||||
values: dict | None = None,
|
||||
model: type[TModel] | None = None,
|
||||
) -> list[TModel]:
|
||||
async with self.connect() as conn:
|
||||
return await conn.fetchall(query, values, model)
|
||||
@@ -340,8 +359,8 @@ class Database(Compat):
|
||||
async def fetchone(
|
||||
self,
|
||||
query: str,
|
||||
values: Optional[dict] = None,
|
||||
model: Optional[type[TModel]] = None,
|
||||
values: dict | None = None,
|
||||
model: type[TModel] | None = None,
|
||||
) -> TModel:
|
||||
async with self.connect() as conn:
|
||||
return await conn.fetchone(query, values, model)
|
||||
@@ -359,16 +378,16 @@ class Database(Compat):
|
||||
async def fetch_page(
|
||||
self,
|
||||
query: str,
|
||||
where: Optional[list[str]] = None,
|
||||
values: Optional[dict] = None,
|
||||
filters: Optional[Filters] = None,
|
||||
model: Optional[type[TModel]] = None,
|
||||
group_by: Optional[list[str]] = None,
|
||||
where: list[str] | None = None,
|
||||
values: dict | None = None,
|
||||
filters: Filters | None = None,
|
||||
model: type[TModel] | None = None,
|
||||
group_by: list[str] | None = 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: Optional[dict] = None):
|
||||
async def execute(self, query: str, values: dict | None = None):
|
||||
async with self.connect() as conn:
|
||||
return await conn.execute(query, values)
|
||||
|
||||
@@ -426,7 +445,7 @@ class Operator(Enum):
|
||||
|
||||
class FilterModel(BaseModel):
|
||||
__search_fields__: list[str] = []
|
||||
__sort_fields__: Optional[list[str]] = None
|
||||
__sort_fields__: list[str] | None = None
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -442,8 +461,8 @@ class Page(BaseModel, Generic[T]):
|
||||
class Filter(BaseModel, Generic[TFilterModel]):
|
||||
field: str
|
||||
op: Operator = Operator.EQ
|
||||
model: Optional[type[TFilterModel]]
|
||||
values: Optional[dict] = None
|
||||
model: type[TFilterModel] | None
|
||||
values: dict | None = None
|
||||
|
||||
@classmethod
|
||||
def parse_query(
|
||||
@@ -498,15 +517,15 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
"""
|
||||
|
||||
filters: list[Filter[TFilterModel]] = []
|
||||
search: Optional[str] = None
|
||||
search: str | None = None
|
||||
|
||||
offset: Optional[int] = None
|
||||
limit: Optional[int] = None
|
||||
offset: int | None = None
|
||||
limit: int | None = None
|
||||
|
||||
sortby: Optional[str] = None
|
||||
direction: Optional[Literal["asc", "desc"]] = None
|
||||
sortby: str | None = None
|
||||
direction: Literal["asc", "desc"] | None = None
|
||||
|
||||
model: Optional[type[TFilterModel]] = None
|
||||
model: type[TFilterModel] | None = None
|
||||
|
||||
@root_validator(pre=True)
|
||||
def validate_sortby(cls, values):
|
||||
@@ -528,18 +547,17 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
stmt += f"OFFSET {self.offset}"
|
||||
return stmt
|
||||
|
||||
def where(self, where_stmts: Optional[list[str]] = None) -> str:
|
||||
def where(self, where_stmts: list[str] | None = 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:
|
||||
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 self.search and self.model and self.model.__search_fields__:
|
||||
where_stmts.append(
|
||||
f"lower(concat({', '.join(self.model.__search_fields__)})) LIKE :search"
|
||||
)
|
||||
|
||||
if where_stmts:
|
||||
return "WHERE " + " AND ".join(where_stmts)
|
||||
return ""
|
||||
@@ -549,7 +567,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
||||
return ""
|
||||
|
||||
def values(self, values: Optional[dict] = None) -> dict:
|
||||
def values(self, values: dict | None = None) -> dict:
|
||||
if not values:
|
||||
values = {}
|
||||
if self.filters:
|
||||
@@ -623,7 +641,7 @@ def model_to_dict(model: BaseModel) -> dict:
|
||||
return _dict
|
||||
|
||||
|
||||
def dict_to_submodel(model: type[TModel], value: Union[dict, str]) -> Optional[TModel]:
|
||||
def dict_to_submodel(model: type[TModel], value: dict | str) -> TModel | None:
|
||||
"""convert a dictionary or JSON string to a Pydantic model"""
|
||||
if isinstance(value, str):
|
||||
if value == "null":
|
||||
|
||||
+77
-27
@@ -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, OAuth2PasswordBearer
|
||||
from fastapi.security import APIKeyHeader, APIKeyQuery, HTTPBearer, OAuth2PasswordBearer
|
||||
from fastapi.security.base import SecurityBase
|
||||
from loguru import logger
|
||||
from pydantic.types import UUID4
|
||||
@@ -18,6 +18,7 @@ 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,
|
||||
@@ -27,10 +28,18 @@ 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)
|
||||
|
||||
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",
|
||||
)
|
||||
api_key_header = APIKeyHeader(
|
||||
name="X-API-KEY",
|
||||
auto_error=False,
|
||||
@@ -130,8 +139,9 @@ 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
|
||||
return header_access_token or cookie_access_token or bearer_access_token
|
||||
|
||||
|
||||
async def check_user_exists(
|
||||
@@ -140,9 +150,13 @@ async def check_user_exists(
|
||||
usr: Optional[UUID4] = None,
|
||||
) -> User:
|
||||
if access_token:
|
||||
account = await _get_account_from_token(access_token)
|
||||
account = await _get_account_from_token(access_token, r["path"], r["method"])
|
||||
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.")
|
||||
|
||||
@@ -161,13 +175,14 @@ 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)
|
||||
account = await _get_account_from_token(access_token, r["path"], r["method"])
|
||||
return account.id if account else None
|
||||
|
||||
return None
|
||||
@@ -188,6 +203,10 @@ async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User
|
||||
raise HTTPException(
|
||||
HTTPStatus.UNAUTHORIZED, "User not authorized. No admin privileges."
|
||||
)
|
||||
if not user.has_password:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "Admin users must have credentials configured."
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
@@ -197,6 +216,10 @@ async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) ->
|
||||
raise HTTPException(
|
||||
HTTPStatus.UNAUTHORIZED, "User not authorized. No super user privileges."
|
||||
)
|
||||
if not user.has_password:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "Super user must have credentials configured."
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@@ -247,19 +270,18 @@ async def check_user_extension_access(
|
||||
success=False, message=f"User not authorized for extension '{ext_id}'."
|
||||
)
|
||||
|
||||
if settings.is_extension_id(ext_id):
|
||||
if settings.is_installed_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"User extension '{ext_id}' not enabled."
|
||||
success=False, message=f"Extension '{ext_id}' not enabled."
|
||||
)
|
||||
|
||||
return SimpleStatus(success=True, message="OK")
|
||||
|
||||
|
||||
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]
|
||||
async def _check_user_extension_access(user_id: str, path: str):
|
||||
ext_id = path_segments(path)[0]
|
||||
status = await check_user_extension_access(user_id, ext_id)
|
||||
if not status.success:
|
||||
raise HTTPException(
|
||||
@@ -268,16 +290,14 @@ async def _check_user_extension_access(user_id: str, current_path: str):
|
||||
)
|
||||
|
||||
|
||||
async def _get_account_from_token(access_token) -> Optional[Account]:
|
||||
async def _get_account_from_token(
|
||||
access_token: str, path: str, method: str
|
||||
) -> Optional[Account]:
|
||||
try:
|
||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
||||
user = await _get_user_from_jwt_payload(payload)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
HTTPStatus.UNAUTHORIZED, "Data missing for access token."
|
||||
)
|
||||
|
||||
return user
|
||||
return await _get_account_from_jwt_payload(
|
||||
AccessTokenPayload(**payload), path, method
|
||||
)
|
||||
|
||||
except jwt.ExpiredSignatureError as exc:
|
||||
raise HTTPException(
|
||||
@@ -288,11 +308,41 @@ async def _get_account_from_token(access_token) -> Optional[Account]:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid access token.") from exc
|
||||
|
||||
|
||||
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
|
||||
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.")
|
||||
|
||||
+54
-3
@@ -8,7 +8,9 @@ from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse, RedirectResponse, Response
|
||||
from loguru import logger
|
||||
|
||||
from .helpers import template_renderer
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .helpers import path_segments, template_renderer
|
||||
|
||||
|
||||
class PaymentError(Exception):
|
||||
@@ -26,9 +28,11 @@ 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 "text/html" not in request.headers.get("accept", ""):
|
||||
|
||||
if not _is_browser_request(request):
|
||||
return None
|
||||
|
||||
if (
|
||||
@@ -49,7 +53,14 @@ def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
||||
)
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
request, "error.html", {"err": f"Error: {exc!s}"}, status_code
|
||||
request,
|
||||
"error.html",
|
||||
{
|
||||
"err": f"Error: {exc!s}",
|
||||
"status_code": int(status_code),
|
||||
"message": str(exc).split(":")[-1].strip(),
|
||||
},
|
||||
status_code,
|
||||
)
|
||||
|
||||
|
||||
@@ -119,3 +130,43 @@ 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
|
||||
|
||||
+102
-14
@@ -5,10 +5,12 @@ 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
|
||||
|
||||
@@ -69,12 +71,17 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
||||
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["LNBITS_DENOMINATION"] = (
|
||||
settings.lnbits_denomination
|
||||
if settings.lnbits_backend_wallet_class == "FakeWallet"
|
||||
else "sats"
|
||||
)
|
||||
t.env.globals["SITE_TAGLINE"] = settings.lnbits_site_tagline
|
||||
t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description
|
||||
t.env.globals["LNBITS_SHOW_HOME_PAGE_ELEMENTS"] = (
|
||||
settings.lnbits_show_home_page_elements
|
||||
)
|
||||
t.env.globals["LNBITS_CUSTOM_IMAGE"] = settings.lnbits_custom_image
|
||||
t.env.globals["LNBITS_CUSTOM_BADGE"] = settings.lnbits_custom_badge
|
||||
t.env.globals["LNBITS_CUSTOM_BADGE_COLOR"] = settings.lnbits_custom_badge_color
|
||||
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.lnbits_theme_options
|
||||
@@ -95,11 +102,26 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
||||
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)
|
||||
t.env.globals["EXTENSIONS"] = list(settings.lnbits_installed_extensions_ids)
|
||||
|
||||
if settings.lnbits_custom_logo:
|
||||
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
|
||||
|
||||
if settings.lnbits_default_reaction:
|
||||
t.env.globals["USE_DEFAULT_REACTION"] = settings.lnbits_default_reaction
|
||||
|
||||
if settings.lnbits_default_theme:
|
||||
t.env.globals["USE_DEFAULT_THEME"] = settings.lnbits_default_theme
|
||||
|
||||
if settings.lnbits_default_border:
|
||||
t.env.globals["USE_DEFAULT_BORDER"] = settings.lnbits_default_border
|
||||
|
||||
if settings.lnbits_default_gradient:
|
||||
t.env.globals["USE_DEFAULT_GRADIENT"] = settings.lnbits_default_gradient
|
||||
|
||||
if settings.lnbits_default_bgimage:
|
||||
t.env.globals["USE_DEFAULT_BGIMAGE"] = settings.lnbits_default_bgimage
|
||||
|
||||
if settings.bundle_assets:
|
||||
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
||||
t.env.globals["INCLUDED_CSS"] = ["bundle.min.css"]
|
||||
@@ -198,27 +220,42 @@ def is_valid_pubkey(pubkey: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
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})
|
||||
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
|
||||
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
||||
|
||||
|
||||
def encrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
|
||||
"""Encrypt message with the internal secret key"""
|
||||
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
|
||||
"""
|
||||
if not m:
|
||||
return None
|
||||
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode())
|
||||
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode(), urlsafe=urlsafe)
|
||||
|
||||
|
||||
def decrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
|
||||
"""Decrypt message with the internal secret key"""
|
||||
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
|
||||
"""
|
||||
if not m:
|
||||
return None
|
||||
return AESCipher(key=settings.auth_secret_key).decrypt(m)
|
||||
return AESCipher(key=settings.auth_secret_key).decrypt(m, urlsafe=urlsafe)
|
||||
|
||||
|
||||
def filter_dict_keys(data: dict, filter_keys: Optional[list[str]]) -> dict:
|
||||
@@ -256,6 +293,15 @@ 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:
|
||||
@@ -270,3 +316,45 @@ 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()
|
||||
|
||||
+10
-5
@@ -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 template_renderer
|
||||
from lnbits.helpers import normalize_path, template_renderer
|
||||
from lnbits.settings import settings
|
||||
|
||||
|
||||
@@ -82,7 +82,11 @@ class InstalledExtensionMiddleware:
|
||||
return HTMLResponse(
|
||||
status_code=status_code,
|
||||
content=template_renderer()
|
||||
.TemplateResponse(Request(scope), "error.html", {"err": msg})
|
||||
.TemplateResponse(
|
||||
Request(scope),
|
||||
"error.html",
|
||||
{"err": msg, "status_code": status_code, "message": msg},
|
||||
)
|
||||
.body,
|
||||
)
|
||||
|
||||
@@ -114,7 +118,6 @@ class ExtensionsRedirectMiddleware:
|
||||
|
||||
|
||||
class AuditMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
def __init__(self, app: ASGIApp, audit_queue: asyncio.Queue) -> None:
|
||||
super().__init__(app)
|
||||
self.audit_queue = audit_queue
|
||||
@@ -131,8 +134,9 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
||||
assert response
|
||||
return response
|
||||
finally:
|
||||
duration = (datetime.now(timezone.utc) - start_time).total_seconds()
|
||||
await self._log_audit(request, response, duration, request_details)
|
||||
if 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,
|
||||
@@ -156,6 +160,7 @@ 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]
|
||||
|
||||
|
||||
+54
-40
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -15,10 +15,10 @@ if TYPE_CHECKING:
|
||||
|
||||
class NodePeerInfo(BaseModel):
|
||||
id: str
|
||||
alias: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
last_timestamp: Optional[int] = None
|
||||
addresses: Optional[list[str]] = None
|
||||
alias: str | None = None
|
||||
color: str | None = None
|
||||
last_timestamp: int | None = None
|
||||
addresses: list[str] | None = None
|
||||
|
||||
|
||||
class ChannelState(Enum):
|
||||
@@ -38,22 +38,29 @@ 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
|
||||
name: Optional[str]
|
||||
color: Optional[str]
|
||||
# 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
|
||||
|
||||
|
||||
class ChannelStats(BaseModel):
|
||||
counts: dict[ChannelState, int]
|
||||
avg_size: int
|
||||
biggest_size: Optional[int]
|
||||
smallest_size: Optional[int]
|
||||
biggest_size: int | None
|
||||
smallest_size: int | None
|
||||
total_capacity: int
|
||||
|
||||
@classmethod
|
||||
@@ -88,9 +95,9 @@ class ChannelStats(BaseModel):
|
||||
|
||||
class NodeFees(BaseModel):
|
||||
total_msat: int
|
||||
daily_msat: Optional[int] = None
|
||||
weekly_msat: Optional[int] = None
|
||||
monthly_msat: Optional[int] = None
|
||||
daily_msat: int | None = None
|
||||
weekly_msat: int | None = None
|
||||
monthly_msat: int | None = None
|
||||
|
||||
|
||||
class PublicNodeInfo(BaseModel):
|
||||
@@ -114,25 +121,25 @@ class NodeInfoResponse(PublicNodeInfo):
|
||||
class NodePayment(BaseModel):
|
||||
pending: bool
|
||||
amount: int
|
||||
fee: Optional[int] = None
|
||||
memo: Optional[str] = None
|
||||
fee: int | None = None
|
||||
memo: str | None = None
|
||||
time: int
|
||||
bolt11: Optional[str] = None
|
||||
preimage: Optional[str]
|
||||
bolt11: str | None = None
|
||||
preimage: str | None
|
||||
payment_hash: str
|
||||
expiry: Optional[float] = None
|
||||
destination: Optional[NodePeerInfo] = None
|
||||
expiry: float | None = None
|
||||
destination: NodePeerInfo | None = None
|
||||
|
||||
|
||||
class NodeInvoice(BaseModel):
|
||||
pending: bool
|
||||
amount: int
|
||||
memo: Optional[str]
|
||||
memo: str | None
|
||||
bolt11: str
|
||||
preimage: Optional[str]
|
||||
preimage: str | None
|
||||
payment_hash: str
|
||||
paid_at: Optional[int] = None
|
||||
expiry: Optional[int] = None
|
||||
paid_at: int | None = None
|
||||
expiry: int | None = None
|
||||
|
||||
|
||||
class NodeInvoiceFilters(FilterModel):
|
||||
@@ -144,11 +151,10 @@ class NodePaymentsFilters(FilterModel):
|
||||
|
||||
|
||||
class Node(ABC):
|
||||
wallet: Wallet
|
||||
|
||||
def __init__(self, wallet: Wallet):
|
||||
self.wallet = wallet
|
||||
self.id: Optional[str] = None
|
||||
self.id: str | None = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -161,7 +167,7 @@ class Node(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def _get_id(self) -> str:
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_peers(self) -> list[NodePeerInfo]:
|
||||
peer_ids = await self.get_peer_ids()
|
||||
@@ -169,15 +175,15 @@ class Node(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_peer_ids(self) -> list[str]:
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def connect_peer(self, uri: str):
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def disconnect_peer(self, peer_id: str):
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def _get_peer_info(self, peer_id: str) -> NodePeerInfo:
|
||||
@@ -197,27 +203,35 @@ class Node(ABC):
|
||||
self,
|
||||
peer_id: str,
|
||||
local_amount: int,
|
||||
push_amount: Optional[int] = None,
|
||||
fee_rate: Optional[int] = None,
|
||||
push_amount: int | None = None,
|
||||
fee_rate: int | None = None,
|
||||
) -> ChannelPoint:
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def close_channel(
|
||||
self,
|
||||
short_id: Optional[str] = None,
|
||||
point: Optional[ChannelPoint] = None,
|
||||
short_id: str | None = None,
|
||||
point: ChannelPoint | None = None,
|
||||
force: bool = False,
|
||||
):
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def get_channels(self) -> list[NodeChannel]:
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def get_info(self) -> NodeInfoResponse:
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_public_info(self) -> PublicNodeInfo:
|
||||
info = await self.get_info()
|
||||
@@ -227,10 +241,10 @@ class Node(ABC):
|
||||
async def get_payments(
|
||||
self, filters: Filters[NodePaymentsFilters]
|
||||
) -> Page[NodePayment]:
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def get_invoices(
|
||||
self, filters: Filters[NodeInvoiceFilters]
|
||||
) -> Page[NodeInvoice]:
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
+57
-33
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -66,6 +66,28 @@ 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
|
||||
@@ -97,8 +119,8 @@ class CoreLightningNode(Node):
|
||||
self,
|
||||
peer_id: str,
|
||||
local_amount: int,
|
||||
push_amount: Optional[int] = None,
|
||||
fee_rate: Optional[int] = None,
|
||||
push_amount: int | None = None,
|
||||
fee_rate: int | None = None,
|
||||
) -> ChannelPoint:
|
||||
try:
|
||||
result = await self.ln_rpc(
|
||||
@@ -151,8 +173,8 @@ class CoreLightningNode(Node):
|
||||
@catch_rpc_errors
|
||||
async def close_channel(
|
||||
self,
|
||||
short_id: Optional[str] = None,
|
||||
point: Optional[ChannelPoint] = None,
|
||||
short_id: str | None = None,
|
||||
point: ChannelPoint | None = None,
|
||||
force: bool = False,
|
||||
):
|
||||
if not short_id:
|
||||
@@ -202,48 +224,45 @@ 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]:
|
||||
funds = await self.ln_rpc("listfunds")
|
||||
channels = await self.ln_rpc("listpeerchannels")
|
||||
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_output"],
|
||||
output_index=ch["funding_outnum"],
|
||||
),
|
||||
peer_id=ch["peer_id"],
|
||||
balance=ChannelBalance(
|
||||
local_msat=ch["our_amount_msat"],
|
||||
remote_msat=ch["amount_msat"] - ch["our_amount_msat"],
|
||||
total_msat=ch["amount_msat"],
|
||||
local_msat=ch["spendable_msat"],
|
||||
remote_msat=ch["receivable_msat"],
|
||||
total_msat=ch["total_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=(
|
||||
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
|
||||
)
|
||||
)
|
||||
),
|
||||
state=self._parse_state(ch["state"]),
|
||||
)
|
||||
for ch in funds["channels"]
|
||||
for ch in channels["channels"]
|
||||
]
|
||||
|
||||
@catch_rpc_errors
|
||||
@@ -285,14 +304,19 @@ class CoreLightningNode(Node):
|
||||
return [
|
||||
NodePayment(
|
||||
bolt11=pay.get("bolt11"),
|
||||
amount=pay["amount_msat"],
|
||||
fee=int(pay["amount_msat"]) - int(pay["amount_sent_msat"]),
|
||||
amount=pay.get("amount_msat", 0),
|
||||
fee=int(pay.get("amount_msat", 0))
|
||||
- int(pay.get("amount_sent_msat", 0)),
|
||||
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["destination"]),
|
||||
destination=(
|
||||
await self.get_peer_info(pay.get("destination"))
|
||||
if pay.get("destination")
|
||||
else None
|
||||
),
|
||||
)
|
||||
for pay in reversed(result["pays"])
|
||||
if pay["status"] != "failed"
|
||||
|
||||
+98
-17
@@ -4,7 +4,7 @@ import asyncio
|
||||
import base64
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastapi import HTTPException
|
||||
from httpx import HTTPStatusError
|
||||
@@ -41,6 +41,14 @@ 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(
|
||||
@@ -52,9 +60,7 @@ def _parse_channel_point(raw: str) -> ChannelPoint:
|
||||
class LndRestNode(Node):
|
||||
wallet: LndRestWallet
|
||||
|
||||
async def request(
|
||||
self, method: str, path: str, json: Optional[dict] = None, **kwargs
|
||||
):
|
||||
async def request(self, method: str, path: str, json: dict | None = None, **kwargs):
|
||||
response = await self.wallet.client.request(
|
||||
method, f"{self.wallet.endpoint}{path}", json=json, **kwargs
|
||||
)
|
||||
@@ -123,21 +129,18 @@ class LndRestNode(Node):
|
||||
self,
|
||||
peer_id: str,
|
||||
local_amount: int,
|
||||
push_amount: Optional[int] = None,
|
||||
fee_rate: Optional[int] = None,
|
||||
push_amount: int | None = None,
|
||||
fee_rate: int | None = None,
|
||||
) -> ChannelPoint:
|
||||
response = await self.request(
|
||||
"POST",
|
||||
"/v1/channels",
|
||||
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,
|
||||
}
|
||||
),
|
||||
json={
|
||||
"node_pubkey": _encode_bytes(peer_id),
|
||||
"sat_per_vbyte": fee_rate,
|
||||
"local_funding_amount": local_amount,
|
||||
"push_sat": push_amount,
|
||||
},
|
||||
)
|
||||
return ChannelPoint(
|
||||
# WHY IS THIS REVERSED?!
|
||||
@@ -171,8 +174,8 @@ class LndRestNode(Node):
|
||||
|
||||
async def close_channel(
|
||||
self,
|
||||
short_id: Optional[str] = None,
|
||||
point: Optional[ChannelPoint] = None,
|
||||
short_id: str | None = None,
|
||||
point: ChannelPoint | None = None,
|
||||
force: bool = False,
|
||||
):
|
||||
if short_id:
|
||||
@@ -184,6 +187,81 @@ 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"),
|
||||
@@ -203,6 +281,7 @@ 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"]),
|
||||
@@ -222,6 +301,7 @@ 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,
|
||||
@@ -239,6 +319,7 @@ 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"],
|
||||
|
||||
+216
-168
@@ -4,15 +4,16 @@ 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 time import time
|
||||
from typing import Any, Optional
|
||||
from pathlib import Path
|
||||
from time import gmtime, strftime, time
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
||||
|
||||
@@ -76,7 +77,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]) -> Optional[RedirectPath]:
|
||||
def find_in_conflict(self, others: list[RedirectPath]) -> RedirectPath | None:
|
||||
for other in others:
|
||||
if self.in_conflict(other):
|
||||
return other
|
||||
@@ -148,11 +149,11 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
||||
lnbits_extensions_redirects: list[RedirectPath] = Field(default=[])
|
||||
|
||||
# list of all extension ids
|
||||
lnbits_all_extensions_ids: set[str] = Field(default=[])
|
||||
lnbits_installed_extensions_ids: set[str] = Field(default=[])
|
||||
|
||||
def find_extension_redirect(
|
||||
self, path: str, req_headers: list[tuple[bytes, bytes]]
|
||||
) -> Optional[RedirectPath]:
|
||||
) -> RedirectPath | None:
|
||||
headers = [(k.decode(), v.decode()) for k, v in req_headers]
|
||||
return next(
|
||||
(
|
||||
@@ -166,8 +167,8 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
||||
def activate_extension_paths(
|
||||
self,
|
||||
ext_id: str,
|
||||
upgrade_hash: Optional[str] = None,
|
||||
ext_redirects: Optional[list[dict]] = None,
|
||||
upgrade_hash: str | None = None,
|
||||
ext_redirects: list[dict] | None = None,
|
||||
):
|
||||
self.lnbits_deactivated_extensions.discard(ext_id)
|
||||
|
||||
@@ -181,7 +182,7 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
||||
if ext_redirects:
|
||||
self._activate_extension_redirects(ext_id, ext_redirects)
|
||||
|
||||
self.lnbits_all_extensions_ids.add(ext_id)
|
||||
self.lnbits_installed_extensions_ids.add(ext_id)
|
||||
|
||||
def deactivate_extension_paths(self, ext_id: str):
|
||||
self.lnbits_deactivated_extensions.add(ext_id)
|
||||
@@ -215,7 +216,6 @@ 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: Optional[str] = Field(
|
||||
lnbits_site_description: str | None = 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: Optional[str] = Field(default=None)
|
||||
lnbits_custom_badge: str | None = Field(default=None)
|
||||
lnbits_custom_badge_color: str = Field(default="warning")
|
||||
lnbits_theme_options: list[str] = Field(
|
||||
default=[
|
||||
@@ -247,33 +247,56 @@ class ThemesSettings(LNbitsSettings):
|
||||
"monochrome",
|
||||
"autumn",
|
||||
"cyber",
|
||||
"flamingo",
|
||||
"bitcoin",
|
||||
]
|
||||
)
|
||||
lnbits_custom_logo: Optional[str] = Field(default=None)
|
||||
lnbits_custom_logo: str | None = Field(default=None)
|
||||
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
|
||||
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: Optional[str] = Field(default=None)
|
||||
lnbits_qr_logo: str = Field(default="/static/images/logos/lnbits.png")
|
||||
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)
|
||||
|
||||
|
||||
class OpsSettings(LNbitsSettings):
|
||||
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
||||
lnbits_hide_api: bool = Field(default=False)
|
||||
lnbits_denomination: str = Field(default="sats")
|
||||
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",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class FeeSettings(LNbitsSettings):
|
||||
|
||||
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_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_service_fee_ignore_internal: bool = Field(default=True)
|
||||
lnbits_service_fee_max: int = Field(default=0)
|
||||
lnbits_service_fee_wallet: Optional[str] = Field(default=None)
|
||||
lnbits_service_fee_wallet: str | None = 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
|
||||
@@ -286,9 +309,9 @@ class FeeSettings(LNbitsSettings):
|
||||
|
||||
|
||||
class ExchangeProvidersSettings(LNbitsSettings):
|
||||
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_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_providers: list[ExchangeRateProvider] = Field(
|
||||
default=[
|
||||
@@ -334,13 +357,6 @@ 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}",
|
||||
@@ -360,25 +376,25 @@ class ExchangeProvidersSettings(LNbitsSettings):
|
||||
|
||||
|
||||
class SecuritySettings(LNbitsSettings):
|
||||
lnbits_rate_limit_no: str = Field(default="200")
|
||||
lnbits_rate_limit_no: int = Field(default=200, ge=0)
|
||||
lnbits_rate_limit_unit: str = Field(default="minute")
|
||||
lnbits_allowed_ips: list[str] = Field(default=[])
|
||||
lnbits_blocked_ips: list[str] = Field(default=[])
|
||||
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_callback_url_rules: list[str] = Field(
|
||||
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
|
||||
)
|
||||
|
||||
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
|
||||
@@ -387,130 +403,161 @@ 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: Optional[str] = Field(default=None)
|
||||
lnbits_admin_key: Optional[str] = Field(default=None)
|
||||
lnbits_invoice_key: Optional[str] = Field(default=None)
|
||||
lnbits_key: str | None = Field(default=None)
|
||||
lnbits_admin_key: str | None = Field(default=None)
|
||||
lnbits_invoice_key: str | None = Field(default=None)
|
||||
|
||||
|
||||
class ClicheFundingSource(LNbitsSettings):
|
||||
cliche_endpoint: Optional[str] = Field(default=None)
|
||||
cliche_endpoint: str | None = Field(default=None)
|
||||
|
||||
|
||||
class CoreLightningFundingSource(LNbitsSettings):
|
||||
corelightning_rpc: Optional[str] = Field(default=None)
|
||||
corelightning_rpc: str | None = Field(default=None)
|
||||
corelightning_pay_command: str = Field(default="pay")
|
||||
clightning_rpc: Optional[str] = Field(default=None)
|
||||
clightning_rpc: str | None = Field(default=None)
|
||||
|
||||
|
||||
class CoreLightningRestFundingSource(LNbitsSettings):
|
||||
corelightning_rest_url: Optional[str] = Field(default=None)
|
||||
corelightning_rest_macaroon: Optional[str] = Field(default=None)
|
||||
corelightning_rest_cert: Optional[str] = Field(default=None)
|
||||
corelightning_rest_url: str | None = Field(default=None)
|
||||
corelightning_rest_macaroon: str | None = Field(default=None)
|
||||
corelightning_rest_cert: str | None = Field(default=None)
|
||||
|
||||
|
||||
class EclairFundingSource(LNbitsSettings):
|
||||
eclair_url: Optional[str] = Field(default=None)
|
||||
eclair_pass: Optional[str] = Field(default=None)
|
||||
eclair_url: str | None = Field(default=None)
|
||||
eclair_pass: str | None = Field(default=None)
|
||||
|
||||
|
||||
class LndRestFundingSource(LNbitsSettings):
|
||||
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_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_route_hints: bool = Field(default=True)
|
||||
lnd_rest_allow_self_payment: bool = Field(default=False)
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
class LndGrpcFundingSource(LNbitsSettings):
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
class LnPayFundingSource(LNbitsSettings):
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
class BlinkFundingSource(LNbitsSettings):
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
class ZBDFundingSource(LNbitsSettings):
|
||||
zbd_api_endpoint: Optional[str] = Field(default="https://api.zebedee.io/v0/")
|
||||
zbd_api_key: Optional[str] = Field(default=None)
|
||||
zbd_api_endpoint: str | None = Field(default="https://api.zebedee.io/v0/")
|
||||
zbd_api_key: str | None = Field(default=None)
|
||||
|
||||
|
||||
class PhoenixdFundingSource(LNbitsSettings):
|
||||
phoenixd_api_endpoint: Optional[str] = Field(default="http://localhost:9740/")
|
||||
phoenixd_api_password: Optional[str] = Field(default=None)
|
||||
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
|
||||
phoenixd_api_password: str | None = Field(default=None)
|
||||
|
||||
|
||||
class AlbyFundingSource(LNbitsSettings):
|
||||
alby_api_endpoint: Optional[str] = Field(default="https://api.getalby.com/")
|
||||
alby_access_token: Optional[str] = Field(default=None)
|
||||
alby_api_endpoint: str | None = Field(default="https://api.getalby.com/")
|
||||
alby_access_token: str | None = Field(default=None)
|
||||
|
||||
|
||||
class OpenNodeFundingSource(LNbitsSettings):
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
class SparkFundingSource(LNbitsSettings):
|
||||
spark_url: Optional[str] = Field(default=None)
|
||||
spark_token: Optional[str] = Field(default=None)
|
||||
spark_url: str | None = Field(default=None)
|
||||
spark_token: str | None = Field(default=None)
|
||||
|
||||
|
||||
class LnTipsFundingSource(LNbitsSettings):
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
class NWCFundingSource(LNbitsSettings):
|
||||
nwc_pairing_url: Optional[str] = Field(default=None)
|
||||
nwc_pairing_url: str | None = Field(default=None)
|
||||
|
||||
|
||||
class BreezSdkFundingSource(LNbitsSettings):
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
class BoltzFundingSource(LNbitsSettings):
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
class LightningSettings(LNbitsSettings):
|
||||
lightning_invoice_expiry: int = Field(default=3600)
|
||||
lightning_invoice_expiry: int = Field(default=3600, gt=0)
|
||||
|
||||
|
||||
class FundingSourcesSettings(
|
||||
@@ -535,11 +582,14 @@ 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: Optional[str] = Field(default=None)
|
||||
lnbits_webpush_privkey: Optional[str] = Field(default=None)
|
||||
lnbits_webpush_pubkey: str | None = Field(default=None)
|
||||
lnbits_webpush_privkey: str | None = Field(default=None)
|
||||
|
||||
|
||||
class NodeUISettings(LNbitsSettings):
|
||||
@@ -573,7 +623,7 @@ class AuthMethods(Enum):
|
||||
|
||||
|
||||
class AuthSettings(LNbitsSettings):
|
||||
auth_token_expire_minutes: int = Field(default=525600)
|
||||
auth_token_expire_minutes: int = Field(default=525600, gt=0)
|
||||
auth_all_methods = [a.value for a in AuthMethods]
|
||||
auth_allowed_methods: list[str] = Field(
|
||||
default=[
|
||||
@@ -583,7 +633,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)
|
||||
auth_credetials_update_threshold: int = Field(default=120, gt=0)
|
||||
|
||||
def is_auth_method_allowed(self, method: AuthMethods):
|
||||
return method.value in self.auth_allowed_methods
|
||||
@@ -615,7 +665,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)
|
||||
lnbits_audit_retention_days: int = Field(default=7, ge=0)
|
||||
|
||||
lnbits_audit_log_ip_address: bool = Field(default=False)
|
||||
lnbits_audit_log_path_params: bool = Field(default=True)
|
||||
@@ -645,9 +695,9 @@ class AuditSettings(LNbitsSettings):
|
||||
|
||||
def audit_http_request(
|
||||
self,
|
||||
http_method: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
http_response_code: Optional[str] = None,
|
||||
http_method: str | None = None,
|
||||
path: str | None = None,
|
||||
http_response_code: str | None = None,
|
||||
) -> bool:
|
||||
if not self.lnbits_audit_enabled:
|
||||
return False
|
||||
@@ -665,24 +715,24 @@ class AuditSettings(LNbitsSettings):
|
||||
|
||||
return True
|
||||
|
||||
def _is_http_request_path_auditable(self, path: Optional[str]):
|
||||
def _is_http_request_path_auditable(self, path: str | None):
|
||||
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:
|
||||
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:
|
||||
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
|
||||
|
||||
return False
|
||||
|
||||
def _is_http_response_code_auditable(
|
||||
self, http_response_code: Optional[str]
|
||||
) -> bool:
|
||||
def _is_http_response_code_auditable(self, http_response_code: str | None) -> bool:
|
||||
if not http_response_code:
|
||||
# No response code means only request filters should apply
|
||||
return True
|
||||
@@ -705,6 +755,7 @@ class EditableSettings(
|
||||
FeeSettings,
|
||||
ExchangeProvidersSettings,
|
||||
SecuritySettings,
|
||||
NotificationsSettings,
|
||||
FundingSourcesSettings,
|
||||
LightningSettings,
|
||||
WebPushSettings,
|
||||
@@ -751,7 +802,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)
|
||||
port: int = Field(default=5000, gt=0)
|
||||
forwarded_allow_ips: str = Field(default="*")
|
||||
lnbits_title: str = Field(default="LNbits API")
|
||||
lnbits_path: str = Field(default=".")
|
||||
@@ -763,19 +814,27 @@ 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")
|
||||
server_startup_time: int = Field(default=time())
|
||||
cleanup_wallets_days: int = Field(default=90)
|
||||
funding_source_max_retries: int = Field(default=4)
|
||||
|
||||
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||
|
||||
@property
|
||||
def has_default_extension_path(self) -> bool:
|
||||
return self.lnbits_extensions_path == "lnbits"
|
||||
|
||||
|
||||
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)
|
||||
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 PersistenceSettings(LNbitsSettings):
|
||||
@@ -822,6 +881,18 @@ 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("_")]
|
||||
@@ -830,12 +901,15 @@ 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,
|
||||
@@ -864,7 +938,7 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
||||
or user_id == self.super_user
|
||||
)
|
||||
|
||||
def is_super_user(self, user_id: Optional[str] = None) -> bool:
|
||||
def is_super_user(self, user_id: str | None = None) -> bool:
|
||||
return user_id == self.super_user
|
||||
|
||||
def is_admin_user(self, user_id: str) -> bool:
|
||||
@@ -873,8 +947,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_extension_id(self, ext_id: str) -> bool:
|
||||
return ext_id in self.lnbits_all_extensions_ids
|
||||
def is_installed_extension_id(self, ext_id: str) -> bool:
|
||||
return ext_id in self.lnbits_installed_extensions_ids
|
||||
|
||||
|
||||
class SuperSettings(EditableSettings):
|
||||
@@ -883,12 +957,12 @@ class SuperSettings(EditableSettings):
|
||||
|
||||
class AdminSettings(EditableSettings):
|
||||
is_super_user: bool
|
||||
lnbits_allowed_funding_sources: Optional[list[str]]
|
||||
lnbits_allowed_funding_sources: list[str] | None
|
||||
|
||||
|
||||
class SettingsField(BaseModel):
|
||||
id: str
|
||||
value: Optional[Any]
|
||||
value: Any | None
|
||||
tag: str = "core"
|
||||
|
||||
|
||||
@@ -905,31 +979,6 @@ 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()
|
||||
|
||||
@@ -938,9 +987,8 @@ settings = Settings()
|
||||
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
||||
|
||||
settings.version = importlib.metadata.version("lnbits")
|
||||
settings.auth_secret_key = (
|
||||
settings.auth_secret_key or sha256(settings.super_user.encode("utf-8")).hexdigest()
|
||||
)
|
||||
|
||||
settings.check_auth_secret_key()
|
||||
|
||||
if not settings.user_agent:
|
||||
settings.user_agent = f"LNbits/{settings.version}"
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+13
-11
File diff suppressed because one or more lines are too long
+174
-7
@@ -1,3 +1,11 @@
|
||||
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 {
|
||||
@@ -10,11 +18,23 @@ 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;
|
||||
}
|
||||
@@ -52,6 +72,14 @@ body[data-theme='classic'].body--dark {
|
||||
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 {
|
||||
@@ -64,22 +92,34 @@ 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: #ff9853 !important;
|
||||
background: #ea611d !important;
|
||||
}
|
||||
[data-theme=bitcoin] .text-primary {
|
||||
color: #ff9853 !important;
|
||||
color: #ea611d !important;
|
||||
}
|
||||
[data-theme=bitcoin] .bg-secondary {
|
||||
background: #ff7353 !important;
|
||||
background: #e56f35 !important;
|
||||
}
|
||||
[data-theme=bitcoin] .text-secondary {
|
||||
color: #ff7353 !important;
|
||||
color: #e56f35 !important;
|
||||
}
|
||||
[data-theme=bitcoin] .bg-dark {
|
||||
background: #2d293b !important;
|
||||
@@ -94,10 +134,10 @@ body[data-theme='bitcoin'].body--dark {
|
||||
color: #333646 !important;
|
||||
}
|
||||
[data-theme=bitcoin] .bg-marginal-bg {
|
||||
background: #2d293b !important;
|
||||
background: #000000 !important;
|
||||
}
|
||||
[data-theme=bitcoin] .text-marginal-bg {
|
||||
color: #2d293b !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
[data-theme=bitcoin] .bg-marginal-text {
|
||||
background: #fff !important;
|
||||
@@ -106,6 +146,14 @@ body[data-theme='bitcoin'].body--dark {
|
||||
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 {
|
||||
@@ -118,11 +166,23 @@ 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;
|
||||
}
|
||||
@@ -160,6 +220,14 @@ body[data-theme='freedom'].body--dark {
|
||||
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 {
|
||||
@@ -172,11 +240,23 @@ 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;
|
||||
}
|
||||
@@ -214,6 +294,14 @@ body[data-theme='cyber'].body--dark {
|
||||
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 {
|
||||
@@ -226,11 +314,23 @@ 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;
|
||||
}
|
||||
@@ -268,6 +368,14 @@ body[data-theme='mint'].body--dark {
|
||||
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 {
|
||||
@@ -280,11 +388,23 @@ 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;
|
||||
}
|
||||
@@ -322,6 +442,14 @@ body[data-theme='autumn'].body--dark {
|
||||
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 {
|
||||
@@ -334,11 +462,23 @@ 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;
|
||||
}
|
||||
@@ -376,6 +516,14 @@ body[data-theme='flamingo'].body--dark {
|
||||
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 {
|
||||
@@ -388,11 +536,23 @@ 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;
|
||||
}
|
||||
@@ -482,6 +642,12 @@ 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 {
|
||||
@@ -535,12 +701,13 @@ video {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.qrcode__wrapper canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: 350px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.qrcode__image {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
+198
-16
@@ -2,8 +2,10 @@ 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',
|
||||
@@ -26,9 +28,11 @@ window.localisation.br = {
|
||||
restart: 'Reiniciar servidor',
|
||||
save: 'Salvar',
|
||||
save_tooltip: 'Salvar suas alterações',
|
||||
topup: 'Recarregar',
|
||||
topup_wallet: 'Recarregar uma carteira',
|
||||
topup_hint: 'Use o ID da carteira para recarregar qualquer carteira',
|
||||
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.',
|
||||
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',
|
||||
@@ -36,14 +40,13 @@ 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',
|
||||
@@ -70,8 +73,6 @@ 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',
|
||||
@@ -88,7 +89,7 @@ window.localisation.br = {
|
||||
pay: 'Pagar',
|
||||
memo: 'Memo',
|
||||
date: 'Data',
|
||||
processing_payment: 'Processando pagamento...',
|
||||
payment_processing: 'Processando pagamento...',
|
||||
not_enough_funds: 'Fundos insuficientes!',
|
||||
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
|
||||
invoice_waiting: 'Fatura aguardando pagamento',
|
||||
@@ -107,10 +108,15 @@ 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?',
|
||||
@@ -119,8 +125,10 @@ 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',
|
||||
@@ -145,15 +153,19 @@ 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',
|
||||
@@ -163,12 +175,6 @@ 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.',
|
||||
@@ -185,7 +191,6 @@ 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',
|
||||
@@ -217,6 +222,7 @@ 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',
|
||||
@@ -225,11 +231,14 @@ 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',
|
||||
@@ -244,14 +253,187 @@ 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',
|
||||
hide_empty_wallets: 'Ocultar carteiras vazias'
|
||||
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'
|
||||
}
|
||||
|
||||
+184
-16
@@ -2,8 +2,10 @@ window.localisation.cn = {
|
||||
confirm: '确定',
|
||||
server: '服务器',
|
||||
theme: '主题',
|
||||
site_customisation: '网站定制',
|
||||
funding: '资金',
|
||||
users: '用户',
|
||||
audit: '审计',
|
||||
apps: '应用程序',
|
||||
channels: '频道',
|
||||
transactions: '交易记录',
|
||||
@@ -26,23 +28,24 @@ window.localisation.cn = {
|
||||
restart: '重新启动服务器',
|
||||
save: '保存',
|
||||
save_tooltip: '保存更改',
|
||||
topup: '充值',
|
||||
topup_wallet: '给钱包充值',
|
||||
topup_hint: '使用钱包ID为任何钱包充值',
|
||||
credit_debit: '信用卡 / 借记卡',
|
||||
credit_hint: '按 Enter 键充值账户',
|
||||
credit_label: '{denomination} 充值',
|
||||
credit_ok:
|
||||
'成功记入/扣除虚拟资金 ({amount} sats)。付款取决于资金来源的实际资金。',
|
||||
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: '删除钱包',
|
||||
@@ -67,8 +70,6 @@ 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: '粘贴请求',
|
||||
@@ -85,7 +86,7 @@ window.localisation.cn = {
|
||||
pay: '付款',
|
||||
memo: '备注',
|
||||
date: '日期',
|
||||
processing_payment: '正在处理支付...',
|
||||
payment_processing: '正在处理支付...',
|
||||
not_enough_funds: '资金不足!',
|
||||
search_by_tag_memo_amount: '按标签、备注、金额搜索',
|
||||
invoice_waiting: '待支付的发票',
|
||||
@@ -104,10 +105,15 @@ 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: '你确定要继续吗?',
|
||||
@@ -116,8 +122,10 @@ window.localisation.cn = {
|
||||
uninstall: '卸载',
|
||||
drop_db: '删除数据',
|
||||
enable: '启用',
|
||||
pay_to_enable: '支付以启用',
|
||||
enable_extension_details: '为当前用户启用扩展程序',
|
||||
disable: '禁用',
|
||||
delete: '删除',
|
||||
installed: '已安装',
|
||||
activated: '已激活',
|
||||
deactivated: '已停用',
|
||||
@@ -137,15 +145,19 @@ 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: '通知',
|
||||
@@ -154,12 +166,7 @@ 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。更新后您将需要手动启用。',
|
||||
@@ -175,7 +182,6 @@ window.localisation.cn = {
|
||||
more: '更多',
|
||||
less: '少',
|
||||
releases: '版本',
|
||||
killswitch: '杀手锏',
|
||||
watchdog: '监控程序',
|
||||
server_logs: '服务器日志',
|
||||
ip_blocker: 'IP 阻止器',
|
||||
@@ -206,6 +212,7 @@ 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登录',
|
||||
@@ -214,11 +221,14 @@ 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: '名字',
|
||||
@@ -233,14 +243,172 @@ 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: '创建新支付',
|
||||
hide_empty_wallets: '隐藏空钱包'
|
||||
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请求详细信息'
|
||||
}
|
||||
|
||||
+196
-19
@@ -2,8 +2,10 @@ 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',
|
||||
@@ -26,23 +28,24 @@ window.localisation.cs = {
|
||||
restart: 'Restartovat server',
|
||||
save: 'Uložit',
|
||||
save_tooltip: 'Uložit změny',
|
||||
topup: 'Dobít',
|
||||
topup_wallet: 'Dobít peněženku',
|
||||
topup_hint: 'Použijte ID peněženky pro dobíjení jakékoliv peněženky',
|
||||
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.',
|
||||
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',
|
||||
@@ -71,8 +74,6 @@ 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',
|
||||
@@ -89,7 +90,7 @@ window.localisation.cs = {
|
||||
pay: 'Platit',
|
||||
memo: 'Poznámka',
|
||||
date: 'Datum',
|
||||
processing_payment: 'Zpracování platby...',
|
||||
payment_processing: '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',
|
||||
@@ -108,10 +109,15 @@ 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?',
|
||||
@@ -120,8 +126,10 @@ 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',
|
||||
@@ -145,15 +153,19 @@ 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',
|
||||
@@ -162,15 +174,6 @@ 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).',
|
||||
@@ -184,7 +187,6 @@ 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',
|
||||
@@ -216,6 +218,7 @@ 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',
|
||||
@@ -224,11 +227,14 @@ 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',
|
||||
@@ -243,14 +249,185 @@ 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',
|
||||
hide_empty_wallets: 'Skrýt prázdné peněženky'
|
||||
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'
|
||||
}
|
||||
|
||||
+201
-16
@@ -2,8 +2,10 @@ 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',
|
||||
@@ -26,9 +28,11 @@ window.localisation.de = {
|
||||
restart: 'Server neu starten',
|
||||
save: 'Speichern',
|
||||
save_tooltip: 'Änderungen speichern',
|
||||
topup: 'Aufladen',
|
||||
topup_wallet: 'Wallet aufladen',
|
||||
topup_hint: 'Nutze die Wallet-ID, um eine beliebige Wallet aufzuladen',
|
||||
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.',
|
||||
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',
|
||||
@@ -36,8 +40,6 @@ 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:
|
||||
@@ -45,6 +47,7 @@ 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',
|
||||
@@ -71,8 +74,6 @@ 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',
|
||||
@@ -90,7 +91,7 @@ window.localisation.de = {
|
||||
pay: 'Zahlen',
|
||||
memo: 'Memo',
|
||||
date: 'Datum',
|
||||
processing_payment: 'Zahlung wird verarbeitet ...',
|
||||
payment_processing: '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',
|
||||
@@ -109,10 +110,16 @@ 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?',
|
||||
@@ -121,8 +128,10 @@ 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',
|
||||
@@ -147,15 +156,19 @@ 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',
|
||||
@@ -164,12 +177,6 @@ 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.',
|
||||
@@ -186,7 +193,7 @@ window.localisation.de = {
|
||||
more: 'mehr',
|
||||
less: 'weniger',
|
||||
releases: 'Veröffentlichungen',
|
||||
killswitch: 'Killswitch',
|
||||
|
||||
watchdog: 'Wachhund',
|
||||
server_logs: 'Serverprotokolle',
|
||||
ip_blocker: 'IP-Sperre',
|
||||
@@ -220,6 +227,7 @@ 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',
|
||||
@@ -228,11 +236,14 @@ 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',
|
||||
@@ -247,15 +258,189 @@ 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',
|
||||
hide_empty_wallets: 'Leere Geldbörsen verbergen'
|
||||
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'
|
||||
}
|
||||
|
||||
+316
-20
@@ -6,6 +6,7 @@ window.localisation.en = {
|
||||
funding: 'Funding',
|
||||
users: 'Users',
|
||||
audit: 'Audit',
|
||||
api_watch: 'Api Watch',
|
||||
apps: 'Apps',
|
||||
channels: 'Channels',
|
||||
transactions: 'Transactions',
|
||||
@@ -26,6 +27,7 @@ window.localisation.en = {
|
||||
close_channel: 'Close Channel',
|
||||
close: 'Close',
|
||||
restart: 'Restart server',
|
||||
image_library: 'Image Library',
|
||||
save: 'Save',
|
||||
save_tooltip: 'Save your changes',
|
||||
credit_debit: 'Credit / Debit',
|
||||
@@ -45,6 +47,8 @@ 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',
|
||||
@@ -56,12 +60,14 @@ 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 don’t 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',
|
||||
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_badge: 'Service fee: {amount} % per transaction',
|
||||
service_fee_max_badge:
|
||||
'Service fee: {amount} % per transaction (max {max} {denom})',
|
||||
service_fee_tooltip:
|
||||
'Service fee charged by the LNbits server admin per outgoing transaction',
|
||||
toggle_darkmode: 'Toggle Dark Mode',
|
||||
@@ -77,6 +83,7 @@ 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',
|
||||
@@ -84,15 +91,22 @@ window.localisation.en = {
|
||||
cancel: 'Cancel',
|
||||
scan: 'Scan',
|
||||
read: 'Read',
|
||||
write: 'Write',
|
||||
pay: 'Pay',
|
||||
memo: 'Memo',
|
||||
date: 'Date',
|
||||
processing_payment: 'Processing payment...',
|
||||
path: 'Path',
|
||||
payment_processing: 'Processing payment...',
|
||||
payment_processing: 'Processing payment...',
|
||||
payment_successful: 'Payment successful!',
|
||||
payment_pending: 'Payment pending...',
|
||||
payment_check: 'Check 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',
|
||||
@@ -103,7 +117,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\nThis service is in BETA. LNbits 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\nLNbits holds no responsibility for loss of access to funds.',
|
||||
no_transactions: 'No transactions made yet',
|
||||
manage: 'Manage',
|
||||
exchanges: 'Exchanges',
|
||||
@@ -111,6 +125,10 @@ 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?',
|
||||
@@ -158,20 +176,81 @@ 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',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
@@ -187,7 +266,6 @@ window.localisation.en = {
|
||||
more: 'more',
|
||||
less: 'less',
|
||||
releases: 'Releases',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Server Logs',
|
||||
ip_blocker: 'IP Blocker',
|
||||
@@ -197,28 +275,39 @@ 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: 'Want instant access?',
|
||||
instant_access_question: 'or 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',
|
||||
@@ -227,11 +316,13 @@ 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',
|
||||
login: 'Login',
|
||||
register: 'Register',
|
||||
username: 'Username',
|
||||
@@ -247,13 +338,23 @@ 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',
|
||||
color_scheme: 'Color Scheme',
|
||||
admin_settings: 'Admin Settings',
|
||||
extension_cost: 'This release requires a payment of minimum {cost} sats.',
|
||||
extension_paid_sats: 'You have already paid {paid_sats} sats.',
|
||||
release_details_error: 'Cannot get the release details.',
|
||||
@@ -275,5 +376,200 @@ window.localisation.en = {
|
||||
reset_key: 'Reset Key',
|
||||
reset_password: 'Reset Password',
|
||||
border_choices: 'Border Choices',
|
||||
select_all: 'Select All'
|
||||
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',
|
||||
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'
|
||||
}
|
||||
|
||||
+204
-17
@@ -2,8 +2,10 @@ 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',
|
||||
@@ -26,9 +28,11 @@ window.localisation.es = {
|
||||
restart: 'Reiniciar el servidor',
|
||||
save: 'Guardar',
|
||||
save_tooltip: 'Guardar cambios',
|
||||
topup: 'Recargar',
|
||||
topup_wallet: 'Recargar billetera',
|
||||
topup_hint: 'Utilice el ID de billetera para recargar cualquier billetera',
|
||||
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.',
|
||||
restart_tooltip: 'Reinicie el servidor para aplicar los cambios',
|
||||
add_funds_tooltip: 'Agregue fondos a una billetera.',
|
||||
reset_defaults: 'Restablecer',
|
||||
@@ -36,14 +40,13 @@ 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',
|
||||
@@ -71,8 +74,6 @@ 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',
|
||||
@@ -89,7 +90,7 @@ window.localisation.es = {
|
||||
pay: 'Pagar',
|
||||
memo: 'Memo',
|
||||
date: 'Fecha',
|
||||
processing_payment: 'Procesando pago ...',
|
||||
payment_processing: 'Procesando pago ...',
|
||||
not_enough_funds: '¡No hay suficientes fondos!',
|
||||
search_by_tag_memo_amount: 'Buscar por etiqueta, memo, cantidad',
|
||||
invoice_waiting: 'Factura esperando pago',
|
||||
@@ -108,10 +109,16 @@ 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?',
|
||||
@@ -120,8 +127,10 @@ 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',
|
||||
@@ -146,15 +155,19 @@ 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',
|
||||
@@ -164,13 +177,7 @@ 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',
|
||||
@@ -186,7 +193,6 @@ 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',
|
||||
@@ -220,6 +226,7 @@ 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',
|
||||
@@ -228,11 +235,14 @@ 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',
|
||||
@@ -247,14 +257,191 @@ 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',
|
||||
hide_empty_wallets: 'Ocultar billeteras vacías'
|
||||
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'
|
||||
}
|
||||
|
||||
+370
-49
@@ -2,8 +2,11 @@ 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',
|
||||
@@ -24,27 +27,30 @@ window.localisation.fi = {
|
||||
close_channel: 'Sulje kanava',
|
||||
close: 'Sulje',
|
||||
restart: 'Palvelimen uudelleen käynnistys',
|
||||
image_library: 'Kuvakirjasto',
|
||||
save: 'Tallenna',
|
||||
save_tooltip: 'Tallenna muutokset',
|
||||
topup: 'Topup',
|
||||
topup_wallet: 'Lisää varoja lompakkoon',
|
||||
topup_hint: 'Lisää varoja lompakkoon sen ID:n perusteella',
|
||||
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.',
|
||||
restart_tooltip: 'Uudelleenkäynnistä palvelu muutosten käyttöönottamiseksi',
|
||||
add_funds_tooltip: 'Lisää varoja lompakkoon',
|
||||
reset_defaults: 'Peruuta muutokset',
|
||||
reset_defaults: 'Palauta oletusasetukset',
|
||||
reset_defaults_tooltip:
|
||||
'Poista kaikki asetusten muutokset ja palauta järjestelmän oletusasetukset.',
|
||||
download_backup: 'Lataa tietokannan varmuuskopio',
|
||||
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 *',
|
||||
name_your_wallet: 'Nimeä lompakkosi {name}',
|
||||
paste_invoice_label: 'Liita lasku, maksupyyntö tai LNURL-koodi*',
|
||||
lnbits_description:
|
||||
'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!',
|
||||
'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!',
|
||||
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. Voi lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
|
||||
'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:',
|
||||
wallets: 'Lompakot',
|
||||
add_wallet: 'Lisää lompakko',
|
||||
delete_wallet: 'Poista lompakko',
|
||||
@@ -56,14 +62,13 @@ 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:
|
||||
'Maksutapahtumat ovat poissa käytöstä, koska VoidWallet on aktiivinen!',
|
||||
voidwallet_active: 'VoidWallet on aktiivinen. Se ei tue maksutapahtumia!',
|
||||
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ä',
|
||||
@@ -73,14 +78,13 @@ 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',
|
||||
@@ -88,15 +92,18 @@ window.localisation.fi = {
|
||||
cancel: 'Peruuta',
|
||||
scan: 'Scannaa',
|
||||
read: 'Lue',
|
||||
write: 'Kirjoita',
|
||||
pay: 'Maksa',
|
||||
memo: 'Kuvaus',
|
||||
date: 'Päiväys',
|
||||
processing_payment: 'Maksua käsitellään...',
|
||||
path: 'Path',
|
||||
payment_processing: 'Maksua käsitellään...',
|
||||
not_enough_funds: 'Varat eivät riitä!',
|
||||
search_by_tag_memo_amount: 'Etsi tunnisteella, muistiolla tai määrällä',
|
||||
invoice_waiting: 'Lasku 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',
|
||||
@@ -107,13 +114,18 @@ window.localisation.fi = {
|
||||
copy_wallet_url: 'Kopioi lompakon URL',
|
||||
disclaimer_dialog_title: 'Tärkeää!',
|
||||
disclaimer_dialog:
|
||||
'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.',
|
||||
'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.',
|
||||
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?',
|
||||
@@ -122,8 +134,10 @@ 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ä',
|
||||
@@ -145,32 +159,96 @@ 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ä julkaisu vaatii vähintään LNbits-version',
|
||||
'Tämä laajennus vaatii vähintään LNbits-version',
|
||||
min_version: 'Minimi (sisältyy)',
|
||||
max_version: 'Enimmäismäärä (ei sisälly)',
|
||||
payment_hash: 'Maksun tiiviste',
|
||||
fee: 'Kulu',
|
||||
amount: 'Määrä',
|
||||
amount_sats: 'Määrä (sat)',
|
||||
tag: 'Tunniste',
|
||||
unit: 'Yksikkö',
|
||||
description: 'Kuvaus',
|
||||
expiry: 'Vanheneminen',
|
||||
expiry: 'Vanhenee',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Maksun varmenne',
|
||||
update_available: 'Saatavilla on päivitys versioon {version}!',
|
||||
update: 'Päivitä',
|
||||
update_available: 'Saatavilla on päivitys {version}-versioon!',
|
||||
update_available: 'Rahoituslähteet',
|
||||
latest_update:
|
||||
'Käytössä oleva versio {version}, on viimeisin saatavilla oleva.',
|
||||
notifications: 'Tiedotteet',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
@@ -178,46 +256,57 @@ 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:
|
||||
'Saldomuutoksen raja-arvo jolloin killswitch-muuttaa rahoituslähteeksi VoidWallet:in [lnbits_balance - node_balance > delta]',
|
||||
'Mikäli rahoituslähteen saldo laskee alle LNbits kokonaissaldon, muutetaan rahoituslähteeksi heti VoidWallet. Päivittämisen jälkeen asetus pitää päivittää manuaalisestsi.',
|
||||
status: 'Tilanne',
|
||||
notification_source: 'Tiedotteiden lähde',
|
||||
notification_source_label:
|
||||
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
|
||||
more: 'enemmän',
|
||||
less: 'vähemmän',
|
||||
more: 'laajenna',
|
||||
less: 'supista',
|
||||
releases: 'Julkaisut',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Palvelimen lokit',
|
||||
ip_blocker: 'IP-suodatin',
|
||||
ip_blocker: 'Palvelimen suojaus IP-osoitesuodattimella',
|
||||
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',
|
||||
wallet_limiter: 'Lompakon 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_limit_max_withdraw_per_day:
|
||||
'Maksimi päivittäinen lompakon nosto sateissa (0 poistaa käytöstä)',
|
||||
wallet_max_ballance: 'Lompakon maksimisaldo satosheina (0 poistaa käytöstä)',
|
||||
'Päivittäin nostettavissa sat maksimi (0 poistaa käytöstä)',
|
||||
wallet_max_ballance: 'Maksimisaldo (sat) (0 poistaa käytöstä)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Min sekuntia transaktioiden välillä lompakkoa kohden (0 poistaa käytöstä)',
|
||||
'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ä',
|
||||
number_of_requests: 'Pyyntöjen lukumäärä',
|
||||
time_unit: 'aikayksikkö',
|
||||
minute: 'minuutti',
|
||||
settings: 'Asetukset',
|
||||
second: 'sekunti',
|
||||
hour: 'tunti',
|
||||
disable_server_log: 'Poista palvelimen loki käytöstä',
|
||||
enable_server_log: 'Ota palvelimen loki käyttöön',
|
||||
disable_server_log: 'Piilota palvelimen loki',
|
||||
enable_server_log: 'Näytä palvelimen loki',
|
||||
coming_soon: 'Ominaisuus on tulossa pian',
|
||||
session_has_expired: 'Käyttämätön sessio on vanhentunut. Kirjaudu uudelleen.',
|
||||
instant_access_question: 'Kirjaudu aikaisemmin luodulla tiedolla',
|
||||
instant_access_question: 'perinteinen kirjautuminen',
|
||||
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',
|
||||
@@ -225,12 +314,17 @@ 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ää',
|
||||
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',
|
||||
@@ -242,17 +336,244 @@ 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',
|
||||
extension_cost: 'Tämä julkaisu edellyttää vähintään {cost} satsin maksua.',
|
||||
admin_settings: 'Pääkäyttäjän asetukset',
|
||||
extension_cost: 'Tämä laajennus edellyttää vähintään {cost} sat 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: 'Tee uusi maksu',
|
||||
hide_empty_wallets: 'Piilota tyhjät lompakot'
|
||||
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'
|
||||
}
|
||||
|
||||
+203
-17
@@ -2,8 +2,10 @@ 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',
|
||||
@@ -26,10 +28,11 @@ window.localisation.fr = {
|
||||
restart: 'Redémarrer le serveur',
|
||||
save: 'Enregistrer',
|
||||
save_tooltip: 'Enregistrer vos modifications',
|
||||
topup: 'Renflouer',
|
||||
topup_wallet: 'Reflouer un portefeuille',
|
||||
topup_hint:
|
||||
"Utilisez l'ID du portefeuille pour recharger n'importe quel portefeuille",
|
||||
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.',
|
||||
restart_tooltip:
|
||||
'Redémarrez le serveur pour que les changements prennent effet',
|
||||
add_funds_tooltip: 'Ajouter des fonds à un portefeuille.',
|
||||
@@ -38,8 +41,6 @@ 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:
|
||||
@@ -47,6 +48,7 @@ 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',
|
||||
@@ -75,8 +77,6 @@ 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',
|
||||
processing_payment: 'Traitement du paiement...',
|
||||
payment_processing: '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,10 +112,16 @@ 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 ?',
|
||||
@@ -124,8 +130,10 @@ 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é',
|
||||
@@ -150,15 +158,19 @@ 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',
|
||||
@@ -168,12 +180,7 @@ 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.',
|
||||
@@ -190,7 +197,6 @@ 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",
|
||||
@@ -224,6 +230,7 @@ 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',
|
||||
@@ -232,11 +239,14 @@ 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',
|
||||
@@ -251,14 +261,190 @@ 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',
|
||||
hide_empty_wallets: 'Masquer les portefeuilles vides'
|
||||
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 d’index',
|
||||
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'
|
||||
}
|
||||
|
||||
+198
-16
@@ -2,8 +2,10 @@ 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',
|
||||
@@ -26,9 +28,11 @@ window.localisation.it = {
|
||||
restart: 'Riavvia il server',
|
||||
save: 'Salva',
|
||||
save_tooltip: 'Salva le modifiche',
|
||||
topup: 'Ricarica',
|
||||
topup_wallet: 'Ricarica un portafoglio',
|
||||
topup_hint: "Usa l'ID del portafoglio per ricaricare qualsiasi portafoglio",
|
||||
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.',
|
||||
restart_tooltip: 'Riavvia il server affinché le modifiche abbiano effetto',
|
||||
add_funds_tooltip: 'Aggiungere fondi a un portafoglio',
|
||||
reset_defaults: 'Ripristina le impostazioni predefinite',
|
||||
@@ -36,8 +40,6 @@ 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:
|
||||
@@ -45,6 +47,7 @@ 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',
|
||||
@@ -72,8 +75,6 @@ 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',
|
||||
@@ -90,7 +91,7 @@ window.localisation.it = {
|
||||
pay: 'Paga',
|
||||
memo: 'Memo',
|
||||
date: 'Dati',
|
||||
processing_payment: 'Elaborazione pagamento...',
|
||||
payment_processing: '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',
|
||||
@@ -109,10 +110,15 @@ 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?',
|
||||
@@ -121,8 +127,10 @@ 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',
|
||||
@@ -147,15 +155,19 @@ 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',
|
||||
@@ -164,12 +176,6 @@ 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.',
|
||||
@@ -186,7 +192,6 @@ 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',
|
||||
@@ -221,6 +226,7 @@ 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',
|
||||
@@ -229,11 +235,14 @@ 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',
|
||||
@@ -248,15 +257,188 @@ 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',
|
||||
hide_empty_wallets: 'Nascondi portafogli vuoti'
|
||||
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'
|
||||
}
|
||||
|
||||
+195
-17
@@ -2,8 +2,10 @@ window.localisation.jp = {
|
||||
confirm: 'はい',
|
||||
server: 'サーバー',
|
||||
theme: 'テーマ',
|
||||
site_customisation: 'サイトカスタマイズ',
|
||||
funding: '資金調達',
|
||||
users: 'ユーザー',
|
||||
audit: '監査',
|
||||
apps: 'アプリ',
|
||||
channels: 'チャンネル',
|
||||
transactions: 'トランザクション',
|
||||
@@ -26,23 +28,23 @@ window.localisation.jp = {
|
||||
restart: 'サーバーを再起動する',
|
||||
save: '保存',
|
||||
save_tooltip: '変更を保存する',
|
||||
topup: 'トップアップ',
|
||||
topup_wallet: 'ウォレットをトップアップする',
|
||||
topup_hint: 'ウォレットIDを使用して、任意のウォレットをトップアップできます',
|
||||
credit_debit: 'クレジット / デビット',
|
||||
credit_hint:
|
||||
'クレジットカードを使用して資金を追加するには、LNbitsを使用してください。',
|
||||
credit_label: '{denomination} をクレジットに',
|
||||
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: 'ウォレットを削除',
|
||||
@@ -69,9 +71,6 @@ 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: 'リクエストを貼り付ける',
|
||||
@@ -88,7 +87,7 @@ window.localisation.jp = {
|
||||
pay: '支払う',
|
||||
memo: 'メモ',
|
||||
date: '日付',
|
||||
processing_payment: '支払い処理中',
|
||||
payment_processing: '支払い処理中',
|
||||
not_enough_funds: '資金が不足しています',
|
||||
search_by_tag_memo_amount: 'タグ、メモ、金額で検索',
|
||||
invoice_waiting: '請求書を待っています',
|
||||
@@ -106,10 +105,15 @@ 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: '続行してもよろしいですか?',
|
||||
@@ -118,8 +122,10 @@ window.localisation.jp = {
|
||||
uninstall: 'アンインストール',
|
||||
drop_db: 'データを削除',
|
||||
enable: '有効',
|
||||
pay_to_enable: '有効にするために支払う',
|
||||
enable_extension_details: '現在のユーザーの拡張機能を有効にする',
|
||||
disable: '無効',
|
||||
delete: '削除',
|
||||
installed: 'インストール済み',
|
||||
activated: '有効化',
|
||||
deactivated: '無効化',
|
||||
@@ -144,15 +150,19 @@ 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: '通知',
|
||||
@@ -161,12 +171,6 @@ 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に変更します。アップデート後は手動で有効にする必要があります。',
|
||||
@@ -183,7 +187,6 @@ window.localisation.jp = {
|
||||
more: 'より多くの',
|
||||
less: '少ない',
|
||||
releases: 'リリース',
|
||||
killswitch: 'キルスイッチ',
|
||||
watchdog: 'ウォッチドッグ',
|
||||
server_logs: 'サーバーログ',
|
||||
ip_blocker: 'IPブロッカー',
|
||||
@@ -217,6 +220,7 @@ 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でサインイン',
|
||||
@@ -225,11 +229,14 @@ 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: '名',
|
||||
@@ -244,14 +251,185 @@ 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: '新しい支払いを作成する',
|
||||
hide_empty_wallets: '空のウォレットを非表示にする'
|
||||
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リクエストの詳細'
|
||||
}
|
||||
|
||||
+191
-16
@@ -2,8 +2,10 @@ window.localisation.kr = {
|
||||
confirm: '확인',
|
||||
server: '서버',
|
||||
theme: '테마',
|
||||
site_customisation: '사이트 사용자 정의',
|
||||
funding: '자금',
|
||||
users: '사용자',
|
||||
audit: '감사',
|
||||
apps: '앱',
|
||||
channels: '채널',
|
||||
transactions: '거래 내역',
|
||||
@@ -26,9 +28,11 @@ window.localisation.kr = {
|
||||
restart: '서버 재시작',
|
||||
save: '저장',
|
||||
save_tooltip: '변경 사항 저장',
|
||||
topup: '자금 추가',
|
||||
topup_wallet: '지갑에 자금 추가',
|
||||
topup_hint: '자금을 추가할 지갑의 ID를 넣어주세요',
|
||||
credit_debit: '크레딧 / 직불카드',
|
||||
credit_hint: '계정에 자금을 넣으려면 Enter를 눌러주세요',
|
||||
credit_label: '{denomination} 단위로 충전하기',
|
||||
credit_ok:
|
||||
'가상 자금({amount} sats) 입출금 성공. 지불은 자금 출처의 실제 자금에 따라 달라집니다.',
|
||||
restart_tooltip: '변경 사항을 적용하려면 서버를 재시작해야 합니다.',
|
||||
add_funds_tooltip: '지갑에 자금을 추가합니다.',
|
||||
reset_defaults: '기본 설정으로 돌아가기',
|
||||
@@ -36,14 +40,13 @@ window.localisation.kr = {
|
||||
'설정했던 내용들을 모두 지우고, 기본 설정으로 돌아갑니다.',
|
||||
download_backup: '데이터베이스 백업 다운로드',
|
||||
name_your_wallet: '사용할 {name}지갑의 이름을 정하세요',
|
||||
wallet_topup_ok:
|
||||
'성공적으로 가상 자금을 생성했습니다 ({amount} sats). 지급은 자금 원천의 실제 자금에 따라 달라집니다.',
|
||||
paste_invoice_label: '인보이스, 결제 요청, 혹은 lnurl 코드를 붙여넣으세요 *',
|
||||
lnbits_description:
|
||||
'설정이 쉽고 가벼운 LNbits는 어떤 라이트닝 네트워크의 예산 자원 위에서든 돌아갈 수 있습니다, 그리고 다른 LNbits 지갑들입니다. 스스로 사용하기 위해, 또는 다른 사람들에게 수탁형 솔루션을 제공하기 위해 LNbits를 운영할 수 있습니다. 각 지갑들은 자신만의 API key를 가지며, 생성 가능한 지갑의 수에는 제한이 없습니다. 자금을 분할할 수 있는 기능으로 인해, LNbits는 자금 운영 도구로써뿐만 아니라 개발 도구로써도 유용합니다. 확장 기능들은 LNbits에 여러분들이 라이트닝 네트워크의 다양한 최신 기술들을 수행해볼 수 있게 하는 추가 기능을 제공합니다. LNbits 개발진들은 확장 기능들의 개발 또한 가능한 쉽게 만들었으며, 무료 오픈 소스 프로젝트답게 사람들이 자신만의 확장 기능들을 개발하고 제출하기를 응원합니다.',
|
||||
export_to_phone: 'QR 코드를 이용해 모바일 기기로 내보내기',
|
||||
export_to_phone_desc:
|
||||
'이 QR 코드는 선택된 지갑의 최대 접근 권한을 가진 전체 URL을 담고 있습니다. 스캔 후, 모바일 기기에서 지갑을 열 수 있습니다.',
|
||||
wallet: '지갑:',
|
||||
wallets: '지갑',
|
||||
add_wallet: '새로운 지갑을 추가합니다',
|
||||
delete_wallet: '지갑을 삭제합니다',
|
||||
@@ -70,8 +73,6 @@ window.localisation.kr = {
|
||||
api_keys_api_docs: '노드 URL, API 키와 API 문서',
|
||||
lnbits_version: 'LNbits 버전',
|
||||
runs_on: 'Runs on',
|
||||
credit_hint: '계정에 자금을 넣으려면 Enter를 눌러주세요',
|
||||
credit_label: '{denomination} 단위로 충전하기',
|
||||
paste: '붙여넣기',
|
||||
paste_from_clipboard: '클립보드에서 붙여넣기',
|
||||
paste_request: '지불 요청 붙여넣기',
|
||||
@@ -88,7 +89,7 @@ window.localisation.kr = {
|
||||
pay: '지불하기',
|
||||
memo: 'Memo',
|
||||
date: '일시',
|
||||
processing_payment: '결제 처리 중...',
|
||||
payment_processing: '결제 처리 중...',
|
||||
not_enough_funds: '자금이 부족합니다!',
|
||||
search_by_tag_memo_amount: '태그, memo, 수량으로 검색하기',
|
||||
invoice_waiting: '결제를 기다리는 인보이스',
|
||||
@@ -107,10 +108,15 @@ window.localisation.kr = {
|
||||
'로그인 기능은 향후 업데이트를 통해 지원될 계획이지만, 현재로써는 이 페이지에 향후 다시 접속하기 위해 북마크 설정하는 것을 잊지 마세요! 이 서비스는 아직 BETA 과정에 있고, LNbits 개발자들은 자금 손실에 대해 전혀 책임을 지지 않습니다.',
|
||||
no_transactions: '아직 아무런 거래도 이루어지지 않았습니다',
|
||||
manage: '관리',
|
||||
exchanges: '거래소',
|
||||
extensions: '확장 기능',
|
||||
no_extensions: '아직 설치된 확장 기능들이 없네요 :(',
|
||||
created: '생성됨',
|
||||
search_extensions: '확장 기능 검색하기',
|
||||
extension_sources: '확장 소스',
|
||||
ext_sources_hint: '확장 프로그램을 다운로드할 수 있는 저장소',
|
||||
ext_sources_label:
|
||||
'출처 URL (공식 LNbits 확장 소스만 사용하고, 신뢰할 수 있는 출처를 사용하세요)',
|
||||
warning: '주의',
|
||||
repository: '저장소',
|
||||
confirm_continue: '정말로 계속할까요?',
|
||||
@@ -119,8 +125,10 @@ window.localisation.kr = {
|
||||
uninstall: '삭제',
|
||||
drop_db: '데이터 삭제',
|
||||
enable: '활성화',
|
||||
pay_to_enable: '지불하여 활성화',
|
||||
enable_extension_details: '현재 사용자 계정에 해당 확장 기능을 활성화합니다',
|
||||
disable: '비활성화',
|
||||
delete: '삭제',
|
||||
installed: '설치됨',
|
||||
activated: '작동됨',
|
||||
deactivated: '작동 중지',
|
||||
@@ -144,15 +152,19 @@ window.localisation.kr = {
|
||||
'해당 확장 기능의 모든 데이터가 영구적으로 삭제될 겁니다. 계속하려면 확장 기능의 이름을 입력해주세요:',
|
||||
extension_required_lnbits_version:
|
||||
'이 배포 버전은 더 높은 버전의 lnbits가 설치되어 있어야 합니다.',
|
||||
min_version: '최소값 (포함됨)',
|
||||
max_version: '최대값 (제외됨)',
|
||||
payment_hash: '결제 해쉬값',
|
||||
fee: '수수료',
|
||||
amount: '액수',
|
||||
amount_sats: '금액 (사토시)',
|
||||
tag: '태그',
|
||||
unit: '단위',
|
||||
description: '상세',
|
||||
expiry: '만료',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Payment 증거',
|
||||
update: '업데이트',
|
||||
update_available: '{version}으로 업데이트가 가능합니다.',
|
||||
latest_update: '이미 {version} 버전으로 업데이트되었습니다.',
|
||||
notifications: '알림',
|
||||
@@ -161,12 +173,6 @@ window.localisation.kr = {
|
||||
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으로 변경합니다. 업데이트 이후 수동으로 활성화해 주어야 합니다.',
|
||||
@@ -183,7 +189,6 @@ window.localisation.kr = {
|
||||
more: '더 알아보기',
|
||||
less: '적게',
|
||||
releases: '배포 버전들',
|
||||
killswitch: '비상 정지',
|
||||
watchdog: '와치독',
|
||||
server_logs: '서버 로그',
|
||||
ip_blocker: 'IP 기반 차단기',
|
||||
@@ -214,6 +219,7 @@ window.localisation.kr = {
|
||||
login_to_account: '계정에 로그인하세요.',
|
||||
create_account: '계정 생성',
|
||||
account_settings: '계정 설정',
|
||||
signin_with_nostr: 'Nostr로 계속하기',
|
||||
signin_with_google: 'Google으로 로그인',
|
||||
signin_with_github: 'GitHub으로 로그인',
|
||||
signin_with_keycloak: 'Keycloak으로 로그인',
|
||||
@@ -222,11 +228,14 @@ window.localisation.kr = {
|
||||
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: '성명',
|
||||
@@ -241,14 +250,180 @@ window.localisation.kr = {
|
||||
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: '새로운 결제하기',
|
||||
hide_empty_wallets: '빈 지갑 숨기기'
|
||||
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: '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: 'Keycloak 디스커버리 URL',
|
||||
auth_keycloak_ci_label: '키클록 클라이언트 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: '서비스 수수료 최대 (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: "'에 의해 구동됨' 등의 홈페이지 요소 제거",
|
||||
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 지갑은 여전히 BETA 상태입니다'",
|
||||
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:
|
||||
'광고 URL 및 이미지 파일 경로를 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: '최상위 5개의 장시간 실행 엔드포인트',
|
||||
http_request_methods: 'HTTP 요청 메서드',
|
||||
http_response_codes: 'HTTP 응답 코드',
|
||||
request_details: '요청 세부사항',
|
||||
http_request_details: 'HTTP 요청 세부사항'
|
||||
}
|
||||
|
||||
+200
-16
@@ -2,8 +2,10 @@ window.localisation.nl = {
|
||||
confirm: 'Ja',
|
||||
server: 'Server',
|
||||
theme: 'Thema',
|
||||
site_customisation: 'Site-aanpassing',
|
||||
funding: 'Financiering',
|
||||
users: 'Gebruikers',
|
||||
audit: 'Controle',
|
||||
apps: 'Apps',
|
||||
channels: 'Kanalen',
|
||||
transactions: 'Transacties',
|
||||
@@ -26,9 +28,11 @@ window.localisation.nl = {
|
||||
restart: 'Server opnieuw opstarten',
|
||||
save: 'Opslaan',
|
||||
save_tooltip: 'Sla uw wijzigingen op',
|
||||
topup: 'Bijvullen',
|
||||
topup_wallet: 'Een portemonnee bijvullen',
|
||||
topup_hint: 'Gebruik de portemonnee-ID om elke portemonnee bij te vullen',
|
||||
credit_debit: 'Credit / Debet',
|
||||
credit_hint: 'Druk op Enter om de rekening te crediteren',
|
||||
credit_label: '{denomination} te crediteren',
|
||||
credit_ok:
|
||||
'Succesvol crediteren/debiteren van virtuele gelden ({amount} sats). Betalingen zijn afhankelijk van de werkelijke fondsen op de financieringsbron.',
|
||||
restart_tooltip:
|
||||
'Start de server opnieuw op zodat wijzigingen van kracht worden',
|
||||
add_funds_tooltip: 'Voeg geld toe aan een portemonnee.',
|
||||
@@ -37,14 +41,13 @@ window.localisation.nl = {
|
||||
'Wis alle instellingen en herstel de standaardinstellingen.',
|
||||
download_backup: 'Databaseback-up downloaden',
|
||||
name_your_wallet: 'Geef je {name} portemonnee een naam',
|
||||
wallet_topup_ok:
|
||||
'Succes met het aanmaken van virtuele fondsen ({amount} sats). Betalingen zijn afhankelijk van de werkelijke fondsen op de financieringsbron.',
|
||||
paste_invoice_label: 'Plak een factuur, betalingsverzoek of lnurl-code*',
|
||||
lnbits_description:
|
||||
'Gemakkelijk in te stellen en lichtgewicht, LNbits kan op elke lightning-netwerkfinancieringsbron draaien en zelfs LNbits zelf! U kunt LNbits voor uzelf laten draaien of gemakkelijk een bewaardersoplossing voor anderen bieden. Elke portemonnee heeft zijn eigen API-sleutels en er is geen limiet aan het aantal portemonnees dat u kunt maken. Het kunnen partitioneren van fondsen maakt LNbits een nuttige tool voor geldbeheer en als ontwikkelingstool. Extensies voegen extra functionaliteit toe aan LNbits, zodat u kunt experimenteren met een reeks toonaangevende technologieën op het bliksemschichtnetwerk. We hebben het ontwikkelen van extensies zo eenvoudig mogelijk gemaakt en als een gratis en opensource-project moedigen we mensen aan om hun eigen ontwikkelingen in te dienen.',
|
||||
export_to_phone: 'Exporteren naar telefoon met QR-code',
|
||||
export_to_phone_desc:
|
||||
'Deze QR-code bevat uw portemonnee-URL met volledige toegang. U kunt het vanaf uw telefoon scannen om uw portemonnee van daaruit te openen.',
|
||||
wallet: 'Wallet:',
|
||||
wallets: 'Portemonnees',
|
||||
add_wallet: 'Een nieuwe portemonnee toevoegen',
|
||||
delete_wallet: 'Portemonnee verwijderen',
|
||||
@@ -72,8 +75,6 @@ window.localisation.nl = {
|
||||
api_keys_api_docs: 'Node URL, API-sleutels en API-documentatie',
|
||||
lnbits_version: 'LNbits-versie',
|
||||
runs_on: 'Draait op',
|
||||
credit_hint: 'Druk op Enter om de rekening te crediteren',
|
||||
credit_label: '{denomination} te crediteren',
|
||||
paste: 'Plakken',
|
||||
paste_from_clipboard: 'Plakken van klembord',
|
||||
paste_request: 'Verzoek plakken',
|
||||
@@ -90,7 +91,7 @@ window.localisation.nl = {
|
||||
pay: 'Betalen',
|
||||
memo: 'Memo',
|
||||
date: 'Datum',
|
||||
processing_payment: 'Verwerking betaling...',
|
||||
payment_processing: 'Verwerking betaling...',
|
||||
not_enough_funds: 'Onvoldoende saldo!',
|
||||
search_by_tag_memo_amount: 'Zoeken op tag, memo, bedrag',
|
||||
invoice_waiting: 'Factuur wachtend op betaling',
|
||||
@@ -109,10 +110,16 @@ window.localisation.nl = {
|
||||
'Inlogfunctionaliteit wordt uitgebracht in een toekomstige update. Zorg er nu voor dat je deze pagina als favoriet markeert om in de toekomst toegang te krijgen tot je portemonnee! Deze service is in BETA en we zijn niet verantwoordelijk voor mensen die de toegang tot hun fondsen verliezen.',
|
||||
no_transactions: 'Er zijn nog geen transacties gedaan',
|
||||
manage: 'Beheer',
|
||||
exchanges: 'Beurzen',
|
||||
extensions: 'Extensies',
|
||||
no_extensions: 'Je hebt geen extensies geïnstalleerd :(',
|
||||
created: 'Aangemaakt',
|
||||
search_extensions: 'Zoekextensies',
|
||||
extension_sources: 'Extensiebronnen',
|
||||
ext_sources_hint:
|
||||
'Repositories van waar de extensies kunnen worden gedownload',
|
||||
ext_sources_label:
|
||||
'Bron-URL (gebruik alleen de officiële LNbits-extensiebron en bronnen die je kunt vertrouwen)',
|
||||
warning: 'Waarschuwing',
|
||||
repository: 'Repository',
|
||||
confirm_continue: 'Weet je zeker dat je wilt doorgaan?',
|
||||
@@ -121,8 +128,10 @@ window.localisation.nl = {
|
||||
uninstall: 'Deïnstalleren',
|
||||
drop_db: 'Gegevens verwijderen',
|
||||
enable: 'Inschakelen',
|
||||
pay_to_enable: 'Betalen om te activeren',
|
||||
enable_extension_details: 'Schakel extensie in voor huidige gebruiker',
|
||||
disable: 'Uitschakelen',
|
||||
delete: 'Verwijderen',
|
||||
installed: 'Geïnstalleerd',
|
||||
activated: 'Geactiveerd',
|
||||
deactivated: 'Gedeactiveerd',
|
||||
@@ -147,15 +156,19 @@ window.localisation.nl = {
|
||||
'U staat op het punt alle gegevens voor de extensie te verwijderen. Typ de naam van de extensie om door te gaan:',
|
||||
extension_required_lnbits_version:
|
||||
'Deze release vereist ten minste LNbits-versie',
|
||||
min_version: 'Minimum (inbegrepen)',
|
||||
max_version: 'Maximum (uitgesloten)',
|
||||
payment_hash: 'Betalings-hash',
|
||||
fee: 'Kosten',
|
||||
amount: 'Bedrag',
|
||||
amount_sats: 'Bedrag (sats)',
|
||||
tag: 'Label',
|
||||
unit: 'Eenheid',
|
||||
description: 'Beschrijving',
|
||||
expiry: 'Vervaldatum',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Betalingsbewijs',
|
||||
update: 'Bijwerken',
|
||||
update_available: 'Update {version} beschikbaar!',
|
||||
latest_update: 'U bent op de nieuwste versie {version}.',
|
||||
notifications: 'Meldingen',
|
||||
@@ -164,12 +177,6 @@ window.localisation.nl = {
|
||||
enable_notifications: 'Schakel meldingen in',
|
||||
enable_notifications_desc:
|
||||
'Indien ingeschakeld zal het de laatste LNbits Status updates ophalen, zoals veiligheidsincidenten en updates.',
|
||||
enable_killswitch: 'Activeer Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Indien ingeschakeld, zal het uw financieringsbron automatisch wijzigen naar VoidWallet als LNbits een killswitch-signaal verzendt. U zult het na een update handmatig moeten inschakelen.',
|
||||
killswitch_interval: 'Uitschakelschakelaar-interval',
|
||||
killswitch_interval_desc:
|
||||
'Hoe vaak de achtergrondtaak moet controleren op het LNbits killswitch signaal van de statusbron (in minuten).',
|
||||
enable_watchdog: 'Inschakelen Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Indien ingeschakeld, wordt uw betaalbron automatisch gewijzigd naar VoidWallet als uw saldo lager is dan het saldo van LNbits. U zult dit na een update handmatig moeten inschakelen.',
|
||||
@@ -186,7 +193,6 @@ window.localisation.nl = {
|
||||
more: 'meer',
|
||||
less: 'minder',
|
||||
releases: 'Uitgaven',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Waakhond',
|
||||
server_logs: 'Serverlogboeken',
|
||||
ip_blocker: 'IP-blokkering',
|
||||
@@ -220,6 +226,7 @@ window.localisation.nl = {
|
||||
login_to_account: 'Log in op je account',
|
||||
create_account: 'Account aanmaken',
|
||||
account_settings: 'Accountinstellingen',
|
||||
signin_with_nostr: 'Doorgaan met Nostr',
|
||||
signin_with_google: 'Inloggen met Google',
|
||||
signin_with_github: 'Inloggen met GitHub',
|
||||
signin_with_keycloak: 'Inloggen met Keycloak',
|
||||
@@ -228,11 +235,14 @@ window.localisation.nl = {
|
||||
password_config: 'Wachtwoordconfiguratie',
|
||||
password_repeat: 'Wachtwoord herhalen',
|
||||
change_password: 'Wachtwoord wijzigen',
|
||||
update_credentials: 'Referenties bijwerken',
|
||||
update_pubkey: 'Openbare Sleutel Bijwerken',
|
||||
set_password: 'Wachtwoord instellen',
|
||||
invalid_password: 'Wachtwoord moet ten minste 8 tekens bevatten',
|
||||
login: 'Inloggen',
|
||||
register: 'Registreren',
|
||||
username: 'Gebruikersnaam',
|
||||
pubkey: 'Publieke Sleutel',
|
||||
user_id: 'Gebruikers-ID',
|
||||
email: 'E-mail',
|
||||
first_name: 'Voornaam',
|
||||
@@ -247,14 +257,188 @@ window.localisation.nl = {
|
||||
back: 'Terug',
|
||||
logout: 'Afmelden',
|
||||
look_and_feel: 'Uiterlijk en gedrag',
|
||||
toggle_gradient: 'Gradiënt Schakelen',
|
||||
gradient_background: 'Verloopachtergrond',
|
||||
language: 'Taal',
|
||||
color_scheme: 'Kleurenschema',
|
||||
admin_settings: 'Beheerdersinstellingen',
|
||||
extension_cost: 'Deze release vereist een betaling van minimaal {cost} sats.',
|
||||
extension_paid_sats: 'U heeft al {paid_sats} sats betaald.',
|
||||
release_details_error: 'Kan de gegevens van de release niet ophalen.',
|
||||
pay_from_wallet: 'Betalen vanuit Portemonnee',
|
||||
wallet_required: 'Wallet *',
|
||||
show_qr: 'Toon QR',
|
||||
retry_install: 'Opnieuw installeren',
|
||||
new_payment: 'Nieuwe betaling maken',
|
||||
hide_empty_wallets: 'Verberg lege portemonnees'
|
||||
update_payment: 'Betaling bijwerken',
|
||||
already_paid_question: 'Heb je al betaald?',
|
||||
sell: 'Verkopen',
|
||||
sell_require: 'Vraag betaling om de extensie te activeren.',
|
||||
sell_info:
|
||||
'De {name} extensie vereist een betaling van minimaal {amount} sats om in te schakelen.',
|
||||
hide_empty_wallets: 'Verberg lege portemonnees',
|
||||
recheck: 'Opnieuw controleren',
|
||||
contributors: 'Bijdragers',
|
||||
license: 'Licentie',
|
||||
reset_key: 'Hersteltoets',
|
||||
reset_password: 'Wachtwoord Resetten',
|
||||
border_choices: 'Randkeuzes',
|
||||
select_all: 'Alles selecteren',
|
||||
nfc_supported: 'NFC Ondersteund',
|
||||
nfc_not_supported: 'NFC niet ondersteund',
|
||||
expire_date: 'Vervaldatum:',
|
||||
hash: 'Hash:',
|
||||
welcome_lnbits: 'Welkom bij LNbits',
|
||||
setup_su_account: 'Stel het Superuser-account hieronder in.',
|
||||
create_ticker_converter: 'Maak Valuta Ticker Converter',
|
||||
enable_audit: 'Audit inschakelen',
|
||||
recommended: 'Aanbevolen',
|
||||
audit_desc: 'HTTP-verzoeken vastleggen volgens de opgegeven filters',
|
||||
audit_record_req: 'Verzoeklichaam registreren',
|
||||
audit_record_warning: 'Waarschuwing:',
|
||||
audit_record_req_warning_1:
|
||||
'vertrouwelijke gegevens (zoals wachtwoorden) worden gelogd.',
|
||||
audit_record_req_warning_2: 'de aanvraagbody kan een grote omvang hebben.',
|
||||
audit_record_use: 'Gebruik het met voorzichtigheid.',
|
||||
audit_ip: 'IP-adres vastleggen',
|
||||
audit_ip_desc: 'Leg het IP-adres van de klant vast',
|
||||
audit_path_params: 'Parameters van het pad opnemen',
|
||||
audit_query_params: 'Queryparameters vastleggen',
|
||||
audit_http_methods: 'Inclusief HTTP-methoden',
|
||||
audit_http_methods_hint:
|
||||
'Lijst van HTTP-methoden die moeten worden opgenomen. Lege lijsten betekenen alles.',
|
||||
audit_http_methods_label: 'HTTP-methoden',
|
||||
audit_resp_codes: 'Inclusief HTTP-responscodes',
|
||||
audit_resp_codes_hint:
|
||||
'Lijst van op te nemen HTTP-codes (regex-overeenkomst). Lege lijst betekent alles. Bijvoorbeeld: 4.*, 5.*',
|
||||
audit_resp_codes_label: 'HTTP-responscode (regex)',
|
||||
audit_paths: 'Inclusiepad',
|
||||
audit_paths_hint:
|
||||
'Lijst met paden die moeten worden opgenomen (regex match). Lege lijst betekent alles.',
|
||||
audit_paths_label: 'HTTP-pad (regex)',
|
||||
audit_paths_exclude: 'Paden uitsluiten',
|
||||
audit_paths_exclude_hint:
|
||||
'Lijst met paden die moeten worden uitgesloten (regex-overeenkomst). Een lege lijst betekent geen.',
|
||||
audit_paths_exclude_label: 'HTTP-pad (regex)',
|
||||
exchange_providers: 'Wisselaanbieders',
|
||||
admin_extensions: 'Beheeruitbreidingen',
|
||||
admin_extensions_label: 'Beheerdersuitbreidingen',
|
||||
admin_extensions_hint:
|
||||
'Alleen gebruikers met beheerdersrechten kunnen extensies gebruiken.',
|
||||
user_default_extensions: 'Standaardextensies voor gebruikers',
|
||||
user_default_extensions_label: 'Gebruikersuitbreidingen',
|
||||
user_default_extensions_hint:
|
||||
'Extensies die standaard voor de gebruikers worden ingeschakeld.',
|
||||
miscellanous: 'Diversen',
|
||||
misc_disable_extensions: 'Extensies uitschakelen',
|
||||
misc_disable_extensions_label: 'Alle extensies uitschakelen',
|
||||
misc_hide_api: 'API verbergen',
|
||||
misc_hide_api_label:
|
||||
'Verbergt de wallet-API, extensies kunnen ervoor kiezen dit te respecteren',
|
||||
wallets_management: 'Beheer van portemonnees',
|
||||
funding_source_info: 'Financieringsbroninfo',
|
||||
funding_source: 'Financieringsbron: {wallet_class}',
|
||||
node_balance: 'Node Balans: {balance} sats',
|
||||
lnbits_balance: 'LNbits Saldo: {balance} sats',
|
||||
funding_reserve_percent: 'Reservepercentage: {percent} %',
|
||||
node_managment: 'Nodebeheer',
|
||||
node_management_not_supported:
|
||||
'Nodebeheer wordt niet ondersteund door de actieve financieringsbron',
|
||||
toggle_node_ui: 'Node UI',
|
||||
toggle_public_node_ui: 'Openbare Node UI',
|
||||
toggle_transactions_node_ui:
|
||||
'Transacties Tabblad (Uitschakelen op grote CLN-nodes)',
|
||||
invoice_expiry: 'Factuurvervaldatum',
|
||||
invoice_expiry_label: 'Factuurverloop (seconden)',
|
||||
fee_reserve: 'Toegangsvergoeding Reserve',
|
||||
fee_reserve_msats: 'Reserveringskosten in msats',
|
||||
fee_reserve_percent: 'Reserveringskosten in procent',
|
||||
server_management: 'Serverbeheer',
|
||||
base_url: 'Basis-URL',
|
||||
base_url_label: 'Statisch/Basis-URL voor de server',
|
||||
authentication: 'Authenticatie',
|
||||
auth_token_expiry_label: 'Token vervalt over minuten',
|
||||
auth_token_expiry_hint: 'Tijd in minuten totdat de token verloopt',
|
||||
auth_allowed_methods_label: 'Toegestane autorisatiemethoden',
|
||||
auth_allowed_methods_hint: 'Selecteer autorisatiemethoden',
|
||||
auth_nostr_label: 'Nostr Aanvraag-URL',
|
||||
auth_nostr_hint:
|
||||
'Absolute URL die de klanten zullen gebruiken om in te loggen.',
|
||||
auth_google_ci_label: 'Google Client-ID',
|
||||
auth_google_ci_hint:
|
||||
"Zorg ervoor dat de geautoriseerde omleidings-URL's https://{domain}/api/v1/auth/google/token bevatten.",
|
||||
auth_google_cs_label: 'Google Client Secret',
|
||||
auth_gh_client_id_label: 'GitHub client-ID',
|
||||
auth_gh_client_id_hint:
|
||||
'Zorg ervoor dat de autorisatie-callback-URL is ingesteld op https://{domain}/api/v1/auth/github/token',
|
||||
auth_gh_client_secret_label: 'GitHub Clientgeheim',
|
||||
auth_keycloak_label: 'Keycloak Ontdekking URL',
|
||||
auth_keycloak_ci_label: 'Keycloak-client-ID',
|
||||
auth_keycloak_ci_hint:
|
||||
'Zorg ervoor dat de autorisatie callback-URL is ingesteld op https://{domain}/api/v1/auth/keycloak/token',
|
||||
auth_keycloak_cs_label: 'Keycloak Clientgeheim',
|
||||
currency_settings: 'Valuta-instellingen',
|
||||
allowed_currencies: "Toegestane valuta's",
|
||||
allowed_currencies_hint: "Beperk het aantal beschikbare fiatvaluta's",
|
||||
default_account_currency: 'Standaardrekeningvaluta',
|
||||
default_account_currency_hint: 'Standaardvaluta voor boekhouding',
|
||||
service_fee_label: 'Servicekosten (%)',
|
||||
service_fee_hint: 'Toeslag per transactie (%)',
|
||||
service_fee_max_label: 'Servicekosten max (sats)',
|
||||
service_fee_max_hint:
|
||||
'Maximale servicekosten om in rekening te brengen in (sats)',
|
||||
fee_wallet: 'Kosten Portemonnee',
|
||||
fee_wallet_label: 'Kosten portemonnee (wallet ID)',
|
||||
fee_wallet_hint: 'Wallet-ID om geld naar over te maken',
|
||||
disable_fee: 'Kosten uitschakelen',
|
||||
disable_fee_internal: 'Servicekosten uitschakelen voor interne betalingen',
|
||||
disable_fee_internal_desc:
|
||||
'Dienstenkosten uitschakelen voor interne Lightning-betalingen',
|
||||
ui_management: 'UI-beheer',
|
||||
ui_site_title: 'Site titel',
|
||||
ui_site_tagline: 'Site-slogan',
|
||||
ui_elements_enable: 'Elementen op de homepage inschakelen',
|
||||
ui_elements_disable: 'Elementen op de homepage uitschakelen',
|
||||
ui_toggle_elements_tip:
|
||||
"Verwijder startpagina-elementen zoals 'werkt op' enz.",
|
||||
ui_site_description: 'Sitebeschrijving',
|
||||
ui_site_description_hint: 'Gebruik platte tekst, Markdown, of ruwe HTML',
|
||||
ui_default_wallet_name: 'Standaard Wallet Naam',
|
||||
lnbits_wallet: 'LNbits-portemonnee',
|
||||
denomination: 'Denominatie',
|
||||
denomination_hint: 'De naam voor de FakeWallet token',
|
||||
ui_qr_code_logo: 'QR-code-logo',
|
||||
ui_qr_code_logo_hint: 'URL naar logo-afbeelding in QR-code',
|
||||
ui_custom_badge: 'Aangepaste badge',
|
||||
ui_custom_badge_label:
|
||||
"Aangepaste Badge 'GEBRUIK MET VOORZICHTIGHEID - LNbits-portemonnee is nog in BÈTA'",
|
||||
ui_custom_badge_color_label: 'Aangepaste Badge Kleur',
|
||||
themes: "Thema's",
|
||||
themes_hint: "Kies thema's beschikbaar voor gebruikers",
|
||||
custom_logo: 'Aangepast logo',
|
||||
custom_logo_hint: 'URL naar logo-afbeelding',
|
||||
ad_space_title: 'Advertentieruimte Titel',
|
||||
ad_space_title_label: 'Ondersteund door',
|
||||
ad_slots: 'Advertentieblokken',
|
||||
ad_slots_hint:
|
||||
'Ad URL en afbeeldingspad in CSV-formaat, extensies kunnen ervoor kiezen te honoreren',
|
||||
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
|
||||
ads_enabled: 'Advertenties ingeschakeld',
|
||||
ads_disabled: 'Advertenties uitgeschakeld',
|
||||
user_management: 'Gebruikersbeheer',
|
||||
admin_users: 'Beheerdersgebruikers',
|
||||
admin_users_hint: 'Gebruikers met beheerdersrechten',
|
||||
admin_users_label: 'Gebruikers-ID',
|
||||
allowed_users: 'Toegestane gebruikers',
|
||||
allowed_users_hint: 'Alleen deze gebruikers kunnen LNbits gebruiken',
|
||||
allowed_users_label: 'Gebruikers-ID',
|
||||
allow_creation_user: 'Sta het aanmaken van nieuwe gebruikers toe',
|
||||
allow_creation_user_desc:
|
||||
'Sta de aanmaak van nieuwe gebruikers op de indexpagina toe',
|
||||
components: 'Componenten',
|
||||
long_running_endpoints: 'Top 5 langlopende eindpunten',
|
||||
http_request_methods: 'HTTP-aanvraagmethoden',
|
||||
http_response_codes: 'HTTP-responscodes',
|
||||
request_details: 'Aanvraagdetails',
|
||||
http_request_details: 'HTTP-verzoekdetails'
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user