Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c820456500 | ||
|
|
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 |
+1
-1
@@ -186,7 +186,7 @@ LNBITS_ADMIN_USERS=""
|
|||||||
# SUPER_USER=""
|
# SUPER_USER=""
|
||||||
|
|
||||||
# Extensions only admin can access
|
# Extensions only admin can access
|
||||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
LNBITS_ADMIN_EXTENSIONS="ngrok, nostrclient"
|
||||||
# Extensions enabled by default when a user is created
|
# Extensions enabled by default when a user is created
|
||||||
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
|
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ inputs:
|
|||||||
default: "3.10"
|
default: "3.10"
|
||||||
poetry-version:
|
poetry-version:
|
||||||
description: "Poetry Version"
|
description: "Poetry Version"
|
||||||
default: "1.7.0"
|
default: "1.8.5"
|
||||||
node-version:
|
node-version:
|
||||||
description: "Node Version"
|
description: "Node Version"
|
||||||
default: "20.x"
|
default: "20.x"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
poetry run pip install pyinstaller
|
poetry run pip install pyinstaller
|
||||||
|
|
||||||
# Build the LNbits binary
|
# Build the LNbits binary
|
||||||
poetry run pyinstaller --onefile --name lnbits --collect-all lnbits --collect-all sqlalchemy --collect-all aiosqlite --hidden-import=passlib.handlers.bcrypt $(poetry run which lnbits)
|
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 ../../../../..
|
cd ../../../../..
|
||||||
chmod +x packaging/linux/AppDir/AppRun
|
chmod +x packaging/linux/AppDir/AppRun
|
||||||
chmod +x packaging/linux/AppDir/lnbits.desktop
|
chmod +x packaging/linux/AppDir/lnbits.desktop
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: release-rc
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "*-rc[0-9]"
|
- "*-rc[0-9]+"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ lnbits/static/bundle.min.css.old
|
|||||||
lnbits/static/bundle-components.min.js.old
|
lnbits/static/bundle-components.min.js.old
|
||||||
lnbits/upgrades
|
lnbits/upgrades
|
||||||
docker
|
docker
|
||||||
|
generated
|
||||||
|
openapi.json*
|
||||||
|
|
||||||
# Nix
|
# Nix
|
||||||
*result*
|
*result*
|
||||||
|
|||||||
@@ -99,9 +99,13 @@ openapi:
|
|||||||
curl -s http://0.0.0.0:5003/openapi.json | poetry run openapi-spec-validator --errors=all -
|
curl -s http://0.0.0.0:5003/openapi.json | poetry run openapi-spec-validator --errors=all -
|
||||||
# kill -9 %1
|
# kill -9 %1
|
||||||
|
|
||||||
bak:
|
generate:
|
||||||
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres
|
rm -rf generated
|
||||||
#
|
mkdir generated
|
||||||
|
rm -f openapi.json
|
||||||
|
wget localhost:5000/openapi.json
|
||||||
|
npm run generate
|
||||||
|
rm openapi.json
|
||||||
|
|
||||||
sass:
|
sass:
|
||||||
npm run sass
|
npm run sass
|
||||||
|
|||||||
@@ -29,11 +29,13 @@ It is recommended to use the latest version of Poetry. Make sure you have Python
|
|||||||
|
|
||||||
### Install Python 3.12
|
### Install Python 3.12
|
||||||
|
|
||||||
|
## Option 2 (recommended): Poetry
|
||||||
|
|
||||||
|
It is recommended to use the latest version of Poetry. Make sure you have Python version 3.9 or higher installed.
|
||||||
|
|
||||||
|
### Verify Python version
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
|
||||||
sudo apt update -y
|
|
||||||
sudo apt install -y python3.12 python3.12-dev # ensure correct headers needed for secp256k1
|
|
||||||
sudo apt install -y pkg-config python3-dev build-essential # ensure correct headers
|
|
||||||
python3 --version
|
python3 --version
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -83,6 +85,18 @@ poetry install --only main
|
|||||||
# Start LNbits with `poetry run lnbits`
|
# Start LNbits with `poetry run lnbits`
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Option 2: Install script (on Debian/Ubuntu)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wget https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits.sh &&
|
||||||
|
chmod +x lnbits.sh &&
|
||||||
|
./lnbits.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Now visit `0.0.0.0:5000` to make a super-user account.
|
||||||
|
|
||||||
|
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `poetry run lnbits` (see previous option).
|
||||||
|
|
||||||
## Option 3: Nix
|
## Option 3: Nix
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -226,7 +240,7 @@ Problems installing? These commands have helped us install LNbits.
|
|||||||
sudo apt install pkg-config libffi-dev libpq-dev
|
sudo apt install pkg-config libffi-dev libpq-dev
|
||||||
|
|
||||||
# build essentials for debian/ubuntu
|
# build essentials for debian/ubuntu
|
||||||
sudo apt install python3.9-dev gcc build-essential
|
sudo apt install python3.10-dev gcc build-essential
|
||||||
|
|
||||||
# if the secp256k1 build fails:
|
# if the secp256k1 build fails:
|
||||||
# if you used poetry
|
# if you used poetry
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check install has not already run
|
||||||
|
if [ ! -d lnbits/data ]; then
|
||||||
|
|
||||||
|
# Update package list and install prerequisites non-interactively
|
||||||
|
sudo apt update -y
|
||||||
|
sudo apt install -y software-properties-common
|
||||||
|
|
||||||
|
# Add the deadsnakes PPA repository non-interactively
|
||||||
|
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||||
|
|
||||||
|
# Install Python 3.9 and distutils non-interactively
|
||||||
|
sudo apt install -y python3.9 python3.9-distutils
|
||||||
|
|
||||||
|
# Install Poetry
|
||||||
|
curl -sSL https://install.python-poetry.org | python3.9 -
|
||||||
|
|
||||||
|
# Add Poetry to PATH for the current session
|
||||||
|
export PATH="/home/$USER/.local/bin:$PATH"
|
||||||
|
|
||||||
|
if [ ! -d lnbits/wallets ]; then
|
||||||
|
# Clone the LNbits repository
|
||||||
|
git clone https://github.com/lnbits/lnbits.git
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to clone the repository ... FAIL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Ensure we are in the lnbits directory
|
||||||
|
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout main
|
||||||
|
# Make data folder
|
||||||
|
mkdir data
|
||||||
|
|
||||||
|
# Copy the .env.example to .env
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
elif [ ! -d lnbits/wallets ]; then
|
||||||
|
# cd into lnbits
|
||||||
|
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install the dependencies using Poetry
|
||||||
|
poetry env use python3.9
|
||||||
|
poetry install --only main
|
||||||
|
|
||||||
|
# Set environment variables for LNbits
|
||||||
|
export LNBITS_ADMIN_UI=true
|
||||||
|
export HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Run LNbits
|
||||||
|
poetry run lnbits
|
||||||
+8
-3
@@ -81,6 +81,7 @@ async def startup(app: FastAPI):
|
|||||||
|
|
||||||
# setup admin settings
|
# setup admin settings
|
||||||
await check_admin_settings()
|
await check_admin_settings()
|
||||||
|
core_app_extra.register_new_ratelimiter()
|
||||||
await check_webpush_settings()
|
await check_webpush_settings()
|
||||||
|
|
||||||
# check extensions after restart
|
# check extensions after restart
|
||||||
@@ -162,9 +163,13 @@ def create_app() -> FastAPI:
|
|||||||
core_app_extra.register_new_ratelimiter = register_new_ratelimiter(app)
|
core_app_extra.register_new_ratelimiter = register_new_ratelimiter(app)
|
||||||
|
|
||||||
# register static files
|
# register static files
|
||||||
static_path = Path("lnbits", "static")
|
app.mount("/static", StaticFiles(directory=Path("lnbits", "static")), name="static")
|
||||||
static = StaticFiles(directory=static_path)
|
Path(settings.lnbits_data_folder, "images").mkdir(parents=True, exist_ok=True)
|
||||||
app.mount("/static", static, name="static")
|
app.mount(
|
||||||
|
"/library",
|
||||||
|
StaticFiles(directory=Path(settings.lnbits_data_folder, "images")),
|
||||||
|
name="library",
|
||||||
|
)
|
||||||
|
|
||||||
g().base_url = f"http://{settings.host}:{settings.port}"
|
g().base_url = f"http://{settings.host}:{settings.port}"
|
||||||
|
|
||||||
|
|||||||
+37
-14
@@ -5,6 +5,7 @@ import time
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import httpx
|
import httpx
|
||||||
@@ -27,13 +28,13 @@ from lnbits.core.crud import (
|
|||||||
update_payment,
|
update_payment,
|
||||||
)
|
)
|
||||||
from lnbits.core.helpers import is_valid_url, migrate_databases
|
from lnbits.core.helpers import is_valid_url, migrate_databases
|
||||||
from lnbits.core.models import Payment, PaymentState
|
from lnbits.core.models import Account, Payment, PaymentState
|
||||||
from lnbits.core.models.extensions import (
|
from lnbits.core.models.extensions import (
|
||||||
CreateExtension,
|
CreateExtension,
|
||||||
ExtensionRelease,
|
ExtensionRelease,
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
)
|
)
|
||||||
from lnbits.core.services import check_admin_settings
|
from lnbits.core.services import check_admin_settings, create_user_account_no_ckeck
|
||||||
from lnbits.core.views.extension_api import (
|
from lnbits.core.views.extension_api import (
|
||||||
api_install_extension,
|
api_install_extension,
|
||||||
api_uninstall_extension,
|
api_uninstall_extension,
|
||||||
@@ -64,6 +65,13 @@ def db():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@lnbits_cli.group()
|
||||||
|
def users():
|
||||||
|
"""
|
||||||
|
Users related commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@lnbits_cli.group()
|
@lnbits_cli.group()
|
||||||
def extensions():
|
def extensions():
|
||||||
"""
|
"""
|
||||||
@@ -109,7 +117,7 @@ async def delete_settings():
|
|||||||
"""Deletes the settings"""
|
"""Deletes the settings"""
|
||||||
|
|
||||||
async with core_db.connect() as conn:
|
async with core_db.connect() as conn:
|
||||||
await conn.execute("DELETE from settings")
|
await conn.execute("DELETE from system_settings")
|
||||||
|
|
||||||
|
|
||||||
@db.command("migrate")
|
@db.command("migrate")
|
||||||
@@ -180,17 +188,6 @@ async def database_revert_payment(checking_id: str):
|
|||||||
click.echo(f"Payment '{checking_id}' marked as pending.")
|
click.echo(f"Payment '{checking_id}' marked as pending.")
|
||||||
|
|
||||||
|
|
||||||
@db.command("cleanup-accounts")
|
|
||||||
@click.argument("days", type=int, required=False)
|
|
||||||
@coro
|
|
||||||
async def database_cleanup_accounts(days: Optional[int] = None):
|
|
||||||
"""Delete all accounts that have no wallets"""
|
|
||||||
async with core_db.connect() as conn:
|
|
||||||
delta = days or settings.cleanup_wallets_days
|
|
||||||
delta = delta * 24 * 60 * 60
|
|
||||||
await delete_accounts_no_wallets(delta, conn)
|
|
||||||
|
|
||||||
|
|
||||||
@db.command("check-payments")
|
@db.command("check-payments")
|
||||||
@click.option("-d", "--days", help="Maximum age of payments in days.")
|
@click.option("-d", "--days", help="Maximum age of payments in days.")
|
||||||
@click.option("-l", "--limit", help="Maximum number of payments to be checked.")
|
@click.option("-l", "--limit", help="Maximum number of payments to be checked.")
|
||||||
@@ -271,6 +268,32 @@ async def check_invalid_payments(
|
|||||||
click.echo(" ".join([w, str(data[0]), str(data[1] / 1000).ljust(10)]))
|
click.echo(" ".join([w, str(data[0]), str(data[1] / 1000).ljust(10)]))
|
||||||
|
|
||||||
|
|
||||||
|
@users.command("new")
|
||||||
|
@click.option("-u", "--username", required=True, help="Username.")
|
||||||
|
@click.option("-p", "--password", required=True, help="Password.")
|
||||||
|
@coro
|
||||||
|
async def create_user(username: str, password: str):
|
||||||
|
"""Create a new user bypassing the system 'new_accounts_allowed' rules"""
|
||||||
|
account = Account(
|
||||||
|
id=uuid4().hex,
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
|
account.hash_password(password)
|
||||||
|
user = await create_user_account_no_ckeck(account)
|
||||||
|
click.echo(f"User '{user.username}' created. Id: '{user.id}'")
|
||||||
|
|
||||||
|
|
||||||
|
@users.command("cleanup-accounts")
|
||||||
|
@click.argument("days", type=int, required=False)
|
||||||
|
@coro
|
||||||
|
async def database_cleanup_accounts(days: Optional[int] = None):
|
||||||
|
"""Delete all accounts that have no wallets"""
|
||||||
|
async with core_db.connect() as conn:
|
||||||
|
delta = days or settings.cleanup_wallets_days
|
||||||
|
delta = delta * 24 * 60 * 60
|
||||||
|
await delete_accounts_no_wallets(delta, conn)
|
||||||
|
|
||||||
|
|
||||||
@extensions.command("list")
|
@extensions.command("list")
|
||||||
@coro
|
@coro
|
||||||
async def extensions_list():
|
async def extensions_list():
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from time import time
|
from time import time
|
||||||
from typing import Optional, Tuple
|
from typing import Any, Optional, Tuple
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_total_balance, get_wallet
|
from lnbits.core.crud.wallets import get_total_balance, get_wallet
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
@@ -110,8 +110,7 @@ async def get_payments_paginated(
|
|||||||
- complete | pending | failed | outgoing | incoming.
|
- complete | pending | failed | outgoing | incoming.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
values: dict = {
|
values: dict[str, Any] = {
|
||||||
"wallet_id": wallet_id,
|
|
||||||
"time": since,
|
"time": since,
|
||||||
}
|
}
|
||||||
clause: list[str] = []
|
clause: list[str] = []
|
||||||
@@ -120,6 +119,7 @@ async def get_payments_paginated(
|
|||||||
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
||||||
|
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
|
values["wallet_id"] = wallet_id
|
||||||
clause.append("wallet_id = :wallet_id")
|
clause.append("wallet_id = :wallet_id")
|
||||||
|
|
||||||
if complete and pending:
|
if complete and pending:
|
||||||
|
|||||||
+15
-16
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -10,16 +9,16 @@ from lnbits.settings import settings
|
|||||||
|
|
||||||
|
|
||||||
class AuditEntry(BaseModel):
|
class AuditEntry(BaseModel):
|
||||||
component: Optional[str] = None
|
component: str | None = None
|
||||||
ip_address: Optional[str] = None
|
ip_address: str | None = None
|
||||||
user_id: Optional[str] = None
|
user_id: str | None = None
|
||||||
path: Optional[str] = None
|
path: str | None = None
|
||||||
request_type: Optional[str] = None
|
request_type: str | None = None
|
||||||
request_method: Optional[str] = None
|
request_method: str | None = None
|
||||||
request_details: Optional[str] = None
|
request_details: str | None = None
|
||||||
response_code: Optional[str] = None
|
response_code: str | None = None
|
||||||
duration: float
|
duration: float
|
||||||
delete_at: Optional[datetime] = None
|
delete_at: datetime | None = None
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, **data):
|
||||||
@@ -42,12 +41,12 @@ class AuditFilters(FilterModel):
|
|||||||
"duration",
|
"duration",
|
||||||
]
|
]
|
||||||
|
|
||||||
ip_address: Optional[str] = None
|
ip_address: str | None = None
|
||||||
user_id: Optional[str] = None
|
user_id: str | None = None
|
||||||
path: Optional[str] = None
|
path: str | None = None
|
||||||
request_method: Optional[str] = None
|
request_method: str | None = None
|
||||||
response_code: Optional[str] = None
|
response_code: str | None = None
|
||||||
component: Optional[str] = None
|
component: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class AuditCountStat(BaseModel):
|
class AuditCountStat(BaseModel):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -29,17 +29,17 @@ class ExplicitRelease(BaseModel):
|
|||||||
archive: str
|
archive: str
|
||||||
hash: str
|
hash: str
|
||||||
dependencies: list[str] = []
|
dependencies: list[str] = []
|
||||||
repo: Optional[str]
|
repo: str | None
|
||||||
icon: Optional[str]
|
icon: str | None
|
||||||
short_description: Optional[str]
|
short_description: str | None
|
||||||
min_lnbits_version: Optional[str]
|
min_lnbits_version: str | None
|
||||||
max_lnbits_version: Optional[str]
|
max_lnbits_version: str | None
|
||||||
html_url: Optional[str] # todo: release_url
|
html_url: str | None # todo: release_url
|
||||||
warning: Optional[str]
|
warning: str | None
|
||||||
info_notification: Optional[str]
|
info_notification: str | None
|
||||||
critical_notification: Optional[str]
|
critical_notification: str | None
|
||||||
details_link: Optional[str]
|
details_link: str | None
|
||||||
pay_link: Optional[str]
|
pay_link: str | None
|
||||||
|
|
||||||
def is_version_compatible(self):
|
def is_version_compatible(self):
|
||||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
@@ -77,9 +77,9 @@ class ExtensionConfig(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
short_description: str
|
short_description: str
|
||||||
tile: str = ""
|
tile: str = ""
|
||||||
warning: Optional[str] = ""
|
warning: str | None = ""
|
||||||
min_lnbits_version: Optional[str]
|
min_lnbits_version: str | None
|
||||||
max_lnbits_version: Optional[str]
|
max_lnbits_version: str | None
|
||||||
|
|
||||||
def is_version_compatible(self) -> bool:
|
def is_version_compatible(self) -> bool:
|
||||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
@@ -87,7 +87,7 @@ class ExtensionConfig(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def fetch_github_release_config(
|
async def fetch_github_release_config(
|
||||||
cls, org: str, repo: str, tag_name: str
|
cls, org: str, repo: str, tag_name: str
|
||||||
) -> Optional[ExtensionConfig]:
|
) -> ExtensionConfig | None:
|
||||||
config_url = (
|
config_url = (
|
||||||
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
|
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
|
||||||
)
|
)
|
||||||
@@ -97,28 +97,28 @@ class ExtensionConfig(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ReleasePaymentInfo(BaseModel):
|
class ReleasePaymentInfo(BaseModel):
|
||||||
amount: Optional[int] = None
|
amount: int | None = None
|
||||||
pay_link: Optional[str] = None
|
pay_link: str | None = None
|
||||||
payment_hash: Optional[str] = None
|
payment_hash: str | None = None
|
||||||
payment_request: Optional[str] = None
|
payment_request: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PayToEnableInfo(BaseModel):
|
class PayToEnableInfo(BaseModel):
|
||||||
amount: int = 0
|
amount: int = 0
|
||||||
required: bool = False
|
required: bool = False
|
||||||
wallet: Optional[str] = None
|
wallet: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class UserExtensionInfo(BaseModel):
|
class UserExtensionInfo(BaseModel):
|
||||||
paid_to_enable: Optional[bool] = False
|
paid_to_enable: bool | None = False
|
||||||
payment_hash_to_enable: Optional[str] = None
|
payment_hash_to_enable: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class UserExtension(BaseModel):
|
class UserExtension(BaseModel):
|
||||||
user: str
|
user: str
|
||||||
extension: str
|
extension: str
|
||||||
active: bool
|
active: bool
|
||||||
extra: Optional[UserExtensionInfo] = None
|
extra: UserExtensionInfo | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_paid(self) -> bool:
|
def is_paid(self) -> bool:
|
||||||
@@ -140,10 +140,10 @@ class UserExtension(BaseModel):
|
|||||||
class Extension(BaseModel):
|
class Extension(BaseModel):
|
||||||
code: str
|
code: str
|
||||||
is_valid: bool
|
is_valid: bool
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
short_description: Optional[str] = None
|
short_description: str | None = None
|
||||||
tile: Optional[str] = None
|
tile: str | None = None
|
||||||
upgrade_hash: Optional[str] = ""
|
upgrade_hash: str | None = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module_name(self) -> str:
|
def module_name(self) -> str:
|
||||||
@@ -176,21 +176,21 @@ class ExtensionRelease(BaseModel):
|
|||||||
archive: str
|
archive: str
|
||||||
source_repo: str
|
source_repo: str
|
||||||
is_github_release: bool = False
|
is_github_release: bool = False
|
||||||
hash: Optional[str] = None
|
hash: str | None = None
|
||||||
min_lnbits_version: Optional[str] = None
|
min_lnbits_version: str | None = None
|
||||||
max_lnbits_version: Optional[str] = None
|
max_lnbits_version: str | None = None
|
||||||
is_version_compatible: Optional[bool] = True
|
is_version_compatible: bool | None = True
|
||||||
html_url: Optional[str] = None
|
html_url: str | None = None
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
warning: Optional[str] = None
|
warning: str | None = None
|
||||||
repo: Optional[str] = None
|
repo: str | None = None
|
||||||
icon: Optional[str] = None
|
icon: str | None = None
|
||||||
details_link: Optional[str] = None
|
details_link: str | None = None
|
||||||
|
|
||||||
pay_link: Optional[str] = None
|
pay_link: str | None = None
|
||||||
cost_sats: Optional[int] = None
|
cost_sats: int | None = None
|
||||||
paid_sats: Optional[int] = 0
|
paid_sats: int | None = 0
|
||||||
payment_hash: Optional[str] = None
|
payment_hash: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def archive_url(self) -> str:
|
def archive_url(self) -> str:
|
||||||
@@ -208,8 +208,8 @@ class ExtensionRelease(BaseModel):
|
|||||||
self.cost_sats = payment_info.amount if payment_info else None
|
self.cost_sats = payment_info.amount if payment_info else None
|
||||||
|
|
||||||
async def fetch_release_payment_info(
|
async def fetch_release_payment_info(
|
||||||
self, amount: Optional[int] = None
|
self, amount: int | None = None
|
||||||
) -> Optional[ReleasePaymentInfo]:
|
) -> ReleasePaymentInfo | None:
|
||||||
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
|
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
|
||||||
assert url, "Missing URL for payment info."
|
assert url, "Missing URL for payment info."
|
||||||
try:
|
try:
|
||||||
@@ -281,7 +281,7 @@ class ExtensionRelease(BaseModel):
|
|||||||
return [GitHubRepoRelease.parse_obj(r) for r in releases]
|
return [GitHubRepoRelease.parse_obj(r) for r in releases]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def fetch_release_details(cls, details_link: str) -> Optional[dict]:
|
async def fetch_release_details(cls, details_link: str) -> dict | None:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
@@ -300,12 +300,12 @@ class ExtensionRelease(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ExtensionMeta(BaseModel):
|
class ExtensionMeta(BaseModel):
|
||||||
installed_release: Optional[ExtensionRelease] = None
|
installed_release: ExtensionRelease | None = None
|
||||||
latest_release: Optional[ExtensionRelease] = None
|
latest_release: ExtensionRelease | None = None
|
||||||
pay_to_enable: Optional[PayToEnableInfo] = None
|
pay_to_enable: PayToEnableInfo | None = None
|
||||||
payments: list[ReleasePaymentInfo] = []
|
payments: list[ReleasePaymentInfo] = []
|
||||||
dependencies: list[str] = []
|
dependencies: list[str] = []
|
||||||
archive: Optional[str] = None
|
archive: str | None = None
|
||||||
featured: bool = False
|
featured: bool = False
|
||||||
|
|
||||||
|
|
||||||
@@ -313,11 +313,11 @@ class InstallableExtension(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
version: str
|
version: str
|
||||||
active: Optional[bool] = False
|
active: bool | None = False
|
||||||
short_description: Optional[str] = None
|
short_description: str | None = None
|
||||||
icon: Optional[str] = None
|
icon: str | None = None
|
||||||
stars: int = 0
|
stars: int = 0
|
||||||
meta: Optional[ExtensionMeta] = None
|
meta: ExtensionMeta | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hash(self) -> str:
|
def hash(self) -> str:
|
||||||
@@ -452,7 +452,7 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
shutil.rmtree(self.ext_upgrade_dir, True)
|
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:
|
if not release:
|
||||||
return
|
return
|
||||||
if not self.meta or not self.meta.latest_release:
|
if not self.meta or not self.meta.latest_release:
|
||||||
@@ -465,9 +465,7 @@ class InstallableExtension(BaseModel):
|
|||||||
):
|
):
|
||||||
self.meta.latest_release = release
|
self.meta.latest_release = release
|
||||||
|
|
||||||
def find_existing_payment(
|
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
|
||||||
self, pay_link: Optional[str]
|
|
||||||
) -> Optional[ReleasePaymentInfo]:
|
|
||||||
if not pay_link or not self.meta or not self.meta.payments:
|
if not pay_link or not self.meta or not self.meta.payments:
|
||||||
return None
|
return None
|
||||||
return next(
|
return next(
|
||||||
@@ -507,7 +505,7 @@ class InstallableExtension(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def from_github_release(
|
async def from_github_release(
|
||||||
cls, github_release: GitHubRelease
|
cls, github_release: GitHubRelease
|
||||||
) -> Optional[InstallableExtension]:
|
) -> InstallableExtension | None:
|
||||||
try:
|
try:
|
||||||
repo, latest_release, config = await cls.fetch_github_repo_info(
|
repo, latest_release, config = await cls.fetch_github_repo_info(
|
||||||
github_release.organisation, github_release.repository
|
github_release.organisation, github_release.repository
|
||||||
@@ -546,7 +544,7 @@ class InstallableExtension(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_ext_dir(cls, ext_id: str) -> Optional[InstallableExtension]:
|
def from_ext_dir(cls, ext_id: str) -> InstallableExtension | None:
|
||||||
try:
|
try:
|
||||||
conf_path = Path(
|
conf_path = Path(
|
||||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||||
@@ -657,7 +655,7 @@ class InstallableExtension(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def get_extension_release(
|
async def get_extension_release(
|
||||||
cls, ext_id: str, source_repo: str, archive: str, version: str
|
cls, ext_id: str, source_repo: str, archive: str, version: str
|
||||||
) -> Optional[ExtensionRelease]:
|
) -> ExtensionRelease | None:
|
||||||
all_releases: list[ExtensionRelease] = (
|
all_releases: list[ExtensionRelease] = (
|
||||||
await InstallableExtension.get_extension_releases(ext_id)
|
await InstallableExtension.get_extension_releases(ext_id)
|
||||||
)
|
)
|
||||||
@@ -708,8 +706,8 @@ class CreateExtension(BaseModel):
|
|||||||
archive: str
|
archive: str
|
||||||
source_repo: str
|
source_repo: str
|
||||||
version: str
|
version: str
|
||||||
cost_sats: Optional[int] = 0
|
cost_sats: int | None = 0
|
||||||
payment_hash: Optional[str] = None
|
payment_hash: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class ExtensionDetailsRequest(BaseModel):
|
class ExtensionDetailsRequest(BaseModel):
|
||||||
@@ -718,7 +716,7 @@ class ExtensionDetailsRequest(BaseModel):
|
|||||||
version: str
|
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}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
if settings.lnbits_ext_github_token:
|
if settings.lnbits_ext_github_token:
|
||||||
headers["Authorization"] = f"Bearer {settings.lnbits_ext_github_token}"
|
headers["Authorization"] = f"Bearer {settings.lnbits_ext_github_token}"
|
||||||
@@ -730,7 +728,7 @@ async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
|
|||||||
return resp.json()
|
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:
|
if not path:
|
||||||
return ""
|
return ""
|
||||||
_, _, *rest = path.split("/")
|
_, _, *rest = path.split("/")
|
||||||
|
|||||||
@@ -46,3 +46,8 @@ class SimpleItem(BaseModel):
|
|||||||
class DbVersion(BaseModel):
|
class DbVersion(BaseModel):
|
||||||
db: str
|
db: str
|
||||||
version: int
|
version: int
|
||||||
|
|
||||||
|
|
||||||
|
class Image(BaseModel):
|
||||||
|
filename: str
|
||||||
|
directory: str = "library"
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ NOTIFICATION_TEMPLATES = {
|
|||||||
*In/Out payments*: `{in_payments_count}`/`{out_payments_count}`.
|
*In/Out payments*: `{in_payments_count}`/`{out_payments_count}`.
|
||||||
*Pending payments*: `{pending_payments_count}`.
|
*Pending payments*: `{pending_payments_count}`.
|
||||||
*Failed payments*: `{failed_payments_count}`.
|
*Failed payments*: `{failed_payments_count}`.
|
||||||
*LNbits balance*: `{lnbits_balance_sats}` sats.""",
|
*LNbits balance*: `{lnbits_balance_sats}` sats.
|
||||||
|
*Node balance*: `{node_balance_sats}` sats.""",
|
||||||
"server_start_stop": """*SERVER*
|
"server_start_stop": """*SERVER*
|
||||||
{message}
|
{message}
|
||||||
*Time*: `{up_time}` seconds.
|
*Time*: `{up_time}` seconds.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Literal, Optional
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
@@ -28,16 +28,16 @@ class PaymentState(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class PaymentExtra(BaseModel):
|
class PaymentExtra(BaseModel):
|
||||||
comment: Optional[str] = None
|
comment: str | None = None
|
||||||
success_action: Optional[str] = None
|
success_action: str | None = None
|
||||||
lnurl_response: Optional[str] = None
|
lnurl_response: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PayInvoice(BaseModel):
|
class PayInvoice(BaseModel):
|
||||||
payment_request: str
|
payment_request: str
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
max_sat: Optional[int] = None
|
max_sat: int | None = None
|
||||||
extra: Optional[dict] = {}
|
extra: dict | None = {}
|
||||||
|
|
||||||
|
|
||||||
class CreatePayment(BaseModel):
|
class CreatePayment(BaseModel):
|
||||||
@@ -46,10 +46,10 @@ class CreatePayment(BaseModel):
|
|||||||
bolt11: str
|
bolt11: str
|
||||||
amount_msat: int
|
amount_msat: int
|
||||||
memo: str
|
memo: str
|
||||||
extra: Optional[dict] = {}
|
extra: dict | None = {}
|
||||||
preimage: Optional[str] = None
|
preimage: str | None = None
|
||||||
expiry: Optional[datetime] = None
|
expiry: datetime | None = None
|
||||||
webhook: Optional[str] = None
|
webhook: str | None = None
|
||||||
fee: int = 0
|
fee: int = 0
|
||||||
|
|
||||||
|
|
||||||
@@ -61,13 +61,13 @@ class Payment(BaseModel):
|
|||||||
fee: int
|
fee: int
|
||||||
bolt11: str
|
bolt11: str
|
||||||
status: str = PaymentState.PENDING
|
status: str = PaymentState.PENDING
|
||||||
memo: Optional[str] = None
|
memo: str | None = None
|
||||||
expiry: Optional[datetime] = None
|
expiry: datetime | None = None
|
||||||
webhook: Optional[str] = None
|
webhook: str | None = None
|
||||||
webhook_status: Optional[int] = None
|
webhook_status: int | None = None
|
||||||
preimage: Optional[str] = None
|
preimage: str | None = None
|
||||||
tag: Optional[str] = None
|
tag: str | None = None
|
||||||
extension: Optional[str] = None
|
extension: str | None = None
|
||||||
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
@@ -129,16 +129,16 @@ class PaymentFilters(FilterModel):
|
|||||||
|
|
||||||
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
||||||
|
|
||||||
status: Optional[str]
|
status: str | None
|
||||||
tag: Optional[str]
|
tag: str | None
|
||||||
checking_id: Optional[str]
|
checking_id: str | None
|
||||||
amount: int
|
amount: int
|
||||||
fee: int
|
fee: int
|
||||||
memo: Optional[str]
|
memo: str | None
|
||||||
time: datetime
|
time: datetime
|
||||||
preimage: Optional[str]
|
preimage: str | None
|
||||||
payment_hash: Optional[str]
|
payment_hash: str | None
|
||||||
wallet_id: Optional[str]
|
wallet_id: str | None
|
||||||
|
|
||||||
|
|
||||||
class PaymentDataPoint(BaseModel):
|
class PaymentDataPoint(BaseModel):
|
||||||
@@ -173,11 +173,11 @@ class PaymentWalletStats(BaseModel):
|
|||||||
class PaymentDailyStats(BaseModel):
|
class PaymentDailyStats(BaseModel):
|
||||||
date: datetime
|
date: datetime
|
||||||
balance: float = 0
|
balance: float = 0
|
||||||
balance_in: Optional[float] = 0
|
balance_in: float | None = 0
|
||||||
balance_out: Optional[float] = 0
|
balance_out: float | None = 0
|
||||||
payments_count: int = 0
|
payments_count: int = 0
|
||||||
count_in: Optional[int] = 0
|
count_in: int | None = 0
|
||||||
count_out: Optional[int] = 0
|
count_out: int | None = 0
|
||||||
fee: float = 0
|
fee: float = 0
|
||||||
|
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ class PaymentHistoryPoint(BaseModel):
|
|||||||
|
|
||||||
class DecodePayment(BaseModel):
|
class DecodePayment(BaseModel):
|
||||||
data: str
|
data: str
|
||||||
filter_fields: Optional[list[str]] = []
|
filter_fields: list[str] | None = []
|
||||||
|
|
||||||
|
|
||||||
class CreateInvoice(BaseModel):
|
class CreateInvoice(BaseModel):
|
||||||
@@ -198,14 +198,14 @@ class CreateInvoice(BaseModel):
|
|||||||
internal: bool = False
|
internal: bool = False
|
||||||
out: bool = True
|
out: bool = True
|
||||||
amount: float = Query(None, ge=0)
|
amount: float = Query(None, ge=0)
|
||||||
memo: Optional[str] = None
|
memo: str | None = Query(None, max_length=640)
|
||||||
description_hash: Optional[str] = None
|
description_hash: str | None = None
|
||||||
unhashed_description: Optional[str] = None
|
unhashed_description: str | None = None
|
||||||
expiry: Optional[int] = None
|
expiry: int | None = None
|
||||||
extra: Optional[dict] = None
|
extra: dict | None = None
|
||||||
webhook: Optional[str] = None
|
webhook: str | None = None
|
||||||
bolt11: Optional[str] = None
|
bolt11: str | None = None
|
||||||
lnurl_callback: Optional[str] = None
|
lnurl_callback: str | None = None
|
||||||
|
|
||||||
@validator("unit")
|
@validator("unit")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
+43
-44
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
@@ -17,16 +16,16 @@ from .wallets import Wallet
|
|||||||
|
|
||||||
|
|
||||||
class UserExtra(BaseModel):
|
class UserExtra(BaseModel):
|
||||||
email_verified: Optional[bool] = False
|
email_verified: bool | None = False
|
||||||
first_name: Optional[str] = None
|
first_name: str | None = None
|
||||||
last_name: Optional[str] = None
|
last_name: str | None = None
|
||||||
display_name: Optional[str] = None
|
display_name: str | None = None
|
||||||
picture: Optional[str] = None
|
picture: str | None = None
|
||||||
# Auth provider, possible values:
|
# Auth provider, possible values:
|
||||||
# - "env": the user was created automatically by the system
|
# - "env": the user was created automatically by the system
|
||||||
# - "lnbits": the user was created via register form (username/pass or user_id only)
|
# - "lnbits": the user was created via register form (username/pass or user_id only)
|
||||||
# - "google | github | ...": the user was created using an SSO provider
|
# - "google | github | ...": the user was created using an SSO provider
|
||||||
provider: Optional[str] = "lnbits" # auth provider
|
provider: str | None = "lnbits" # auth provider
|
||||||
|
|
||||||
|
|
||||||
class EndpointAccess(BaseModel):
|
class EndpointAccess(BaseModel):
|
||||||
@@ -50,13 +49,13 @@ class AccessControlList(BaseModel):
|
|||||||
endpoints: list[EndpointAccess] = []
|
endpoints: list[EndpointAccess] = []
|
||||||
token_id_list: list[SimpleItem] = []
|
token_id_list: list[SimpleItem] = []
|
||||||
|
|
||||||
def get_endpoint(self, path: str) -> Optional[EndpointAccess]:
|
def get_endpoint(self, path: str) -> EndpointAccess | None:
|
||||||
for e in self.endpoints:
|
for e in self.endpoints:
|
||||||
if e.path == path:
|
if e.path == path:
|
||||||
return e
|
return e
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_token_by_id(self, token_id: str) -> Optional[SimpleItem]:
|
def get_token_by_id(self, token_id: str) -> SimpleItem | None:
|
||||||
for t in self.token_id_list:
|
for t in self.token_id_list:
|
||||||
if t.id == token_id:
|
if t.id == token_id:
|
||||||
return t
|
return t
|
||||||
@@ -71,7 +70,7 @@ class UserAcls(BaseModel):
|
|||||||
access_control_list: list[AccessControlList] = []
|
access_control_list: list[AccessControlList] = []
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
def get_acl_by_id(self, acl_id: str) -> Optional[AccessControlList]:
|
def get_acl_by_id(self, acl_id: str) -> AccessControlList | None:
|
||||||
for acl in self.access_control_list:
|
for acl in self.access_control_list:
|
||||||
if acl.id == acl_id:
|
if acl.id == acl_id:
|
||||||
return acl
|
return acl
|
||||||
@@ -82,7 +81,7 @@ class UserAcls(BaseModel):
|
|||||||
acl for acl in self.access_control_list if acl.id != acl_id
|
acl for acl in self.access_control_list if acl.id != acl_id
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_acl_by_token_id(self, token_id: str) -> Optional[AccessControlList]:
|
def get_acl_by_token_id(self, token_id: str) -> AccessControlList | None:
|
||||||
for acl in self.access_control_list:
|
for acl in self.access_control_list:
|
||||||
if acl.get_token_by_id(token_id):
|
if acl.get_token_by_id(token_id):
|
||||||
return acl
|
return acl
|
||||||
@@ -91,10 +90,10 @@ class UserAcls(BaseModel):
|
|||||||
|
|
||||||
class Account(BaseModel):
|
class Account(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
username: Optional[str] = None
|
username: str | None = None
|
||||||
password_hash: Optional[str] = None
|
password_hash: str | None = None
|
||||||
pubkey: Optional[str] = None
|
pubkey: str | None = None
|
||||||
email: Optional[str] = None
|
email: str | None = None
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
@@ -134,10 +133,10 @@ class Account(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
transaction_count: Optional[int] = 0
|
transaction_count: int | None = 0
|
||||||
wallet_count: Optional[int] = 0
|
wallet_count: int | None = 0
|
||||||
balance_msat: Optional[int] = 0
|
balance_msat: int | None = 0
|
||||||
last_payment: Optional[datetime] = None
|
last_payment: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
class AccountFilters(FilterModel):
|
class AccountFilters(FilterModel):
|
||||||
@@ -151,20 +150,20 @@ class AccountFilters(FilterModel):
|
|||||||
"last_payment",
|
"last_payment",
|
||||||
]
|
]
|
||||||
|
|
||||||
email: Optional[str] = None
|
email: str | None = None
|
||||||
user: Optional[str] = None
|
user: str | None = None
|
||||||
username: Optional[str] = None
|
username: str | None = None
|
||||||
pubkey: Optional[str] = None
|
pubkey: str | None = None
|
||||||
wallet_id: Optional[str] = None
|
wallet_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
email: Optional[str] = None
|
email: str | None = None
|
||||||
username: Optional[str] = None
|
username: str | None = None
|
||||||
pubkey: Optional[str] = None
|
pubkey: str | None = None
|
||||||
extensions: list[str] = []
|
extensions: list[str] = []
|
||||||
wallets: list[Wallet] = []
|
wallets: list[Wallet] = []
|
||||||
admin: bool = False
|
admin: bool = False
|
||||||
@@ -176,7 +175,7 @@ class User(BaseModel):
|
|||||||
def wallet_ids(self) -> list[str]:
|
def wallet_ids(self) -> list[str]:
|
||||||
return [wallet.id for wallet in self.wallets]
|
return [wallet.id for wallet in self.wallets]
|
||||||
|
|
||||||
def get_wallet(self, wallet_id: str) -> Optional[Wallet]:
|
def get_wallet(self, wallet_id: str) -> Wallet | None:
|
||||||
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
||||||
return w[0] if w else None
|
return w[0] if w else None
|
||||||
|
|
||||||
@@ -192,33 +191,33 @@ class User(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class RegisterUser(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)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
|
||||||
|
|
||||||
class CreateUser(BaseModel):
|
class CreateUser(BaseModel):
|
||||||
id: Optional[str] = Query(default=None)
|
id: str | None = Query(default=None)
|
||||||
email: Optional[str] = Query(default=None)
|
email: str | None = Query(default=None)
|
||||||
username: Optional[str] = Query(default=None, min_length=2, max_length=20)
|
username: str | None = Query(default=None, min_length=2, max_length=20)
|
||||||
password: Optional[str] = Query(default=None, min_length=8, max_length=50)
|
password: str | None = Query(default=None, min_length=8, max_length=50)
|
||||||
password_repeat: Optional[str] = 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)
|
pubkey: str = Query(default=None, max_length=64)
|
||||||
extensions: Optional[list[str]] = None
|
extensions: list[str] | None = None
|
||||||
extra: Optional[UserExtra] = None
|
extra: UserExtra | None = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateUser(BaseModel):
|
class UpdateUser(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
email: Optional[str] = Query(default=None)
|
email: str | None = Query(default=None)
|
||||||
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
|
username: str | None = Query(default=..., min_length=2, max_length=20)
|
||||||
extra: Optional[UserExtra] = None
|
extra: UserExtra | None = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserPassword(BaseModel):
|
class UpdateUserPassword(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
password_old: Optional[str] = None
|
password_old: str | None = None
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
@@ -252,10 +251,10 @@ class LoginUsernamePassword(BaseModel):
|
|||||||
|
|
||||||
class AccessTokenPayload(BaseModel):
|
class AccessTokenPayload(BaseModel):
|
||||||
sub: str
|
sub: str
|
||||||
usr: Optional[str] = None
|
usr: str | None = None
|
||||||
email: Optional[str] = None
|
email: str | None = None
|
||||||
auth_time: Optional[int] = 0
|
auth_time: int | None = 0
|
||||||
api_token_id: Optional[str] = None
|
api_token_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateBalance(BaseModel):
|
class UpdateBalance(BaseModel):
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import hmac
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from ecdsa import SECP256k1, SigningKey
|
from ecdsa import SECP256k1, SigningKey
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -37,7 +36,7 @@ class Wallet(BaseModel):
|
|||||||
deleted: bool = False
|
deleted: bool = False
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
currency: Optional[str] = None
|
currency: str | None = None
|
||||||
balance_msat: int = Field(default=0, no_database=True)
|
balance_msat: int = Field(default=0, no_database=True)
|
||||||
extra: WalletExtra = WalletExtra()
|
extra: WalletExtra = WalletExtra()
|
||||||
|
|
||||||
@@ -67,7 +66,7 @@ class Wallet(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CreateWallet(BaseModel):
|
class CreateWallet(BaseModel):
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class KeyType(Enum):
|
class KeyType(Enum):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from .funding_source import (
|
|||||||
switch_to_voidwallet,
|
switch_to_voidwallet,
|
||||||
)
|
)
|
||||||
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
||||||
from .notifications import enqueue_notification
|
from .notifications import enqueue_notification, send_payment_notification
|
||||||
from .payments import (
|
from .payments import (
|
||||||
calculate_fiat_amounts,
|
calculate_fiat_amounts,
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
@@ -12,7 +12,6 @@ from .payments import (
|
|||||||
fee_reserve,
|
fee_reserve,
|
||||||
fee_reserve_total,
|
fee_reserve_total,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
send_payment_notification,
|
|
||||||
service_fee,
|
service_fee,
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
update_wallet_balance,
|
update_wallet_balance,
|
||||||
@@ -25,7 +24,6 @@ from .users import (
|
|||||||
check_admin_settings,
|
check_admin_settings,
|
||||||
create_user_account,
|
create_user_account,
|
||||||
create_user_account_no_ckeck,
|
create_user_account_no_ckeck,
|
||||||
init_admin_settings,
|
|
||||||
update_user_account,
|
update_user_account,
|
||||||
update_user_extensions,
|
update_user_extensions,
|
||||||
)
|
)
|
||||||
@@ -40,6 +38,7 @@ __all__ = [
|
|||||||
"perform_lnurlauth",
|
"perform_lnurlauth",
|
||||||
# notifications
|
# notifications
|
||||||
"enqueue_notification",
|
"enqueue_notification",
|
||||||
|
"send_payment_notification",
|
||||||
# payments
|
# payments
|
||||||
"calculate_fiat_amounts",
|
"calculate_fiat_amounts",
|
||||||
"check_transaction_status",
|
"check_transaction_status",
|
||||||
@@ -48,7 +47,6 @@ __all__ = [
|
|||||||
"fee_reserve",
|
"fee_reserve",
|
||||||
"fee_reserve_total",
|
"fee_reserve_total",
|
||||||
"pay_invoice",
|
"pay_invoice",
|
||||||
"send_payment_notification",
|
|
||||||
"service_fee",
|
"service_fee",
|
||||||
"update_pending_payments",
|
"update_pending_payments",
|
||||||
"update_wallet_balance",
|
"update_wallet_balance",
|
||||||
@@ -59,7 +57,6 @@ __all__ = [
|
|||||||
"check_admin_settings",
|
"check_admin_settings",
|
||||||
"create_user_account",
|
"create_user_account",
|
||||||
"create_user_account_no_ckeck",
|
"create_user_account_no_ckeck",
|
||||||
"init_admin_settings",
|
|
||||||
"update_user_account",
|
"update_user_account",
|
||||||
"update_user_extensions",
|
"update_user_extensions",
|
||||||
# websockets
|
# websockets
|
||||||
|
|||||||
@@ -1,15 +1,30 @@
|
|||||||
import asyncio
|
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
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
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 (
|
from lnbits.core.models.notifications import (
|
||||||
NOTIFICATION_TEMPLATES,
|
NOTIFICATION_TEMPLATES,
|
||||||
NotificationMessage,
|
NotificationMessage,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
)
|
)
|
||||||
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
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.settings import settings
|
||||||
from lnbits.utils.nostr import normalize_private_key
|
from lnbits.utils.nostr import normalize_private_key
|
||||||
|
|
||||||
@@ -50,6 +65,12 @@ async def send_notification(
|
|||||||
logger.debug(f"Sent nostr notification: {message_type}")
|
logger.debug(f"Sent nostr notification: {message_type}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending nostr notification {message_type}: {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:
|
async def send_nostr_notification(message: str) -> dict:
|
||||||
@@ -90,6 +111,58 @@ async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
|
|||||||
return response.json()
|
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:
|
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
||||||
if message_type == NotificationType.balance_update:
|
if message_type == NotificationType.balance_update:
|
||||||
return settings.lnbits_notification_credit_debit
|
return settings.lnbits_notification_credit_debit
|
||||||
@@ -123,3 +196,135 @@ def _notification_message_to_text(
|
|||||||
text = meesage_value
|
text = meesage_value
|
||||||
text = f"""[{settings.lnbits_site_title}]\n{text}"""
|
text = f"""[{settings.lnbits_site_title}]\n{text}"""
|
||||||
return message_type, 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,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -12,8 +11,6 @@ from loguru import logger
|
|||||||
from lnbits.core.crud.payments import get_daily_stats
|
from lnbits.core.crud.payments import get_daily_stats
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
||||||
from lnbits.core.models.notifications import NotificationType
|
|
||||||
from lnbits.core.services.notifications import enqueue_notification
|
|
||||||
from lnbits.db import Connection, Filters
|
from lnbits.db import Connection, Filters
|
||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError
|
from lnbits.exceptions import InvoiceError, PaymentError
|
||||||
@@ -45,7 +42,7 @@ from ..models import (
|
|||||||
PaymentState,
|
PaymentState,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from .websockets import websocket_manager
|
from .notifications import send_payment_notification
|
||||||
|
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
@@ -111,7 +108,7 @@ async def create_invoice(
|
|||||||
if not user_wallet:
|
if not user_wallet:
|
||||||
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
|
|
||||||
invoice_memo = None if description_hash else memo
|
invoice_memo = None if description_hash else memo[:640]
|
||||||
|
|
||||||
# use the fake wallet if the invoice is for internal use only
|
# use the fake wallet if the invoice is for internal use only
|
||||||
funding_source = fake_wallet if internal else get_funding_source()
|
funding_source = fake_wallet if internal else get_funding_source()
|
||||||
@@ -181,13 +178,20 @@ async def update_pending_payments(wallet_id: str):
|
|||||||
exclude_uncheckable=True,
|
exclude_uncheckable=True,
|
||||||
)
|
)
|
||||||
for payment in pending_payments:
|
for payment in pending_payments:
|
||||||
status = await payment.check_status()
|
await update_pending_payment(payment)
|
||||||
if status.failed:
|
|
||||||
payment.status = PaymentState.FAILED
|
|
||||||
await update_payment(payment)
|
async def update_pending_payment(payment: Payment) -> bool:
|
||||||
elif status.success:
|
status = await payment.check_status()
|
||||||
payment.status = PaymentState.SUCCESS
|
if status.failed:
|
||||||
await update_payment(payment)
|
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:
|
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
||||||
@@ -278,60 +282,6 @@ async def update_wallet_balance(
|
|||||||
await internal_invoice_queue.put(payment.checking_id)
|
await internal_invoice_queue.put(payment.checking_id)
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
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}), 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 check_wallet_limits(
|
async def check_wallet_limits(
|
||||||
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
|
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from lnbits.core.models.extensions import UserExtension
|
|||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
SuperSettings,
|
SuperSettings,
|
||||||
send_admin_user_to_saas,
|
|
||||||
settings,
|
settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,7 +46,9 @@ async def create_user_account(
|
|||||||
|
|
||||||
|
|
||||||
async def create_user_account_no_ckeck(
|
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:
|
) -> User:
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
@@ -70,9 +71,13 @@ async def create_user_account_no_ckeck(
|
|||||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
for ext_id in settings.lnbits_user_default_extensions:
|
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
|
||||||
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
|
for ext_id in user_extensions:
|
||||||
await update_user_extension(user_ext)
|
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)
|
user = await get_user_from_account(account)
|
||||||
assert user, "Cannot find user for 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:
|
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
|
||||||
file.write(settings.super_user)
|
file.write(settings.super_user)
|
||||||
|
|
||||||
# callback for saas
|
|
||||||
if (
|
|
||||||
settings.lnbits_saas_callback
|
|
||||||
and settings.lnbits_saas_secret
|
|
||||||
and settings.lnbits_saas_instance_id
|
|
||||||
):
|
|
||||||
send_admin_user_to_saas()
|
|
||||||
|
|
||||||
account = await get_account(settings.super_user)
|
account = await get_account(settings.super_user)
|
||||||
if account and account.extra and account.extra.provider == "env":
|
if account and account.extra and account.extra.provider == "env":
|
||||||
settings.first_install = True
|
settings.first_install = True
|
||||||
|
|||||||
+3
-65
@@ -2,25 +2,19 @@ import asyncio
|
|||||||
import traceback
|
import traceback
|
||||||
from typing import Callable, Coroutine
|
from typing import Callable, Coroutine
|
||||||
|
|
||||||
import httpx
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
from lnbits.core.crud import (
|
||||||
create_audit_entry,
|
create_audit_entry,
|
||||||
get_wallet,
|
get_wallet,
|
||||||
get_webpush_subscriptions_for_user,
|
|
||||||
mark_webhook_sent,
|
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.audit import delete_expired_audit_entries
|
from lnbits.core.crud.audit import delete_expired_audit_entries
|
||||||
from lnbits.core.crud.payments import get_payments_status_count
|
from lnbits.core.crud.payments import get_payments_status_count
|
||||||
from lnbits.core.crud.users import get_accounts
|
from lnbits.core.crud.users import get_accounts
|
||||||
from lnbits.core.crud.wallets import get_wallets_count
|
from lnbits.core.crud.wallets import get_wallets_count
|
||||||
from lnbits.core.models import AuditEntry, Payment
|
from lnbits.core.models import AuditEntry
|
||||||
from lnbits.core.models.extensions import InstallableExtension
|
from lnbits.core.models.extensions import InstallableExtension
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services import (
|
|
||||||
send_payment_notification,
|
|
||||||
)
|
|
||||||
from lnbits.core.services.funding_source import (
|
from lnbits.core.services.funding_source import (
|
||||||
check_balance_delta_changed,
|
check_balance_delta_changed,
|
||||||
check_server_balance_against_node,
|
check_server_balance_against_node,
|
||||||
@@ -29,11 +23,11 @@ from lnbits.core.services.funding_source import (
|
|||||||
from lnbits.core.services.notifications import (
|
from lnbits.core.services.notifications import (
|
||||||
enqueue_notification,
|
enqueue_notification,
|
||||||
process_next_notification,
|
process_next_notification,
|
||||||
|
send_payment_notification,
|
||||||
)
|
)
|
||||||
from lnbits.db import Filters
|
from lnbits.db import Filters
|
||||||
from lnbits.helpers import check_callback_url
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.tasks import create_unique_task, send_push_notification
|
from lnbits.tasks import create_unique_task
|
||||||
from lnbits.utils.exchange_rates import btc_rates
|
from lnbits.utils.exchange_rates import btc_rates
|
||||||
|
|
||||||
audit_queue: asyncio.Queue = asyncio.Queue()
|
audit_queue: asyncio.Queue = asyncio.Queue()
|
||||||
@@ -106,62 +100,6 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
|||||||
wallet = await get_wallet(payment.wallet_id)
|
wallet = await get_wallet(payment.wallet_id)
|
||||||
if wallet:
|
if wallet:
|
||||||
await send_payment_notification(wallet, payment)
|
await send_payment_notification(wallet, payment)
|
||||||
# dispatch webhook
|
|
||||||
if payment.webhook and not payment.webhook_status:
|
|
||||||
await dispatch_webhook(payment)
|
|
||||||
# dispatch push notification
|
|
||||||
await send_payment_push_notification(payment)
|
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_webhook(payment: Payment):
|
|
||||||
"""
|
|
||||||
Dispatches the webhook to the webhook url.
|
|
||||||
"""
|
|
||||||
logger.debug("sending webhook", payment.webhook)
|
|
||||||
|
|
||||||
if not payment.webhook:
|
|
||||||
return await mark_webhook_sent(payment.payment_hash, -1)
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
|
||||||
data = payment.dict()
|
|
||||||
try:
|
|
||||||
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_push_notification(payment: Payment):
|
|
||||||
wallet = await get_wallet(payment.wallet_id)
|
|
||||||
|
|
||||||
if wallet:
|
|
||||||
subscriptions = await get_webpush_subscriptions_for_user(wallet.user)
|
|
||||||
|
|
||||||
amount = int(payment.amount / 1000)
|
|
||||||
|
|
||||||
title = f"LNbits: {wallet.name}"
|
|
||||||
body = f"You just received {amount} sat{'s'[:amount^1]}!"
|
|
||||||
|
|
||||||
if payment.memo:
|
|
||||||
body += f"\r\n{payment.memo}"
|
|
||||||
|
|
||||||
for subscription in subscriptions:
|
|
||||||
# todo: review permissions when user-id-only not allowed
|
|
||||||
# todo: replace all this logic with websockets?
|
|
||||||
url = (
|
|
||||||
f"https://{subscription.host}/wallet?usr={wallet.user}&wal={wallet.id}"
|
|
||||||
)
|
|
||||||
await send_push_notification(subscription, title, body, url)
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_audit_data():
|
async def wait_for_audit_data():
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -2,7 +2,14 @@
|
|||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none q-mb-sm">
|
<h6 class="q-my-none q-mb-sm">
|
||||||
<span v-text="$t('notifications_configure')"></span>
|
<span v-text="$t('notifications_configure')"></span>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click="hideInputsToggle()"
|
||||||
|
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
|
||||||
|
></q-btn>
|
||||||
</h6>
|
</h6>
|
||||||
|
|
||||||
<q-separator class="q-mt-md q-mb-sm"></q-separator>
|
<q-separator class="q-mt-md q-mb-sm"></q-separator>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-sm-12 col-md-6">
|
<div class="col-sm-12 col-md-6">
|
||||||
@@ -39,12 +46,13 @@
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-input
|
<q-input
|
||||||
type="password"
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_nostr_notifications_private_key"
|
v-model="formData.lnbits_nostr_notifications_private_key"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
@@ -117,7 +125,7 @@
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-input
|
<q-input
|
||||||
type="password"
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_telegram_notifications_access_token"
|
v-model="formData.lnbits_telegram_notifications_access_token"
|
||||||
/>
|
/>
|
||||||
@@ -139,6 +147,174 @@
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-separator> </q-separator>
|
<q-separator> </q-separator>
|
||||||
<h6 class="q-mb-sm">
|
<h6 class="q-mb-sm">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_site_title"
|
v-model="formData.lnbits_site_title"
|
||||||
:label="$t('ui_site_title')"
|
:label="$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')"
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
@@ -53,16 +53,6 @@
|
|||||||
:label="$t('lnbits_wallet')"
|
:label="$t('lnbits_wallet')"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
<p><span v-text="$t('denomination')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="text"
|
|
||||||
v-model="formData.lnbits_denomination"
|
|
||||||
label="sats"
|
|
||||||
:hint="$t('denomination_hint')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
||||||
<q-input
|
<q-input
|
||||||
|
|||||||
@@ -149,6 +149,14 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
><span v-text="$t('audit')"></span></q-tooltip
|
><span v-text="$t('audit')"></span></q-tooltip
|
||||||
></q-tab>
|
></q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="library"
|
||||||
|
icon="image"
|
||||||
|
:label="$q.screen.gt.sm ? $t('library') : null"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('library')"></span></q-tooltip
|
||||||
|
></q-tab>
|
||||||
<q-tab
|
<q-tab
|
||||||
style="word-break: break-all"
|
style="word-break: break-all"
|
||||||
name="site_customisation"
|
name="site_customisation"
|
||||||
@@ -177,7 +185,8 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
"admin/_tab_extensions.html" %} {% include
|
"admin/_tab_extensions.html" %} {% include
|
||||||
"admin/_tab_notifications.html" %} {% include
|
"admin/_tab_notifications.html" %} {% include
|
||||||
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
"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-tab-panels>
|
||||||
</q-form>
|
</q-form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -85,7 +85,12 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
|
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"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">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 200 OK (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
@@ -96,7 +101,8 @@
|
|||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
|
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
|
||||||
<i v-text="wallet.inkey"></i>"</code
|
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i
|
||||||
|
>"</code
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -112,7 +118,12 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
|
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"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>
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
<code
|
<code
|
||||||
>{"out": false, "amount": <int>, "memo": <string>,
|
>{"out": false, "amount": <int>, "memo": <string>,
|
||||||
@@ -130,8 +141,9 @@
|
|||||||
<code
|
<code
|
||||||
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
||||||
'{"out": false, "amount": <int>, "memo": <string>}' -H
|
'{"out": false, "amount": <int>, "memo": <string>}' -H
|
||||||
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
|
"X-Api-Key:
|
||||||
application/json"</code
|
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
||||||
|
"Content-type: application/json"</code
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -218,7 +230,12 @@
|
|||||||
/api/v1/payments/<payment_hash></code
|
/api/v1/payments/<payment_hash></code
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"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">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 200 OK (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
@@ -227,11 +244,33 @@
|
|||||||
<code
|
<code
|
||||||
>curl -X GET
|
>curl -X GET
|
||||||
<span v-text="baseUrl"></span>api/v1/payments/<payment_hash> -H
|
<span v-text="baseUrl"></span>api/v1/payments/<payment_hash> -H
|
||||||
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
|
"X-Api-Key:
|
||||||
application/json"</code
|
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
||||||
|
"Content-type: application/json"</code
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-pink">WS</span>
|
||||||
|
/api/v1/ws/<invoice_key></code
|
||||||
|
>
|
||||||
|
<h5
|
||||||
|
class="text-caption q-mt-sm q-mb-none"
|
||||||
|
v-text="$t('websocket_example')"
|
||||||
|
></h5>
|
||||||
|
<code
|
||||||
|
>wscat -c <span v-text="websocketUrl"></span>/<span
|
||||||
|
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||||
|
></span
|
||||||
|
></code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 200 OK (application/json)/payments
|
||||||
|
</h5>
|
||||||
|
<code>{"balance": <int>, "payment": <object>}</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="q-my-none">
|
<h4 class="q-my-none">
|
||||||
<span v-text="$t('password_config')"></span>
|
<span v-text="$t('password')"></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
:label="$t('change_password')"
|
:label="$t('update_password')"
|
||||||
>
|
>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="col q-mb-sm">
|
<div class="col q-mb-sm">
|
||||||
<h4 class="q-my-none">
|
<h4 class="q-my-none">
|
||||||
<span v-text="$t('pubkey')"></span>
|
Nostr <span v-text="$t('pubkey')"></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showUpdateCredentials()"
|
@click="showUpdateCredentials()"
|
||||||
:label="$t('update_credentials')"
|
:label="$t('change_password')"
|
||||||
filled
|
filled
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('bitcoin')"
|
@click="themeChoiceFunc('bitcoin')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="orange"
|
color="deep-orange"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>bitcoin</q-tooltip>
|
><q-tooltip>bitcoin</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
:type="loginData.isPwdRepeat ? 'password' : 'text'"
|
:type="loginData.isPwdRepeat ? 'password' : 'text'"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:label="$t('password_repeat')"
|
:label="$t('password_repeat')"
|
||||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || 'Passwords_dont_match']"
|
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || $t('invalid_password_repeat')]"
|
||||||
><template v-slot:append>
|
><template v-slot:append>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
|
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
|
||||||
|
|||||||
@@ -59,6 +59,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script>
|
||||||
|
const ALLOWED_REGISTER = {{ LNBITS_NEW_ACCOUNTS_ALLOWED | tojson }};
|
||||||
|
</script>
|
||||||
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
|
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
|
||||||
{% endblock %} {% block page_container %}
|
{% endblock %} {% block page_container %}
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
@@ -84,18 +87,20 @@
|
|||||||
>
|
>
|
||||||
{{SITE_TITLE}}
|
{{SITE_TITLE}}
|
||||||
</h5>
|
</h5>
|
||||||
<h6
|
<template v-if="$q.screen.gt.sm">
|
||||||
class="q-my-sm"
|
<h6
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
class="q-my-sm"
|
||||||
>
|
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||||
{{SITE_TAGLINE}}
|
>
|
||||||
</h6>
|
{{SITE_TAGLINE}}
|
||||||
<p
|
</h6>
|
||||||
class="q-my-sm"
|
<p
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
class="q-my-sm"
|
||||||
>
|
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||||
{{SITE_DESCRIPTION}}
|
>
|
||||||
</p>
|
{{SITE_DESCRIPTION}}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<!-- <div
|
<!-- <div
|
||||||
class="gt-sm"
|
class="gt-sm"
|
||||||
v-html="formatDescription"
|
v-html="formatDescription"
|
||||||
@@ -145,7 +150,7 @@
|
|||||||
{%else%}
|
{%else%}
|
||||||
<username-password
|
<username-password
|
||||||
v-if="authMethod != 'user-id-only'"
|
v-if="authMethod != 'user-id-only'"
|
||||||
:allowed_new_users="{{ LNBITS_NEW_ACCOUNTS_ALLOWED | tojson }}"
|
:allowed_new_users="allowedRegister"
|
||||||
:auth-methods="{{ LNBITS_AUTH_METHODS }}"
|
:auth-methods="{{ LNBITS_AUTH_METHODS }}"
|
||||||
:auth-action="authAction"
|
:auth-action="authAction"
|
||||||
v-model:user-name="username"
|
v-model:user-name="username"
|
||||||
@@ -160,7 +165,10 @@
|
|||||||
class="text-center text-grey-6"
|
class="text-center text-grey-6"
|
||||||
v-if="authAction !== 'reset'"
|
v-if="authAction !== 'reset'"
|
||||||
>
|
>
|
||||||
<p v-if="authAction === 'login'" class="q-mb-none">
|
<p
|
||||||
|
v-if="authAction === 'login' && allowedRegister"
|
||||||
|
class="q-mb-none"
|
||||||
|
>
|
||||||
Not registered? Create an
|
Not registered? Create an
|
||||||
<span
|
<span
|
||||||
class="text-secondary cursor-pointer"
|
class="text-secondary cursor-pointer"
|
||||||
@@ -168,19 +176,27 @@
|
|||||||
>Account</span
|
>Account</span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="q-mb-none">
|
<p
|
||||||
Aready have an account?
|
v-else-if="authAction === 'login' && !allowedRegister"
|
||||||
|
class="q-mb-none"
|
||||||
|
>
|
||||||
|
<span v-text="$t('new_user_not_allowed')"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-else-if="authAction === 'register'" class="q-mb-none">
|
||||||
|
<span v-text="$t('existing_account_question')"></span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="text-secondary cursor-pointer"
|
class="text-secondary cursor-pointer"
|
||||||
@click="showLogin('username-password')"
|
@click="showLogin('username-password')"
|
||||||
>Login</span
|
v-text="$t('login')"
|
||||||
>
|
></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</username-password>
|
</username-password>
|
||||||
{% if "user-id-only" in LNBITS_AUTH_METHODS %}
|
{% if "user-id-only" in LNBITS_AUTH_METHODS %}
|
||||||
<user-id-only
|
<user-id-only
|
||||||
:allowed_new_users="{{ LNBITS_NEW_ACCOUNTS_ALLOWED | tojson }}"
|
:allowed_new_users="allowedRegister"
|
||||||
v-model:usr="usr"
|
v-model:usr="usr"
|
||||||
v-model:wallet="walletName"
|
v-model:wallet="walletName"
|
||||||
:auth-action="authAction"
|
:auth-action="authAction"
|
||||||
@@ -210,7 +226,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
|
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats' && '{{ SITE_TITLE }}' == 'LNbits' && '{{ LNBITS_SHOW_HOME_PAGE_ELEMENTS }}' == 'True'"
|
||||||
class="full-width q-mb-lg q-mt-sm"
|
class="full-width q-mb-lg q-mt-sm"
|
||||||
>
|
>
|
||||||
<div class="flex flex-center q-gutter-md q-py-md">
|
<div class="flex flex-center q-gutter-md q-py-md">
|
||||||
@@ -266,7 +282,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'" class="full-width">
|
<div
|
||||||
|
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats' && '{{ SITE_TITLE }}' == 'LNbits' && '{{ LNBITS_SHOW_HOME_PAGE_ELEMENTS }}' == 'True'"
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="marquee">
|
<div class="marquee">
|
||||||
<div class="marquee__group">
|
<div class="marquee__group">
|
||||||
|
|||||||
@@ -59,9 +59,11 @@
|
|||||||
<div class="col-7">
|
<div class="col-7">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div class="text-h3 q-my-none text-no-wrap">
|
<div class="text-h3 q-my-none full-width">
|
||||||
<strong
|
<strong
|
||||||
v-text="walletFormatBalance(this.g.wallet.sat)"
|
v-text="walletFormatBalance(this.g.wallet.sat)"
|
||||||
|
class="text-no-wrap"
|
||||||
|
:style="{fontSize: 'clamp(0.75rem, 10vw, 3rem)', display: 'inline-block', maxWidth: '100%'}"
|
||||||
></strong>
|
></strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,7 +168,12 @@
|
|||||||
@click="showReceiveDialog"
|
@click="showReceiveDialog"
|
||||||
:label="$t('create_invoice')"
|
:label="$t('create_invoice')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn unelevated color="secondary" icon="qr_code_scanner">
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="secondary"
|
||||||
|
icon="qr_code_scanner"
|
||||||
|
@click="showCamera"
|
||||||
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('camera_tooltip')"></span
|
><span v-text="$t('camera_tooltip')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
@@ -726,6 +733,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="g.fiatTracking">
|
<div v-if="g.fiatTracking">
|
||||||
<div v-if="isFiatPriority">
|
<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">
|
<div class="text-h5 text-italic">
|
||||||
<span
|
<span
|
||||||
v-text="parse.invoice.fiatAmount"
|
v-text="parse.invoice.fiatAmount"
|
||||||
@@ -733,13 +747,6 @@
|
|||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="opacity: 0.75">
|
|
||||||
<h5 class="q-my-none text-bold">
|
|
||||||
<span
|
|
||||||
v-text="walletFormatBalance(parse.invoice.sat)"
|
|
||||||
></span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<q-table :rows="wallets" :columns="walletTable.columns">
|
<q-table :rows="wallets" :columns="walletTable.columns">
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width v-if="g.user.super_user"></q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th
|
<q-th
|
||||||
auto-width
|
auto-width
|
||||||
@@ -47,12 +48,14 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td auto-width>
|
<q-td auto-width v-if="g.user.super_user">
|
||||||
<lnbits-update-balance
|
<lnbits-update-balance
|
||||||
:wallet_id="props.row.id"
|
:wallet_id="props.row.id"
|
||||||
@credit-value="handleBalanceUpdate"
|
@credit-value="handleBalanceUpdate"
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
></lnbits-update-balance>
|
></lnbits-update-balance>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
icon="menu"
|
icon="menu"
|
||||||
|
|||||||
@@ -127,7 +127,17 @@
|
|||||||
:label="props.row.wallet_count"
|
:label="props.row.wallet_count"
|
||||||
@click="fetchWallets(props.row.id)"
|
@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-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from shutil import make_archive
|
from pathlib import Path
|
||||||
|
from shutil import make_archive, move
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from typing import Optional
|
from tempfile import NamedTemporaryFile
|
||||||
|
from typing import IO, Optional
|
||||||
from urllib.parse import urlparse
|
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 fastapi.responses import FileResponse
|
||||||
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
|
from lnbits.core.models.misc import Image, SimpleStatus
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
enqueue_notification,
|
enqueue_notification,
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
update_cached_settings,
|
update_cached_settings,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.services.notifications import send_email_notification
|
||||||
from lnbits.core.services.settings import dict_to_settings
|
from lnbits.core.services.settings import dict_to_settings
|
||||||
from lnbits.decorators import check_admin, check_super_user
|
from lnbits.decorators import check_admin, check_super_user
|
||||||
|
from lnbits.helpers import safe_upload_file_path
|
||||||
from lnbits.server import server_restart
|
from lnbits.server import server_restart
|
||||||
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||||
from lnbits.tasks import invoice_listeners
|
from lnbits.tasks import invoice_listeners
|
||||||
@@ -26,6 +32,7 @@ from .. import core_app_extra
|
|||||||
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
|
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
|
||||||
|
|
||||||
admin_router = APIRouter(tags=["Admin UI"], prefix="/admin")
|
admin_router = APIRouter(tags=["Admin UI"], prefix="/admin")
|
||||||
|
file_upload = File(...)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get(
|
@admin_router.get(
|
||||||
@@ -50,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])
|
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
||||||
async def api_get_settings(
|
async def api_get_settings(
|
||||||
user: User = Depends(check_admin),
|
user: User = Depends(check_admin),
|
||||||
@@ -146,3 +165,93 @@ async def api_download_backup() -> FileResponse:
|
|||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.post(
|
||||||
|
"/api/v1/images",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def upload_image(
|
||||||
|
file: UploadFile = file_upload,
|
||||||
|
content_length: int = Header(..., le=settings.lnbits_upload_size_bytes),
|
||||||
|
) -> Image:
|
||||||
|
if not file.filename:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail="No filename provided."
|
||||||
|
)
|
||||||
|
|
||||||
|
# validate file types
|
||||||
|
file_info = filetype.guess(file.file)
|
||||||
|
if file_info is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE,
|
||||||
|
detail="Unable to determine file type",
|
||||||
|
)
|
||||||
|
detected_content_type = file_info.extension.lower()
|
||||||
|
if (
|
||||||
|
file.content_type not in settings.lnbits_upload_allowed_types
|
||||||
|
or detected_content_type not in settings.lnbits_upload_allowed_types
|
||||||
|
):
|
||||||
|
raise HTTPException(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported file type")
|
||||||
|
|
||||||
|
# validate file name
|
||||||
|
try:
|
||||||
|
file_path = safe_upload_file_path(file.filename)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail=f"The requested filename '{file.filename}' is forbidden.",
|
||||||
|
) from e
|
||||||
|
|
||||||
|
# validate file size
|
||||||
|
real_file_size = 0
|
||||||
|
temp: IO = NamedTemporaryFile(delete=False)
|
||||||
|
for chunk in file.file:
|
||||||
|
real_file_size += len(chunk)
|
||||||
|
if real_file_size > content_length:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
|
||||||
|
detail=f"File too large ({content_length / 1000} KB max)",
|
||||||
|
)
|
||||||
|
temp.write(chunk)
|
||||||
|
temp.close()
|
||||||
|
|
||||||
|
move(temp.name, file_path)
|
||||||
|
return Image(filename=file.filename)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get(
|
||||||
|
"/api/v1/images",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def list_uploaded_images() -> list[Image]:
|
||||||
|
image_folder = Path(settings.lnbits_data_folder, "images")
|
||||||
|
files = image_folder.glob("*")
|
||||||
|
images = []
|
||||||
|
for file in files:
|
||||||
|
if file.is_file():
|
||||||
|
images.append(Image(filename=file.name))
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.delete(
|
||||||
|
"/api/v1/images/{filename}",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def delete_uploaded_image(filename: str) -> SimpleStatus:
|
||||||
|
try:
|
||||||
|
file_path = safe_upload_file_path(filename)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail=f"The requested filename '{filename}' is forbidden.",
|
||||||
|
) from e
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Image not found.")
|
||||||
|
|
||||||
|
file_path.unlink()
|
||||||
|
return SimpleStatus(success=True, message=f"{filename} deleted")
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
|
||||||
from typing import Annotated, List, Optional, Union
|
from typing import Annotated, List, Optional, Union
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ generic_router = APIRouter(
|
|||||||
|
|
||||||
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return FileResponse(Path("lnbits", "static", "favicon.ico"))
|
return RedirectResponse(settings.lnbits_qr_logo)
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/", response_class=HTMLResponse)
|
@generic_router.get("/", response_class=HTMLResponse)
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ from lnbits.core.models import (
|
|||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.users import User
|
from lnbits.core.models.users import User
|
||||||
from lnbits.core.services.payments import get_payments_daily_stats
|
from lnbits.core.services.payments import (
|
||||||
|
get_payments_daily_stats,
|
||||||
|
update_pending_payment,
|
||||||
|
)
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
@@ -180,11 +183,14 @@ async def api_payments_paginated(
|
|||||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||||
):
|
):
|
||||||
await update_pending_payments(key_info.wallet.id)
|
|
||||||
page = await get_payments_paginated(
|
page = await get_payments_paginated(
|
||||||
wallet_id=key_info.wallet.id,
|
wallet_id=key_info.wallet.id,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
)
|
)
|
||||||
|
for payment in page.data:
|
||||||
|
if payment.pending:
|
||||||
|
await update_pending_payment(payment)
|
||||||
|
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -104,16 +104,24 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
|||||||
)
|
)
|
||||||
account.validate_fields()
|
account.validate_fields()
|
||||||
account.hash_password(data.password)
|
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
|
data.id = user.id
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@users_router.put("/user/{user_id}", name="Update user")
|
@users_router.put("/user/{user_id}", name="Update user")
|
||||||
async def api_update_user(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:
|
if user_id != data.id:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
|
||||||
|
|
||||||
|
if user_id == settings.super_user and user.id != settings.super_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Action only allowed for super user.",
|
||||||
|
)
|
||||||
|
|
||||||
if data.password or data.password_repeat:
|
if data.password or data.password_repeat:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
|
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
|
||||||
@@ -255,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)."
|
summary="First time it is called it does a soft delete (only sets a flag)."
|
||||||
"The second time it is called will delete the entry from the DB",
|
"The second time it is called will delete the entry from the DB",
|
||||||
)
|
)
|
||||||
async def api_users_delete_user_wallet(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)
|
wal = await get_wallet(wallet)
|
||||||
if not wal:
|
if not wal:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
detail="Wallet does not exist.",
|
detail="Wallet does not exist.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if user_id == settings.super_user and user.id != settings.super_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Action only allowed for super user.",
|
||||||
|
)
|
||||||
|
|
||||||
if wal.deleted:
|
if wal.deleted:
|
||||||
await force_delete_wallet(wallet)
|
await force_delete_wallet(wallet)
|
||||||
await delete_wallet(user_id=user_id, wallet_id=wallet)
|
await delete_wallet(user_id=user_id, wallet_id=wallet)
|
||||||
|
|||||||
+35
-35
@@ -8,7 +8,7 @@ import time
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Generic, Literal, Optional, TypeVar, Union, get_origin
|
from typing import Any, Generic, Literal, TypeVar, get_origin
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, ValidationError, root_validator
|
from pydantic import BaseModel, ValidationError, root_validator
|
||||||
@@ -65,8 +65,8 @@ def get_placeholder(model: Any, field: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class Compat:
|
class Compat:
|
||||||
type: Optional[str] = "<inherited>"
|
type: str | None = "<inherited>"
|
||||||
schema: Optional[str] = "<inherited>"
|
schema: str | None = "<inherited>"
|
||||||
|
|
||||||
def interval_seconds(self, seconds: int) -> str:
|
def interval_seconds(self, seconds: int) -> str:
|
||||||
if self.type in {POSTGRES, COCKROACH}:
|
if self.type in {POSTGRES, COCKROACH}:
|
||||||
@@ -167,8 +167,8 @@ class Connection(Compat):
|
|||||||
async def fetchall(
|
async def fetchall(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
) -> list[TModel]:
|
) -> list[TModel]:
|
||||||
params = self.rewrite_values(values) if values else {}
|
params = self.rewrite_values(values) if values else {}
|
||||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||||
@@ -183,8 +183,8 @@ class Connection(Compat):
|
|||||||
async def fetchone(
|
async def fetchone(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
) -> TModel:
|
) -> TModel:
|
||||||
params = self.rewrite_values(values) if values else {}
|
params = self.rewrite_values(values) if values else {}
|
||||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||||
@@ -211,11 +211,11 @@ class Connection(Compat):
|
|||||||
async def fetch_page(
|
async def fetch_page(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
where: Optional[list[str]] = None,
|
where: list[str] | None = None,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
filters: Optional[Filters] = None,
|
filters: Filters | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
group_by: Optional[list[str]] = None,
|
group_by: list[str] | None = None,
|
||||||
) -> Page[TModel]:
|
) -> Page[TModel]:
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
@@ -268,7 +268,7 @@ class Connection(Compat):
|
|||||||
total=count,
|
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 {}
|
params = self.rewrite_values(values) if values else {}
|
||||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||||
await self.conn.commit()
|
await self.conn.commit()
|
||||||
@@ -350,8 +350,8 @@ class Database(Compat):
|
|||||||
async def fetchall(
|
async def fetchall(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
) -> list[TModel]:
|
) -> list[TModel]:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetchall(query, values, model)
|
return await conn.fetchall(query, values, model)
|
||||||
@@ -359,8 +359,8 @@ class Database(Compat):
|
|||||||
async def fetchone(
|
async def fetchone(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
) -> TModel:
|
) -> TModel:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetchone(query, values, model)
|
return await conn.fetchone(query, values, model)
|
||||||
@@ -378,16 +378,16 @@ class Database(Compat):
|
|||||||
async def fetch_page(
|
async def fetch_page(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
where: Optional[list[str]] = None,
|
where: list[str] | None = None,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
filters: Optional[Filters] = None,
|
filters: Filters | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
group_by: Optional[list[str]] = None,
|
group_by: list[str] | None = None,
|
||||||
) -> Page[TModel]:
|
) -> Page[TModel]:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
||||||
|
|
||||||
async def execute(self, query: str, values: Optional[dict] = None):
|
async def execute(self, query: str, values: dict | None = None):
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.execute(query, values)
|
return await conn.execute(query, values)
|
||||||
|
|
||||||
@@ -445,7 +445,7 @@ class Operator(Enum):
|
|||||||
|
|
||||||
class FilterModel(BaseModel):
|
class FilterModel(BaseModel):
|
||||||
__search_fields__: list[str] = []
|
__search_fields__: list[str] = []
|
||||||
__sort_fields__: Optional[list[str]] = None
|
__sort_fields__: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
@@ -461,8 +461,8 @@ class Page(BaseModel, Generic[T]):
|
|||||||
class Filter(BaseModel, Generic[TFilterModel]):
|
class Filter(BaseModel, Generic[TFilterModel]):
|
||||||
field: str
|
field: str
|
||||||
op: Operator = Operator.EQ
|
op: Operator = Operator.EQ
|
||||||
model: Optional[type[TFilterModel]]
|
model: type[TFilterModel] | None
|
||||||
values: Optional[dict] = None
|
values: dict | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_query(
|
def parse_query(
|
||||||
@@ -517,15 +517,15 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
filters: list[Filter[TFilterModel]] = []
|
filters: list[Filter[TFilterModel]] = []
|
||||||
search: Optional[str] = None
|
search: str | None = None
|
||||||
|
|
||||||
offset: Optional[int] = None
|
offset: int | None = None
|
||||||
limit: Optional[int] = None
|
limit: int | None = None
|
||||||
|
|
||||||
sortby: Optional[str] = None
|
sortby: str | None = None
|
||||||
direction: Optional[Literal["asc", "desc"]] = None
|
direction: Literal["asc", "desc"] | None = None
|
||||||
|
|
||||||
model: Optional[type[TFilterModel]] = None
|
model: type[TFilterModel] | None = None
|
||||||
|
|
||||||
@root_validator(pre=True)
|
@root_validator(pre=True)
|
||||||
def validate_sortby(cls, values):
|
def validate_sortby(cls, values):
|
||||||
@@ -547,7 +547,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
stmt += f"OFFSET {self.offset}"
|
stmt += f"OFFSET {self.offset}"
|
||||||
return stmt
|
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:
|
if not where_stmts:
|
||||||
where_stmts = []
|
where_stmts = []
|
||||||
if self.filters:
|
if self.filters:
|
||||||
@@ -567,7 +567,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def values(self, values: Optional[dict] = None) -> dict:
|
def values(self, values: dict | None = None) -> dict:
|
||||||
if not values:
|
if not values:
|
||||||
values = {}
|
values = {}
|
||||||
if self.filters:
|
if self.filters:
|
||||||
@@ -641,7 +641,7 @@ def model_to_dict(model: BaseModel) -> dict:
|
|||||||
return _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"""
|
"""convert a dictionary or JSON string to a Pydantic model"""
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
if value == "null":
|
if value == "null":
|
||||||
|
|||||||
+12
-4
@@ -5,7 +5,7 @@ import jwt
|
|||||||
from fastapi import Cookie, Depends, Query, Request, Security
|
from fastapi import Cookie, Depends, Query, Request, Security
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.openapi.models import APIKey, APIKeyIn, SecuritySchemeType
|
from fastapi.openapi.models import APIKey, APIKeyIn, SecuritySchemeType
|
||||||
from fastapi.security import APIKeyHeader, APIKeyQuery, OAuth2PasswordBearer
|
from fastapi.security import APIKeyHeader, APIKeyQuery, HTTPBearer, OAuth2PasswordBearer
|
||||||
from fastapi.security.base import SecurityBase
|
from fastapi.security.base import SecurityBase
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic.types import UUID4
|
from pydantic.types import UUID4
|
||||||
@@ -31,8 +31,15 @@ from lnbits.db import Connection, Filter, Filters, TFilterModel
|
|||||||
from lnbits.helpers import path_segments
|
from lnbits.helpers import path_segments
|
||||||
from lnbits.settings import AuthMethods, settings
|
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(
|
api_key_header = APIKeyHeader(
|
||||||
name="X-API-KEY",
|
name="X-API-KEY",
|
||||||
auto_error=False,
|
auto_error=False,
|
||||||
@@ -132,8 +139,9 @@ async def require_invoice_key(
|
|||||||
async def check_access_token(
|
async def check_access_token(
|
||||||
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
|
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
|
||||||
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
|
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
|
||||||
|
bearer_access_token: Annotated[Union[str, None], Depends(http_bearer)] = None,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
return header_access_token or cookie_access_token
|
return header_access_token or cookie_access_token or bearer_access_token
|
||||||
|
|
||||||
|
|
||||||
async def check_user_exists(
|
async def check_user_exists(
|
||||||
|
|||||||
+36
-10
@@ -10,7 +10,6 @@ from urllib.parse import urlparse
|
|||||||
import jinja2
|
import jinja2
|
||||||
import jwt
|
import jwt
|
||||||
import shortuuid
|
import shortuuid
|
||||||
from fastapi import Request
|
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from packaging import version
|
from packaging import version
|
||||||
from pydantic.schema import field_schema
|
from pydantic.schema import field_schema
|
||||||
@@ -72,7 +71,11 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
|||||||
t.env.globals["VOIDWALLET"] = settings.lnbits_backend_wallet_class == "VoidWallet"
|
t.env.globals["VOIDWALLET"] = settings.lnbits_backend_wallet_class == "VoidWallet"
|
||||||
t.env.globals["HIDE_API"] = settings.lnbits_hide_api
|
t.env.globals["HIDE_API"] = settings.lnbits_hide_api
|
||||||
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
|
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_TAGLINE"] = settings.lnbits_site_tagline
|
||||||
t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description
|
t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description
|
||||||
t.env.globals["LNBITS_SHOW_HOME_PAGE_ELEMENTS"] = (
|
t.env.globals["LNBITS_SHOW_HOME_PAGE_ELEMENTS"] = (
|
||||||
@@ -225,18 +228,34 @@ def create_access_token(data: dict, token_expire_minutes: Optional[int] = None)
|
|||||||
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
||||||
|
|
||||||
|
|
||||||
def encrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
|
def encrypt_internal_message(
|
||||||
"""Encrypt message with the internal secret key"""
|
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:
|
if not m:
|
||||||
return None
|
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]:
|
def decrypt_internal_message(
|
||||||
"""Decrypt message with the internal secret key"""
|
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:
|
if not m:
|
||||||
return None
|
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:
|
def filter_dict_keys(data: dict, filter_keys: Optional[list[str]]) -> dict:
|
||||||
@@ -330,5 +349,12 @@ def normalize_path(path: Optional[str]) -> str:
|
|||||||
return "/" + "/".join(path_segments(path))
|
return "/" + "/".join(path_segments(path))
|
||||||
|
|
||||||
|
|
||||||
def normalized_path(request: Request) -> str:
|
def safe_upload_file_path(filename: str, directory: str = "images") -> Path:
|
||||||
return "/" + "/".join(path_segments(request.url.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()
|
||||||
|
|||||||
@@ -134,8 +134,9 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
|||||||
assert response
|
assert response
|
||||||
return response
|
return response
|
||||||
finally:
|
finally:
|
||||||
duration = (datetime.now(timezone.utc) - start_time).total_seconds()
|
if request_details:
|
||||||
await self._log_audit(request, response, duration, request_details)
|
duration = (datetime.now(timezone.utc) - start_time).total_seconds()
|
||||||
|
await self._log_audit(request, response, duration, request_details)
|
||||||
|
|
||||||
async def _log_audit(
|
async def _log_audit(
|
||||||
self,
|
self,
|
||||||
|
|||||||
+33
-33
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -15,10 +15,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class NodePeerInfo(BaseModel):
|
class NodePeerInfo(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
alias: Optional[str] = None
|
alias: str | None = None
|
||||||
color: Optional[str] = None
|
color: str | None = None
|
||||||
last_timestamp: Optional[int] = None
|
last_timestamp: int | None = None
|
||||||
addresses: Optional[list[str]] = None
|
addresses: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
class ChannelState(Enum):
|
class ChannelState(Enum):
|
||||||
@@ -47,20 +47,20 @@ class NodeChannel(BaseModel):
|
|||||||
balance: ChannelBalance
|
balance: ChannelBalance
|
||||||
state: ChannelState
|
state: ChannelState
|
||||||
# could be optional for closing/pending channels on lndrest
|
# could be optional for closing/pending channels on lndrest
|
||||||
id: Optional[str] = None
|
id: str | None = None
|
||||||
short_id: Optional[str] = None
|
short_id: str | None = None
|
||||||
point: Optional[ChannelPoint] = None
|
point: ChannelPoint | None = None
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
color: Optional[str] = None
|
color: str | None = None
|
||||||
fee_ppm: Optional[int] = None
|
fee_ppm: int | None = None
|
||||||
fee_base_msat: Optional[int] = None
|
fee_base_msat: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class ChannelStats(BaseModel):
|
class ChannelStats(BaseModel):
|
||||||
counts: dict[ChannelState, int]
|
counts: dict[ChannelState, int]
|
||||||
avg_size: int
|
avg_size: int
|
||||||
biggest_size: Optional[int]
|
biggest_size: int | None
|
||||||
smallest_size: Optional[int]
|
smallest_size: int | None
|
||||||
total_capacity: int
|
total_capacity: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -95,9 +95,9 @@ class ChannelStats(BaseModel):
|
|||||||
|
|
||||||
class NodeFees(BaseModel):
|
class NodeFees(BaseModel):
|
||||||
total_msat: int
|
total_msat: int
|
||||||
daily_msat: Optional[int] = None
|
daily_msat: int | None = None
|
||||||
weekly_msat: Optional[int] = None
|
weekly_msat: int | None = None
|
||||||
monthly_msat: Optional[int] = None
|
monthly_msat: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class PublicNodeInfo(BaseModel):
|
class PublicNodeInfo(BaseModel):
|
||||||
@@ -121,25 +121,25 @@ class NodeInfoResponse(PublicNodeInfo):
|
|||||||
class NodePayment(BaseModel):
|
class NodePayment(BaseModel):
|
||||||
pending: bool
|
pending: bool
|
||||||
amount: int
|
amount: int
|
||||||
fee: Optional[int] = None
|
fee: int | None = None
|
||||||
memo: Optional[str] = None
|
memo: str | None = None
|
||||||
time: int
|
time: int
|
||||||
bolt11: Optional[str] = None
|
bolt11: str | None = None
|
||||||
preimage: Optional[str]
|
preimage: str | None
|
||||||
payment_hash: str
|
payment_hash: str
|
||||||
expiry: Optional[float] = None
|
expiry: float | None = None
|
||||||
destination: Optional[NodePeerInfo] = None
|
destination: NodePeerInfo | None = None
|
||||||
|
|
||||||
|
|
||||||
class NodeInvoice(BaseModel):
|
class NodeInvoice(BaseModel):
|
||||||
pending: bool
|
pending: bool
|
||||||
amount: int
|
amount: int
|
||||||
memo: Optional[str]
|
memo: str | None
|
||||||
bolt11: str
|
bolt11: str
|
||||||
preimage: Optional[str]
|
preimage: str | None
|
||||||
payment_hash: str
|
payment_hash: str
|
||||||
paid_at: Optional[int] = None
|
paid_at: int | None = None
|
||||||
expiry: Optional[int] = None
|
expiry: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class NodeInvoiceFilters(FilterModel):
|
class NodeInvoiceFilters(FilterModel):
|
||||||
@@ -154,7 +154,7 @@ class Node(ABC):
|
|||||||
|
|
||||||
def __init__(self, wallet: Wallet):
|
def __init__(self, wallet: Wallet):
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
self.id: Optional[str] = None
|
self.id: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@@ -203,22 +203,22 @@ class Node(ABC):
|
|||||||
self,
|
self,
|
||||||
peer_id: str,
|
peer_id: str,
|
||||||
local_amount: int,
|
local_amount: int,
|
||||||
push_amount: Optional[int] = None,
|
push_amount: int | None = None,
|
||||||
fee_rate: Optional[int] = None,
|
fee_rate: int | None = None,
|
||||||
) -> ChannelPoint:
|
) -> ChannelPoint:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def close_channel(
|
async def close_channel(
|
||||||
self,
|
self,
|
||||||
short_id: Optional[str] = None,
|
short_id: str | None = None,
|
||||||
point: Optional[ChannelPoint] = None,
|
point: ChannelPoint | None = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
):
|
):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_channel(self, channel_id: str) -> Optional[NodeChannel]:
|
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
+6
-6
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
@@ -119,8 +119,8 @@ class CoreLightningNode(Node):
|
|||||||
self,
|
self,
|
||||||
peer_id: str,
|
peer_id: str,
|
||||||
local_amount: int,
|
local_amount: int,
|
||||||
push_amount: Optional[int] = None,
|
push_amount: int | None = None,
|
||||||
fee_rate: Optional[int] = None,
|
fee_rate: int | None = None,
|
||||||
) -> ChannelPoint:
|
) -> ChannelPoint:
|
||||||
try:
|
try:
|
||||||
result = await self.ln_rpc(
|
result = await self.ln_rpc(
|
||||||
@@ -173,8 +173,8 @@ class CoreLightningNode(Node):
|
|||||||
@catch_rpc_errors
|
@catch_rpc_errors
|
||||||
async def close_channel(
|
async def close_channel(
|
||||||
self,
|
self,
|
||||||
short_id: Optional[str] = None,
|
short_id: str | None = None,
|
||||||
point: Optional[ChannelPoint] = None,
|
point: ChannelPoint | None = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
):
|
):
|
||||||
if not short_id:
|
if not short_id:
|
||||||
@@ -229,7 +229,7 @@ class CoreLightningNode(Node):
|
|||||||
await self.ln_rpc("setchannel", channel_id, feebase=base_msat, feeppm=ppm)
|
await self.ln_rpc("setchannel", channel_id, feebase=base_msat, feeppm=ppm)
|
||||||
|
|
||||||
@catch_rpc_errors
|
@catch_rpc_errors
|
||||||
async def get_channel(self, channel_id: str) -> Optional[NodeChannel]:
|
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
||||||
channels = await self.get_channels()
|
channels = await self.get_channels()
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
if channel.id == channel_id:
|
if channel.id == channel_id:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from httpx import HTTPStatusError
|
from httpx import HTTPStatusError
|
||||||
@@ -60,9 +60,7 @@ def _parse_channel_point(raw: str) -> ChannelPoint:
|
|||||||
class LndRestNode(Node):
|
class LndRestNode(Node):
|
||||||
wallet: LndRestWallet
|
wallet: LndRestWallet
|
||||||
|
|
||||||
async def request(
|
async def request(self, method: str, path: str, json: dict | None = None, **kwargs):
|
||||||
self, method: str, path: str, json: Optional[dict] = None, **kwargs
|
|
||||||
):
|
|
||||||
response = await self.wallet.client.request(
|
response = await self.wallet.client.request(
|
||||||
method, f"{self.wallet.endpoint}{path}", json=json, **kwargs
|
method, f"{self.wallet.endpoint}{path}", json=json, **kwargs
|
||||||
)
|
)
|
||||||
@@ -131,8 +129,8 @@ class LndRestNode(Node):
|
|||||||
self,
|
self,
|
||||||
peer_id: str,
|
peer_id: str,
|
||||||
local_amount: int,
|
local_amount: int,
|
||||||
push_amount: Optional[int] = None,
|
push_amount: int | None = None,
|
||||||
fee_rate: Optional[int] = None,
|
fee_rate: int | None = None,
|
||||||
) -> ChannelPoint:
|
) -> ChannelPoint:
|
||||||
response = await self.request(
|
response = await self.request(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -176,8 +174,8 @@ class LndRestNode(Node):
|
|||||||
|
|
||||||
async def close_channel(
|
async def close_channel(
|
||||||
self,
|
self,
|
||||||
short_id: Optional[str] = None,
|
short_id: str | None = None,
|
||||||
point: Optional[ChannelPoint] = None,
|
point: ChannelPoint | None = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
):
|
):
|
||||||
if short_id:
|
if short_id:
|
||||||
@@ -218,7 +216,7 @@ class LndRestNode(Node):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_channel(self, channel_id: str) -> Optional[NodeChannel]:
|
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
||||||
channel_info = await self.get(f"/v1/graph/edge/{channel_id}")
|
channel_info = await self.get(f"/v1/graph/edge/{channel_id}")
|
||||||
info = await self.get("/v1/getinfo")
|
info = await self.get("/v1/getinfo")
|
||||||
if info["identity_pubkey"] == channel_info["node1_pub"]:
|
if info["identity_pubkey"] == channel_info["node1_pub"]:
|
||||||
|
|||||||
+169
-160
@@ -4,16 +4,16 @@ import importlib
|
|||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from hashlib import sha256
|
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import gmtime, strftime, time
|
from time import gmtime, strftime, time
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import httpx
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ class RedirectPath(BaseModel):
|
|||||||
other.from_path, list(other.header_filters.items())
|
other.from_path, list(other.header_filters.items())
|
||||||
) or other.redirect_matches(self.from_path, list(self.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:
|
for other in others:
|
||||||
if self.in_conflict(other):
|
if self.in_conflict(other):
|
||||||
return other
|
return other
|
||||||
@@ -153,7 +153,7 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
|||||||
|
|
||||||
def find_extension_redirect(
|
def find_extension_redirect(
|
||||||
self, path: str, req_headers: list[tuple[bytes, bytes]]
|
self, path: str, req_headers: list[tuple[bytes, bytes]]
|
||||||
) -> Optional[RedirectPath]:
|
) -> RedirectPath | None:
|
||||||
headers = [(k.decode(), v.decode()) for k, v in req_headers]
|
headers = [(k.decode(), v.decode()) for k, v in req_headers]
|
||||||
return next(
|
return next(
|
||||||
(
|
(
|
||||||
@@ -167,8 +167,8 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
|||||||
def activate_extension_paths(
|
def activate_extension_paths(
|
||||||
self,
|
self,
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
upgrade_hash: Optional[str] = None,
|
upgrade_hash: str | None = None,
|
||||||
ext_redirects: Optional[list[dict]] = None,
|
ext_redirects: list[dict] | None = None,
|
||||||
):
|
):
|
||||||
self.lnbits_deactivated_extensions.discard(ext_id)
|
self.lnbits_deactivated_extensions.discard(ext_id)
|
||||||
|
|
||||||
@@ -231,12 +231,12 @@ class ExchangeHistorySettings(LNbitsSettings):
|
|||||||
class ThemesSettings(LNbitsSettings):
|
class ThemesSettings(LNbitsSettings):
|
||||||
lnbits_site_title: str = Field(default="LNbits")
|
lnbits_site_title: str = Field(default="LNbits")
|
||||||
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
|
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."
|
default="The world's most powerful suite of bitcoin tools."
|
||||||
)
|
)
|
||||||
lnbits_show_home_page_elements: bool = Field(default=True)
|
lnbits_show_home_page_elements: bool = Field(default=True)
|
||||||
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
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_custom_badge_color: str = Field(default="warning")
|
||||||
lnbits_theme_options: list[str] = Field(
|
lnbits_theme_options: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
@@ -251,18 +251,16 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
"bitcoin",
|
"bitcoin",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
lnbits_custom_logo: Optional[str] = Field(default=None)
|
lnbits_custom_logo: str | None = Field(default=None)
|
||||||
lnbits_custom_image: Optional[str] = Field(
|
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
|
||||||
default="/static/images/logos/lnbits.svg"
|
|
||||||
)
|
|
||||||
lnbits_ad_space_title: str = Field(default="Supported by")
|
lnbits_ad_space_title: str = Field(default="Supported by")
|
||||||
lnbits_ad_space: str = Field(
|
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"
|
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
|
) # sneaky sneaky
|
||||||
lnbits_ad_space_enabled: bool = Field(default=False)
|
lnbits_ad_space_enabled: bool = Field(default=False)
|
||||||
lnbits_allowed_currencies: list[str] = Field(default=[])
|
lnbits_allowed_currencies: list[str] = Field(default=[])
|
||||||
lnbits_default_accounting_currency: Optional[str] = Field(default=None)
|
lnbits_default_accounting_currency: str | None = Field(default=None)
|
||||||
lnbits_qr_logo: str = Field(default="/static/images/logos/lnbits.png")
|
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
|
||||||
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
||||||
lnbits_default_theme: str = Field(default="salvador")
|
lnbits_default_theme: str = Field(default="salvador")
|
||||||
lnbits_default_border: str = Field(default="hard-border")
|
lnbits_default_border: str = Field(default="hard-border")
|
||||||
@@ -273,16 +271,32 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
class OpsSettings(LNbitsSettings):
|
class OpsSettings(LNbitsSettings):
|
||||||
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
||||||
lnbits_hide_api: bool = Field(default=False)
|
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):
|
class FeeSettings(LNbitsSettings):
|
||||||
lnbits_reserve_fee_min: int = Field(default=2000)
|
lnbits_reserve_fee_min: int = Field(default=2000, ge=0)
|
||||||
lnbits_reserve_fee_percent: float = Field(default=1.0)
|
lnbits_reserve_fee_percent: float = Field(default=1.0, ge=0)
|
||||||
lnbits_service_fee: float = Field(default=0)
|
lnbits_service_fee: float = Field(default=0, ge=0)
|
||||||
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
||||||
lnbits_service_fee_max: int = Field(default=0)
|
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
|
# 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
|
# funding_source.pay_invoice(), it may cause a vulnerability if the values differ
|
||||||
@@ -295,9 +309,9 @@ class FeeSettings(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class ExchangeProvidersSettings(LNbitsSettings):
|
class ExchangeProvidersSettings(LNbitsSettings):
|
||||||
lnbits_exchange_rate_cache_seconds: int = Field(default=30)
|
lnbits_exchange_rate_cache_seconds: int = Field(default=30, ge=0)
|
||||||
lnbits_exchange_history_size: int = Field(default=60)
|
lnbits_exchange_history_size: int = Field(default=60, ge=0)
|
||||||
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300)
|
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300, ge=0)
|
||||||
|
|
||||||
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
||||||
default=[
|
default=[
|
||||||
@@ -362,7 +376,7 @@ class ExchangeProvidersSettings(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class SecuritySettings(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_rate_limit_unit: str = Field(default="minute")
|
||||||
lnbits_allowed_ips: list[str] = Field(default=[])
|
lnbits_allowed_ips: list[str] = Field(default=[])
|
||||||
lnbits_blocked_ips: list[str] = Field(default=[])
|
lnbits_blocked_ips: list[str] = Field(default=[])
|
||||||
@@ -370,16 +384,16 @@ class SecuritySettings(LNbitsSettings):
|
|||||||
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
|
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
|
||||||
)
|
)
|
||||||
|
|
||||||
lnbits_wallet_limit_max_balance: int = Field(default=0)
|
lnbits_wallet_limit_max_balance: int = Field(default=0, ge=0)
|
||||||
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
|
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0, ge=0)
|
||||||
lnbits_wallet_limit_secs_between_trans: int = Field(default=0)
|
lnbits_wallet_limit_secs_between_trans: int = Field(default=0, ge=0)
|
||||||
lnbits_only_allow_incoming_payments: bool = Field(default=False)
|
lnbits_only_allow_incoming_payments: bool = Field(default=False)
|
||||||
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
|
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
|
||||||
lnbits_watchdog_interval_minutes: int = Field(default=60)
|
lnbits_watchdog_interval_minutes: int = Field(default=60, gt=0)
|
||||||
lnbits_watchdog_delta: int = Field(default=1_000_000)
|
lnbits_watchdog_delta: int = Field(default=1_000_000, gt=0)
|
||||||
|
|
||||||
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000)
|
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)
|
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
|
|
||||||
def is_wallet_max_balance_exceeded(self, amount):
|
def is_wallet_max_balance_exceeded(self, amount):
|
||||||
return (
|
return (
|
||||||
@@ -396,142 +410,154 @@ class NotificationsSettings(LNbitsSettings):
|
|||||||
lnbits_telegram_notifications_enabled: bool = Field(default=False)
|
lnbits_telegram_notifications_enabled: bool = Field(default=False)
|
||||||
lnbits_telegram_notifications_access_token: str = Field(default="")
|
lnbits_telegram_notifications_access_token: str = Field(default="")
|
||||||
lnbits_telegram_notifications_chat_id: 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_settings_update: bool = Field(default=True)
|
||||||
lnbits_notification_credit_debit: bool = Field(default=True)
|
lnbits_notification_credit_debit: bool = Field(default=True)
|
||||||
notification_balance_delta_changed: bool = Field(default=True)
|
notification_balance_delta_changed: bool = Field(default=True)
|
||||||
lnbits_notification_server_start_stop: bool = Field(default=True)
|
lnbits_notification_server_start_stop: bool = Field(default=True)
|
||||||
lnbits_notification_watchdog: bool = Field(default=False)
|
lnbits_notification_watchdog: bool = Field(default=False)
|
||||||
lnbits_notification_server_status_hours: int = Field(default=24)
|
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
|
||||||
lnbits_notification_incoming_payment_amount_sats: int = Field(default=1_000_000)
|
lnbits_notification_incoming_payment_amount_sats: int = Field(
|
||||||
lnbits_notification_outgoing_payment_amount_sats: int = Field(default=1_000_000)
|
default=1_000_000, ge=0
|
||||||
|
)
|
||||||
|
lnbits_notification_outgoing_payment_amount_sats: int = Field(
|
||||||
|
default=1_000_000, ge=0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeWalletFundingSource(LNbitsSettings):
|
class FakeWalletFundingSource(LNbitsSettings):
|
||||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||||
|
lnbits_denomination: str = Field(default="sats")
|
||||||
|
|
||||||
|
|
||||||
class LNbitsFundingSource(LNbitsSettings):
|
class LNbitsFundingSource(LNbitsSettings):
|
||||||
lnbits_endpoint: str = Field(default="https://demo.lnbits.com")
|
lnbits_endpoint: str = Field(default="https://demo.lnbits.com")
|
||||||
lnbits_key: Optional[str] = Field(default=None)
|
lnbits_key: str | None = Field(default=None)
|
||||||
lnbits_admin_key: Optional[str] = Field(default=None)
|
lnbits_admin_key: str | None = Field(default=None)
|
||||||
lnbits_invoice_key: Optional[str] = Field(default=None)
|
lnbits_invoice_key: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class ClicheFundingSource(LNbitsSettings):
|
class ClicheFundingSource(LNbitsSettings):
|
||||||
cliche_endpoint: Optional[str] = Field(default=None)
|
cliche_endpoint: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class CoreLightningFundingSource(LNbitsSettings):
|
class CoreLightningFundingSource(LNbitsSettings):
|
||||||
corelightning_rpc: Optional[str] = Field(default=None)
|
corelightning_rpc: str | None = Field(default=None)
|
||||||
corelightning_pay_command: str = Field(default="pay")
|
corelightning_pay_command: str = Field(default="pay")
|
||||||
clightning_rpc: Optional[str] = Field(default=None)
|
clightning_rpc: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class CoreLightningRestFundingSource(LNbitsSettings):
|
class CoreLightningRestFundingSource(LNbitsSettings):
|
||||||
corelightning_rest_url: Optional[str] = Field(default=None)
|
corelightning_rest_url: str | None = Field(default=None)
|
||||||
corelightning_rest_macaroon: Optional[str] = Field(default=None)
|
corelightning_rest_macaroon: str | None = Field(default=None)
|
||||||
corelightning_rest_cert: Optional[str] = Field(default=None)
|
corelightning_rest_cert: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class EclairFundingSource(LNbitsSettings):
|
class EclairFundingSource(LNbitsSettings):
|
||||||
eclair_url: Optional[str] = Field(default=None)
|
eclair_url: str | None = Field(default=None)
|
||||||
eclair_pass: Optional[str] = Field(default=None)
|
eclair_pass: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class LndRestFundingSource(LNbitsSettings):
|
class LndRestFundingSource(LNbitsSettings):
|
||||||
lnd_rest_endpoint: Optional[str] = Field(default=None)
|
lnd_rest_endpoint: str | None = Field(default=None)
|
||||||
lnd_rest_cert: Optional[str] = Field(default=None)
|
lnd_rest_cert: str | None = Field(default=None)
|
||||||
lnd_rest_macaroon: Optional[str] = Field(default=None)
|
lnd_rest_macaroon: str | None = Field(default=None)
|
||||||
lnd_rest_macaroon_encrypted: Optional[str] = Field(default=None)
|
lnd_rest_macaroon_encrypted: str | None = Field(default=None)
|
||||||
lnd_rest_route_hints: bool = Field(default=True)
|
lnd_rest_route_hints: bool = Field(default=True)
|
||||||
lnd_rest_allow_self_payment: bool = Field(default=False)
|
lnd_rest_allow_self_payment: bool = Field(default=False)
|
||||||
lnd_cert: Optional[str] = Field(default=None)
|
lnd_cert: str | None = Field(default=None)
|
||||||
lnd_admin_macaroon: Optional[str] = Field(default=None)
|
lnd_admin_macaroon: str | None = Field(default=None)
|
||||||
lnd_invoice_macaroon: Optional[str] = Field(default=None)
|
lnd_invoice_macaroon: str | None = Field(default=None)
|
||||||
lnd_rest_admin_macaroon: Optional[str] = Field(default=None)
|
lnd_rest_admin_macaroon: str | None = Field(default=None)
|
||||||
lnd_rest_invoice_macaroon: Optional[str] = Field(default=None)
|
lnd_rest_invoice_macaroon: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class LndGrpcFundingSource(LNbitsSettings):
|
class LndGrpcFundingSource(LNbitsSettings):
|
||||||
lnd_grpc_endpoint: Optional[str] = Field(default=None)
|
lnd_grpc_endpoint: str | None = Field(default=None)
|
||||||
lnd_grpc_cert: Optional[str] = Field(default=None)
|
lnd_grpc_cert: str | None = Field(default=None)
|
||||||
lnd_grpc_port: Optional[int] = Field(default=None)
|
lnd_grpc_port: int | None = Field(default=None)
|
||||||
lnd_grpc_admin_macaroon: Optional[str] = Field(default=None)
|
lnd_grpc_admin_macaroon: str | None = Field(default=None)
|
||||||
lnd_grpc_invoice_macaroon: Optional[str] = Field(default=None)
|
lnd_grpc_invoice_macaroon: str | None = Field(default=None)
|
||||||
lnd_grpc_macaroon: Optional[str] = Field(default=None)
|
lnd_grpc_macaroon: str | None = Field(default=None)
|
||||||
lnd_grpc_macaroon_encrypted: Optional[str] = Field(default=None)
|
lnd_grpc_macaroon_encrypted: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class LnPayFundingSource(LNbitsSettings):
|
class LnPayFundingSource(LNbitsSettings):
|
||||||
lnpay_api_endpoint: Optional[str] = Field(default=None)
|
lnpay_api_endpoint: str | None = Field(default=None)
|
||||||
lnpay_api_key: Optional[str] = Field(default=None)
|
lnpay_api_key: str | None = Field(default=None)
|
||||||
lnpay_wallet_key: Optional[str] = Field(default=None)
|
lnpay_wallet_key: str | None = Field(default=None)
|
||||||
lnpay_admin_key: Optional[str] = Field(default=None)
|
lnpay_admin_key: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class BlinkFundingSource(LNbitsSettings):
|
class BlinkFundingSource(LNbitsSettings):
|
||||||
blink_api_endpoint: Optional[str] = Field(default="https://api.blink.sv/graphql")
|
blink_api_endpoint: str | None = Field(default="https://api.blink.sv/graphql")
|
||||||
blink_ws_endpoint: Optional[str] = Field(default="wss://ws.blink.sv/graphql")
|
blink_ws_endpoint: str | None = Field(default="wss://ws.blink.sv/graphql")
|
||||||
blink_token: Optional[str] = Field(default=None)
|
blink_token: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class ZBDFundingSource(LNbitsSettings):
|
class ZBDFundingSource(LNbitsSettings):
|
||||||
zbd_api_endpoint: Optional[str] = Field(default="https://api.zebedee.io/v0/")
|
zbd_api_endpoint: str | None = Field(default="https://api.zebedee.io/v0/")
|
||||||
zbd_api_key: Optional[str] = Field(default=None)
|
zbd_api_key: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class PhoenixdFundingSource(LNbitsSettings):
|
class PhoenixdFundingSource(LNbitsSettings):
|
||||||
phoenixd_api_endpoint: Optional[str] = Field(default="http://localhost:9740/")
|
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
|
||||||
phoenixd_api_password: Optional[str] = Field(default=None)
|
phoenixd_api_password: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class AlbyFundingSource(LNbitsSettings):
|
class AlbyFundingSource(LNbitsSettings):
|
||||||
alby_api_endpoint: Optional[str] = Field(default="https://api.getalby.com/")
|
alby_api_endpoint: str | None = Field(default="https://api.getalby.com/")
|
||||||
alby_access_token: Optional[str] = Field(default=None)
|
alby_access_token: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class OpenNodeFundingSource(LNbitsSettings):
|
class OpenNodeFundingSource(LNbitsSettings):
|
||||||
opennode_api_endpoint: Optional[str] = Field(default=None)
|
opennode_api_endpoint: str | None = Field(default=None)
|
||||||
opennode_key: Optional[str] = Field(default=None)
|
opennode_key: str | None = Field(default=None)
|
||||||
opennode_admin_key: Optional[str] = Field(default=None)
|
opennode_admin_key: str | None = Field(default=None)
|
||||||
opennode_invoice_key: Optional[str] = Field(default=None)
|
opennode_invoice_key: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class SparkFundingSource(LNbitsSettings):
|
class SparkFundingSource(LNbitsSettings):
|
||||||
spark_url: Optional[str] = Field(default=None)
|
spark_url: str | None = Field(default=None)
|
||||||
spark_token: Optional[str] = Field(default=None)
|
spark_token: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class LnTipsFundingSource(LNbitsSettings):
|
class LnTipsFundingSource(LNbitsSettings):
|
||||||
lntips_api_endpoint: Optional[str] = Field(default=None)
|
lntips_api_endpoint: str | None = Field(default=None)
|
||||||
lntips_api_key: Optional[str] = Field(default=None)
|
lntips_api_key: str | None = Field(default=None)
|
||||||
lntips_admin_key: Optional[str] = Field(default=None)
|
lntips_admin_key: str | None = Field(default=None)
|
||||||
lntips_invoice_key: Optional[str] = Field(default=None)
|
lntips_invoice_key: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class NWCFundingSource(LNbitsSettings):
|
class NWCFundingSource(LNbitsSettings):
|
||||||
nwc_pairing_url: Optional[str] = Field(default=None)
|
nwc_pairing_url: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class BreezSdkFundingSource(LNbitsSettings):
|
class BreezSdkFundingSource(LNbitsSettings):
|
||||||
breez_api_key: Optional[str] = Field(default=None)
|
breez_api_key: str | None = Field(default=None)
|
||||||
breez_greenlight_seed: Optional[str] = Field(default=None)
|
breez_greenlight_seed: str | None = Field(default=None)
|
||||||
breez_greenlight_invite_code: Optional[str] = Field(default=None)
|
breez_greenlight_invite_code: str | None = Field(default=None)
|
||||||
breez_greenlight_device_key: Optional[str] = Field(default=None)
|
breez_greenlight_device_key: str | None = Field(default=None)
|
||||||
breez_greenlight_device_cert: Optional[str] = Field(default=None)
|
breez_greenlight_device_cert: str | None = Field(default=None)
|
||||||
breez_use_trampoline: bool = Field(default=True)
|
breez_use_trampoline: bool = Field(default=True)
|
||||||
|
|
||||||
|
|
||||||
class BoltzFundingSource(LNbitsSettings):
|
class BoltzFundingSource(LNbitsSettings):
|
||||||
boltz_client_endpoint: Optional[str] = Field(default="127.0.0.1:9002")
|
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
|
||||||
boltz_client_macaroon: Optional[str] = Field(default=None)
|
boltz_client_macaroon: str | None = Field(default=None)
|
||||||
boltz_client_wallet: Optional[str] = Field(default="lnbits")
|
boltz_client_wallet: str | None = Field(default="lnbits")
|
||||||
boltz_client_cert: Optional[str] = Field(default=None)
|
boltz_client_cert: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class LightningSettings(LNbitsSettings):
|
class LightningSettings(LNbitsSettings):
|
||||||
lightning_invoice_expiry: int = Field(default=3600)
|
lightning_invoice_expiry: int = Field(default=3600, gt=0)
|
||||||
|
|
||||||
|
|
||||||
class FundingSourcesSettings(
|
class FundingSourcesSettings(
|
||||||
@@ -558,12 +584,12 @@ class FundingSourcesSettings(
|
|||||||
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
||||||
# How long to wait for the payment to be confirmed before returning a pending status
|
# 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
|
# 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)
|
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
|
||||||
|
|
||||||
|
|
||||||
class WebPushSettings(LNbitsSettings):
|
class WebPushSettings(LNbitsSettings):
|
||||||
lnbits_webpush_pubkey: Optional[str] = Field(default=None)
|
lnbits_webpush_pubkey: str | None = Field(default=None)
|
||||||
lnbits_webpush_privkey: Optional[str] = Field(default=None)
|
lnbits_webpush_privkey: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class NodeUISettings(LNbitsSettings):
|
class NodeUISettings(LNbitsSettings):
|
||||||
@@ -597,7 +623,7 @@ class AuthMethods(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class AuthSettings(LNbitsSettings):
|
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_all_methods = [a.value for a in AuthMethods]
|
||||||
auth_allowed_methods: list[str] = Field(
|
auth_allowed_methods: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
@@ -607,7 +633,7 @@ class AuthSettings(LNbitsSettings):
|
|||||||
)
|
)
|
||||||
# How many seconds after login the user is allowed to update its credentials.
|
# How many seconds after login the user is allowed to update its credentials.
|
||||||
# A fresh login is required afterwards.
|
# 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):
|
def is_auth_method_allowed(self, method: AuthMethods):
|
||||||
return method.value in self.auth_allowed_methods
|
return method.value in self.auth_allowed_methods
|
||||||
@@ -639,7 +665,7 @@ class AuditSettings(LNbitsSettings):
|
|||||||
lnbits_audit_enabled: bool = Field(default=True)
|
lnbits_audit_enabled: bool = Field(default=True)
|
||||||
|
|
||||||
# number of days to keep the audit entry
|
# 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_ip_address: bool = Field(default=False)
|
||||||
lnbits_audit_log_path_params: bool = Field(default=True)
|
lnbits_audit_log_path_params: bool = Field(default=True)
|
||||||
@@ -669,9 +695,9 @@ class AuditSettings(LNbitsSettings):
|
|||||||
|
|
||||||
def audit_http_request(
|
def audit_http_request(
|
||||||
self,
|
self,
|
||||||
http_method: Optional[str] = None,
|
http_method: str | None = None,
|
||||||
path: Optional[str] = None,
|
path: str | None = None,
|
||||||
http_response_code: Optional[str] = None,
|
http_response_code: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self.lnbits_audit_enabled:
|
if not self.lnbits_audit_enabled:
|
||||||
return False
|
return False
|
||||||
@@ -689,24 +715,24 @@ class AuditSettings(LNbitsSettings):
|
|||||||
|
|
||||||
return True
|
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:
|
if len(self.lnbits_audit_exclude_paths) != 0 and path:
|
||||||
for exclude_path in self.lnbits_audit_exclude_paths:
|
for exclude_path in self.lnbits_audit_exclude_paths:
|
||||||
if _re_fullmatch_safe(exclude_path, path):
|
if _re_fullmatch_safe(exclude_path, path):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if len(self.lnbits_audit_include_paths) != 0:
|
if len(self.lnbits_audit_include_paths) == 0:
|
||||||
if not path:
|
return True
|
||||||
return False
|
|
||||||
for include_path in self.lnbits_audit_include_paths:
|
if not path:
|
||||||
if _re_fullmatch_safe(include_path, path):
|
return False
|
||||||
return True
|
for include_path in self.lnbits_audit_include_paths:
|
||||||
|
if _re_fullmatch_safe(include_path, path):
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _is_http_response_code_auditable(
|
def _is_http_response_code_auditable(self, http_response_code: str | None) -> bool:
|
||||||
self, http_response_code: Optional[str]
|
|
||||||
) -> bool:
|
|
||||||
if not http_response_code:
|
if not http_response_code:
|
||||||
# No response code means only request filters should apply
|
# No response code means only request filters should apply
|
||||||
return True
|
return True
|
||||||
@@ -776,7 +802,7 @@ class EnvSettings(LNbitsSettings):
|
|||||||
debug_database: bool = Field(default=False)
|
debug_database: bool = Field(default=False)
|
||||||
bundle_assets: bool = Field(default=True)
|
bundle_assets: bool = Field(default=True)
|
||||||
host: str = Field(default="127.0.0.1")
|
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="*")
|
forwarded_allow_ips: str = Field(default="*")
|
||||||
lnbits_title: str = Field(default="LNbits API")
|
lnbits_title: str = Field(default="LNbits API")
|
||||||
lnbits_path: str = Field(default=".")
|
lnbits_path: str = Field(default=".")
|
||||||
@@ -788,24 +814,27 @@ class EnvSettings(LNbitsSettings):
|
|||||||
enable_log_to_file: bool = Field(default=True)
|
enable_log_to_file: bool = Field(default=True)
|
||||||
log_rotation: str = Field(default="100 MB")
|
log_rotation: str = Field(default="100 MB")
|
||||||
log_retention: str = Field(default="3 months")
|
log_retention: str = Field(default="3 months")
|
||||||
server_startup_time: int = Field(default=time())
|
|
||||||
cleanup_wallets_days: int = Field(default=90)
|
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||||
funding_source_max_retries: int = Field(default=4)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_default_extension_path(self) -> bool:
|
def has_default_extension_path(self) -> bool:
|
||||||
return self.lnbits_extensions_path == "lnbits"
|
return self.lnbits_extensions_path == "lnbits"
|
||||||
|
|
||||||
@property
|
def check_auth_secret_key(self):
|
||||||
def lnbits_server_up_time(self) -> str:
|
if self.auth_secret_key:
|
||||||
up_time = int(time() - self.server_startup_time)
|
return
|
||||||
return strftime("%H:%M:%S", gmtime(up_time))
|
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")
|
||||||
class SaaSSettings(LNbitsSettings):
|
if auth_key_file.is_file():
|
||||||
lnbits_saas_callback: Optional[str] = Field(default=None)
|
with open(auth_key_file) as file:
|
||||||
lnbits_saas_secret: Optional[str] = Field(default=None)
|
self.auth_secret_key = file.readline()
|
||||||
lnbits_saas_instance_id: Optional[str] = Field(default=None)
|
return
|
||||||
|
self.auth_secret_key = uuid4().hex
|
||||||
|
with open(auth_key_file, "w+") as file:
|
||||||
|
file.write(self.auth_secret_key)
|
||||||
|
|
||||||
|
|
||||||
class PersistenceSettings(LNbitsSettings):
|
class PersistenceSettings(LNbitsSettings):
|
||||||
@@ -857,6 +886,13 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
|||||||
|
|
||||||
lnbits_all_extensions_ids: set[str] = Field(default=[])
|
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
|
@classmethod
|
||||||
def readonly_fields(cls):
|
def readonly_fields(cls):
|
||||||
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
||||||
@@ -865,7 +901,6 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
|||||||
class ReadOnlySettings(
|
class ReadOnlySettings(
|
||||||
EnvSettings,
|
EnvSettings,
|
||||||
ExtensionsInstallSettings,
|
ExtensionsInstallSettings,
|
||||||
SaaSSettings,
|
|
||||||
PersistenceSettings,
|
PersistenceSettings,
|
||||||
SuperUserSettings,
|
SuperUserSettings,
|
||||||
):
|
):
|
||||||
@@ -903,7 +938,7 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
|||||||
or user_id == self.super_user
|
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
|
return user_id == self.super_user
|
||||||
|
|
||||||
def is_admin_user(self, user_id: str) -> bool:
|
def is_admin_user(self, user_id: str) -> bool:
|
||||||
@@ -922,12 +957,12 @@ class SuperSettings(EditableSettings):
|
|||||||
|
|
||||||
class AdminSettings(EditableSettings):
|
class AdminSettings(EditableSettings):
|
||||||
is_super_user: bool
|
is_super_user: bool
|
||||||
lnbits_allowed_funding_sources: Optional[list[str]]
|
lnbits_allowed_funding_sources: list[str] | None
|
||||||
|
|
||||||
|
|
||||||
class SettingsField(BaseModel):
|
class SettingsField(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
value: Optional[Any]
|
value: Any | None
|
||||||
tag: str = "core"
|
tag: str = "core"
|
||||||
|
|
||||||
|
|
||||||
@@ -944,31 +979,6 @@ def set_cli_settings(**kwargs):
|
|||||||
setattr(settings, key, value)
|
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()
|
readonly_variables = ReadOnlySettings.readonly_fields()
|
||||||
transient_variables = TransientSettings.readonly_fields()
|
transient_variables = TransientSettings.readonly_fields()
|
||||||
|
|
||||||
@@ -977,9 +987,8 @@ settings = Settings()
|
|||||||
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
||||||
|
|
||||||
settings.version = importlib.metadata.version("lnbits")
|
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:
|
if not settings.user_agent:
|
||||||
settings.user_agent = f"LNbits/{settings.version}"
|
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
+7
-5
File diff suppressed because one or more lines are too long
@@ -110,16 +110,16 @@ body[data-theme=bitcoin].body--light {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme=bitcoin] .bg-primary {
|
[data-theme=bitcoin] .bg-primary {
|
||||||
background: #ff9853 !important;
|
background: #ea611d !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-primary {
|
[data-theme=bitcoin] .text-primary {
|
||||||
color: #ff9853 !important;
|
color: #ea611d !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-secondary {
|
[data-theme=bitcoin] .bg-secondary {
|
||||||
background: #ff7353 !important;
|
background: #e56f35 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-secondary {
|
[data-theme=bitcoin] .text-secondary {
|
||||||
color: #ff7353 !important;
|
color: #e56f35 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-dark {
|
[data-theme=bitcoin] .bg-dark {
|
||||||
background: #2d293b !important;
|
background: #2d293b !important;
|
||||||
@@ -134,10 +134,10 @@ body[data-theme=bitcoin].body--light {
|
|||||||
color: #333646 !important;
|
color: #333646 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-marginal-bg {
|
[data-theme=bitcoin] .bg-marginal-bg {
|
||||||
background: #2d293b !important;
|
background: #000000 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-marginal-bg {
|
[data-theme=bitcoin] .text-marginal-bg {
|
||||||
color: #2d293b !important;
|
color: #000000 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-marginal-text {
|
[data-theme=bitcoin] .bg-marginal-text {
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
+43
-11
@@ -27,6 +27,7 @@ window.localisation.en = {
|
|||||||
close_channel: 'Close Channel',
|
close_channel: 'Close Channel',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
restart: 'Restart server',
|
restart: 'Restart server',
|
||||||
|
image_library: 'Image Library',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
save_tooltip: 'Save your changes',
|
save_tooltip: 'Save your changes',
|
||||||
credit_debit: 'Credit / Debit',
|
credit_debit: 'Credit / Debit',
|
||||||
@@ -64,9 +65,9 @@ window.localisation.en = {
|
|||||||
donate: 'Donate',
|
donate: 'Donate',
|
||||||
view_github: 'View on GitHub',
|
view_github: 'View on GitHub',
|
||||||
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
||||||
use_with_caution: 'USE WITH CAUTION - {name} wallet is still in BETA',
|
service_fee_badge: 'Service fee: {amount} % per transaction',
|
||||||
service_fee: 'Service fee: {amount} % per transaction',
|
service_fee_max_badge:
|
||||||
service_fee_max: 'Service fee: {amount} % per transaction (max {max} sats)',
|
'Service fee: {amount} % per transaction (max {max} {denom})',
|
||||||
service_fee_tooltip:
|
service_fee_tooltip:
|
||||||
'Service fee charged by the LNbits server admin per outgoing transaction',
|
'Service fee charged by the LNbits server admin per outgoing transaction',
|
||||||
toggle_darkmode: 'Toggle Dark Mode',
|
toggle_darkmode: 'Toggle Dark Mode',
|
||||||
@@ -82,6 +83,7 @@ window.localisation.en = {
|
|||||||
create_invoice: 'Create Invoice',
|
create_invoice: 'Create Invoice',
|
||||||
camera_tooltip: 'Use camera to scan an invoice/QR',
|
camera_tooltip: 'Use camera to scan an invoice/QR',
|
||||||
export_csv: 'Export to CSV',
|
export_csv: 'Export to CSV',
|
||||||
|
export_csv_details: 'Export to CSV with details',
|
||||||
chart_tooltip: 'Show chart',
|
chart_tooltip: 'Show chart',
|
||||||
pending: 'Pending',
|
pending: 'Pending',
|
||||||
copy_invoice: 'Copy invoice',
|
copy_invoice: 'Copy invoice',
|
||||||
@@ -115,7 +117,7 @@ window.localisation.en = {
|
|||||||
copy_wallet_url: 'Copy wallet URL',
|
copy_wallet_url: 'Copy wallet URL',
|
||||||
disclaimer_dialog_title: 'Important!',
|
disclaimer_dialog_title: 'Important!',
|
||||||
disclaimer_dialog:
|
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',
|
no_transactions: 'No transactions made yet',
|
||||||
manage: 'Manage',
|
manage: 'Manage',
|
||||||
exchanges: 'Exchanges',
|
exchanges: 'Exchanges',
|
||||||
@@ -174,6 +176,7 @@ window.localisation.en = {
|
|||||||
payment_proof: 'Payment Proof',
|
payment_proof: 'Payment Proof',
|
||||||
update: 'Update',
|
update: 'Update',
|
||||||
update_available: 'Update {version} available!',
|
update_available: 'Update {version} available!',
|
||||||
|
funding_sources: 'Funding Sources',
|
||||||
latest_update: 'You are on the latest version {version}.',
|
latest_update: 'You are on the latest version {version}.',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
notifications_configure: 'Configure Notifications',
|
notifications_configure: 'Configure Notifications',
|
||||||
@@ -195,6 +198,26 @@ window.localisation.en = {
|
|||||||
notifications_chat_id: 'Chat ID',
|
notifications_chat_id: 'Chat ID',
|
||||||
notifications_chat_id_desc: 'Chat ID to send the notifications to',
|
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: 'Settings updated',
|
||||||
notification_settings_update_desc:
|
notification_settings_update_desc:
|
||||||
'Notify when server settings have been updated',
|
'Notify when server settings have been updated',
|
||||||
@@ -293,11 +316,13 @@ window.localisation.en = {
|
|||||||
password: 'Password',
|
password: 'Password',
|
||||||
password_config: 'Password Config',
|
password_config: 'Password Config',
|
||||||
password_repeat: 'Password repeat',
|
password_repeat: 'Password repeat',
|
||||||
|
update_password: 'Update Password',
|
||||||
change_password: 'Change Password',
|
change_password: 'Change Password',
|
||||||
update_credentials: 'Update Credentials',
|
update_credentials: 'Update Credentials',
|
||||||
update_pubkey: 'Update Public Key',
|
update_pubkey: 'Update Public Key',
|
||||||
set_password: 'Set Password',
|
set_password: 'Set Password',
|
||||||
invalid_password: 'Password must have at least 8 characters',
|
invalid_password: 'Password must have at least 8 characters',
|
||||||
|
invalid_password_repeat: 'Passwords do not match',
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
register: 'Register',
|
register: 'Register',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
@@ -313,6 +338,7 @@ window.localisation.en = {
|
|||||||
invalid_username: 'Invalid Username',
|
invalid_username: 'Invalid Username',
|
||||||
auth_provider: 'Auth Provider',
|
auth_provider: 'Auth Provider',
|
||||||
my_account: 'My Account',
|
my_account: 'My Account',
|
||||||
|
existing_account_question: 'Already have an account?',
|
||||||
background_image: 'Background Image',
|
background_image: 'Background Image',
|
||||||
back: 'Back',
|
back: 'Back',
|
||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
@@ -471,9 +497,11 @@ window.localisation.en = {
|
|||||||
'Disable Service Fee for Internal Lightning Payments',
|
'Disable Service Fee for Internal Lightning Payments',
|
||||||
ui_management: 'UI Management',
|
ui_management: 'UI Management',
|
||||||
ui_site_title: 'Site Title',
|
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_site_tagline: 'Site Tagline',
|
||||||
ui_elements_enable: 'Enable elements on homepage',
|
ui_elements_enable: 'Enable elements on homepage/footer',
|
||||||
ui_elements_disable: 'Disable elements on homepage',
|
ui_elements_disable: 'Disable elements on homepage/footer',
|
||||||
ui_toggle_elements_tip: "Remove homepage elements like 'runs on' etc",
|
ui_toggle_elements_tip: "Remove homepage elements like 'runs on' etc",
|
||||||
ui_site_description: 'Site Description',
|
ui_site_description: 'Site Description',
|
||||||
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
||||||
@@ -482,14 +510,14 @@ window.localisation.en = {
|
|||||||
lnbits_wallet: 'LNbits wallet',
|
lnbits_wallet: 'LNbits wallet',
|
||||||
denomination: 'Denomination',
|
denomination: 'Denomination',
|
||||||
denomination_hint: 'The name for the FakeWallet token',
|
denomination_hint: 'The name for the FakeWallet token',
|
||||||
ui_qr_code_logo: 'QR Code Logo',
|
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
||||||
ui_qr_code_logo_hint: 'URL to logo image in QR code',
|
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: 'Custom Image',
|
||||||
ui_custom_image_label: 'URL to custom image',
|
ui_custom_image_label: 'URL to custom image',
|
||||||
ui_custom_image_hint: 'Image showed at homepage/login',
|
ui_custom_image_hint: 'Image showed at homepage/login',
|
||||||
ui_custom_badge: 'Custom Badge',
|
ui_custom_badge: 'Custom Badge',
|
||||||
ui_custom_badge_label:
|
ui_custom_badge_label: "Custom Badge 'USE WITH CAUTION'",
|
||||||
"Custom Badge 'USE WITH CAUTION - LNbits wallet is still in BETA'",
|
|
||||||
ui_custom_badge_color_label: 'Custom Badge Color',
|
ui_custom_badge_color_label: 'Custom Badge Color',
|
||||||
themes: 'Themes',
|
themes: 'Themes',
|
||||||
themes_hint: 'Choose themes available for users',
|
themes_hint: 'Choose themes available for users',
|
||||||
@@ -512,6 +540,7 @@ window.localisation.en = {
|
|||||||
allowed_users_label: 'User ID',
|
allowed_users_label: 'User ID',
|
||||||
allow_creation_user: 'Allow creation of new users',
|
allow_creation_user: 'Allow creation of new users',
|
||||||
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
||||||
|
new_user_not_allowed: 'Registration is disabled.',
|
||||||
components: 'Components',
|
components: 'Components',
|
||||||
long_running_endpoints: 'Top 5 Long Running Endpoints',
|
long_running_endpoints: 'Top 5 Long Running Endpoints',
|
||||||
http_request_methods: 'HTTP Request Methods',
|
http_request_methods: 'HTTP Request Methods',
|
||||||
@@ -539,5 +568,8 @@ window.localisation.en = {
|
|||||||
reset_wallet_keys_desc:
|
reset_wallet_keys_desc:
|
||||||
'Reset the API keys for this wallet. This will invalidate the current keys and generate new ones.',
|
'Reset the API keys for this wallet. This will invalidate the current keys and generate new ones.',
|
||||||
view_list: 'View wallets as list',
|
view_list: 'View wallets as list',
|
||||||
view_column: 'View wallets as rows'
|
view_column: 'View wallets as rows',
|
||||||
|
filter_payments: 'Filter payments',
|
||||||
|
filter_date: 'Filter by date',
|
||||||
|
websocket_example: 'Websocket example'
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-55
@@ -5,8 +5,8 @@ window.localisation.fi = {
|
|||||||
site_customisation: 'Sivuston kustomointi',
|
site_customisation: 'Sivuston kustomointi',
|
||||||
funding: 'Rahoitus',
|
funding: 'Rahoitus',
|
||||||
users: 'Käyttäjät',
|
users: 'Käyttäjät',
|
||||||
audit: 'Auditointi',
|
audit: 'Seuranta',
|
||||||
api_watch: 'Api Watch',
|
api_watch: 'API-seuranta',
|
||||||
apps: 'Sovellukset',
|
apps: 'Sovellukset',
|
||||||
channels: 'Kanavat',
|
channels: 'Kanavat',
|
||||||
transactions: 'Tapahtumat',
|
transactions: 'Tapahtumat',
|
||||||
@@ -27,6 +27,7 @@ window.localisation.fi = {
|
|||||||
close_channel: 'Sulje kanava',
|
close_channel: 'Sulje kanava',
|
||||||
close: 'Sulje',
|
close: 'Sulje',
|
||||||
restart: 'Palvelimen uudelleen käynnistys',
|
restart: 'Palvelimen uudelleen käynnistys',
|
||||||
|
image_library: 'Kuvakirjasto',
|
||||||
save: 'Tallenna',
|
save: 'Tallenna',
|
||||||
save_tooltip: 'Tallenna muutokset',
|
save_tooltip: 'Tallenna muutokset',
|
||||||
credit_debit: 'Hyvitä / Veloita',
|
credit_debit: 'Hyvitä / Veloita',
|
||||||
@@ -37,7 +38,7 @@ window.localisation.fi = {
|
|||||||
'Virtuaalivarojen ({amount} sat) hyvitys-/veloitustapahtuma onnistui. Maksukyky riippuuu rahoituslähteen todellisista varoista.',
|
'Virtuaalivarojen ({amount} sat) hyvitys-/veloitustapahtuma onnistui. Maksukyky riippuuu rahoituslähteen todellisista varoista.',
|
||||||
restart_tooltip: 'Uudelleenkäynnistä palvelu muutosten käyttöönottamiseksi',
|
restart_tooltip: 'Uudelleenkäynnistä palvelu muutosten käyttöönottamiseksi',
|
||||||
add_funds_tooltip: 'Lisää varoja lompakkoon',
|
add_funds_tooltip: 'Lisää varoja lompakkoon',
|
||||||
reset_defaults: 'Peruuta muutokset',
|
reset_defaults: 'Palauta oletusasetukset',
|
||||||
reset_defaults_tooltip:
|
reset_defaults_tooltip:
|
||||||
'Poista kaikki asetusten muutokset ja palauta järjestelmän oletusasetukset.',
|
'Poista kaikki asetusten muutokset ja palauta järjestelmän oletusasetukset.',
|
||||||
download_backup: 'Lataa tietokannan varmuuskopio',
|
download_backup: 'Lataa tietokannan varmuuskopio',
|
||||||
@@ -65,8 +66,7 @@ window.localisation.fi = {
|
|||||||
'Näyttää että sinulla on lunastamattomia bitcoin varoja, mutta sinulla ei vielä ole lompakkoa. Lunasta varat allaolevaa nappia painamalla, ja sinulle luodaan lompakko.',
|
'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',
|
donate: 'Lahjoita',
|
||||||
view_github: 'Näytä GitHub:ssa',
|
view_github: 'Näytä GitHub:ssa',
|
||||||
voidwallet_active:
|
voidwallet_active: 'VoidWallet on aktiivinen. Se ei tue maksutapahtumia!',
|
||||||
'Maksutapahtumat ovat poissa käytöstä, koska VoidWallet on aktiivinen!',
|
|
||||||
use_with_caution:
|
use_with_caution:
|
||||||
'KÄYTÄ VAROEN - BETA-ohjelmisto on käytössä palvelussa: {name}',
|
'KÄYTÄ VAROEN - BETA-ohjelmisto on käytössä palvelussa: {name}',
|
||||||
service_fee_tooltip:
|
service_fee_tooltip:
|
||||||
@@ -84,6 +84,7 @@ window.localisation.fi = {
|
|||||||
create_invoice: 'Laskuta',
|
create_invoice: 'Laskuta',
|
||||||
camera_tooltip: 'Kuvaa lasku tai QR-koodi',
|
camera_tooltip: 'Kuvaa lasku tai QR-koodi',
|
||||||
export_csv: 'Vie CSV-tiedostoon',
|
export_csv: 'Vie CSV-tiedostoon',
|
||||||
|
export_csv_details: 'Vie CSV-tiedostoon lisätietoineen',
|
||||||
chart_tooltip: 'Näytä kaaviokuva',
|
chart_tooltip: 'Näytä kaaviokuva',
|
||||||
pending: 'Odottaa',
|
pending: 'Odottaa',
|
||||||
copy_invoice: 'Kopioi lasku',
|
copy_invoice: 'Kopioi lasku',
|
||||||
@@ -113,10 +114,10 @@ window.localisation.fi = {
|
|||||||
copy_wallet_url: 'Kopioi lompakon URL',
|
copy_wallet_url: 'Kopioi lompakon URL',
|
||||||
disclaimer_dialog_title: 'Tärkeää!',
|
disclaimer_dialog_title: 'Tärkeää!',
|
||||||
disclaimer_dialog:
|
disclaimer_dialog:
|
||||||
'Sinun *PITÄÄ TALLETTAA* kirjautumistietosi turvallisesta ja helposti saataville, jotta pääset jatkossa kirjautumaan lompakkoosi! Löydät kirjautumistiedot Tilin asetukset -sivulta. 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',
|
no_transactions: 'Lompakossa ei ole yhtään tapahtumaa',
|
||||||
manage: 'Hallinnointi',
|
manage: 'Hallinnointi',
|
||||||
exchanges: 'Pörssit',
|
exchanges: 'Vaihtokurssit',
|
||||||
extensions: 'Laajennukset',
|
extensions: 'Laajennukset',
|
||||||
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
||||||
created: 'Luotu',
|
created: 'Luotu',
|
||||||
@@ -124,7 +125,7 @@ window.localisation.fi = {
|
|||||||
extension_sources: 'Laajennuslähteet',
|
extension_sources: 'Laajennuslähteet',
|
||||||
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
|
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
|
||||||
ext_sources_label:
|
ext_sources_label:
|
||||||
'Lähde-URL (käytä vain virallista LNbits ja luotettavia laajennuslähteitä)',
|
'Lähde-URL (käytä vain virallista LNbits tai muuta luotettaa laajennuslähdettä)',
|
||||||
warning: 'Varoitus',
|
warning: 'Varoitus',
|
||||||
repository: 'Laajennuksien lähde',
|
repository: 'Laajennuksien lähde',
|
||||||
confirm_continue: 'Haluatko varmasti jatkaa?',
|
confirm_continue: 'Haluatko varmasti jatkaa?',
|
||||||
@@ -168,11 +169,12 @@ window.localisation.fi = {
|
|||||||
tag: 'Tunniste',
|
tag: 'Tunniste',
|
||||||
unit: 'Yksikkö',
|
unit: 'Yksikkö',
|
||||||
description: 'Kuvaus',
|
description: 'Kuvaus',
|
||||||
expiry: 'Vanheneminen',
|
expiry: 'Vanhenee',
|
||||||
webhook: 'Webhook',
|
webhook: 'Webhook',
|
||||||
payment_proof: 'Maksun varmenne',
|
payment_proof: 'Maksun varmenne',
|
||||||
update: 'Päivitä',
|
update: 'Päivitä',
|
||||||
update_available: 'Saatavilla on päivitys {version}-versioon!',
|
update_available: 'Saatavilla on päivitys {version}-versioon!',
|
||||||
|
update_available: 'Rahoituslähteet',
|
||||||
latest_update:
|
latest_update:
|
||||||
'Käytössä oleva versio {version}, on viimeisin saatavilla oleva.',
|
'Käytössä oleva versio {version}, on viimeisin saatavilla oleva.',
|
||||||
notifications: 'Tiedotteet',
|
notifications: 'Tiedotteet',
|
||||||
@@ -182,7 +184,7 @@ window.localisation.fi = {
|
|||||||
notifications_enable_nostr_desc: 'Lähetä tietodukset Nostr:in kautta',
|
notifications_enable_nostr_desc: 'Lähetä tietodukset Nostr:in kautta',
|
||||||
notifications_nostr_private_key: 'Nostr-yksityisavain',
|
notifications_nostr_private_key: 'Nostr-yksityisavain',
|
||||||
notifications_nostr_private_key_desc:
|
notifications_nostr_private_key_desc:
|
||||||
'Yksityinen avain (hex tai nsec) Nostr viestien lähettämisen allekirjoitukseen',
|
'Yksityinen avain (hex tai nsec) Nostr-viestien lähettämisen allekirjoitukseen',
|
||||||
notifications_nostr_identifiers: 'Nostr-tunnisteet',
|
notifications_nostr_identifiers: 'Nostr-tunnisteet',
|
||||||
notifications_nostr_identifiers_desc:
|
notifications_nostr_identifiers_desc:
|
||||||
'Lista tunnisteista kenelle tiedotukset lähetetään',
|
'Lista tunnisteista kenelle tiedotukset lähetetään',
|
||||||
@@ -195,6 +197,25 @@ window.localisation.fi = {
|
|||||||
notifications_chat_id: 'Keskustelun tunnus',
|
notifications_chat_id: 'Keskustelun tunnus',
|
||||||
notifications_chat_id_desc: 'Keskustelun tunnus minne tiedotukset lähetetään',
|
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: 'Asetuksia päivitetty',
|
||||||
notification_settings_update_desc:
|
notification_settings_update_desc:
|
||||||
'Tiedota kun palvelimen asetuksia on päivitetty',
|
'Tiedota kun palvelimen asetuksia on päivitetty',
|
||||||
@@ -213,15 +234,15 @@ window.localisation.fi = {
|
|||||||
|
|
||||||
notification_incoming_payment: 'Saapuvat maksut',
|
notification_incoming_payment: 'Saapuvat maksut',
|
||||||
notification_incoming_payment_desc:
|
notification_incoming_payment_desc:
|
||||||
'Tiedota kun lompakko vastaanottaa ja saapuvan maksun määrä ylittää rajan (sat)',
|
'Tiedota kun lompakon vastaanottaman ja saapuvan maksun määrä ylittää rajan (sat)',
|
||||||
|
|
||||||
notification_outgoing_payment: 'Lähtevät maksut',
|
notification_outgoing_payment: 'Lähtevät maksut',
|
||||||
notification_outgoing_payment_desc:
|
notification_outgoing_payment_desc:
|
||||||
'Tiedota kun lompakko lähettää ja lähtevän maksun määrä ylittää rajan (sat)',
|
'Tiedota kun lompakon lähettävän ja maksettavan maksun määrä ylittää rajan (sat)',
|
||||||
|
|
||||||
notification_credit_debit: 'Hyvitä / Veloita',
|
notification_credit_debit: 'Hyvitys / Veloitus',
|
||||||
notification_credit_debit_desc:
|
notification_credit_debit_desc:
|
||||||
'Tiedota kun lompakkoa Superuser hyvittää tai veloittaa lompakkoa',
|
'Tiedota kun Superuser tekee lompakon hyvitys- tai veloitustapahtumia',
|
||||||
|
|
||||||
notification_balance_delta_changed: 'Saldon määrän muutos',
|
notification_balance_delta_changed: 'Saldon määrän muutos',
|
||||||
notification_balance_delta_changed_desc:
|
notification_balance_delta_changed_desc:
|
||||||
@@ -235,7 +256,7 @@ 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.',
|
'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: 'Watchdog Delta',
|
||||||
watchdog_delta_desc:
|
watchdog_delta_desc:
|
||||||
'Mikäli rahoituslähteen saldo laskee alle LNbits kokonaissaldon, muutetaan Rahoituslähteeksi muutetaan heti VoidWallet. Päivittämisen jälkeen asetus pitää päivittää manuaalisestsi.',
|
'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',
|
status: 'Tilanne',
|
||||||
notification_source: 'Tiedotteiden lähde',
|
notification_source: 'Tiedotteiden lähde',
|
||||||
notification_source_label:
|
notification_source_label:
|
||||||
@@ -245,38 +266,38 @@ window.localisation.fi = {
|
|||||||
releases: 'Julkaisut',
|
releases: 'Julkaisut',
|
||||||
watchdog: 'Watchdog',
|
watchdog: 'Watchdog',
|
||||||
server_logs: 'Palvelimen lokit',
|
server_logs: 'Palvelimen lokit',
|
||||||
ip_blocker: 'IP-suodatin',
|
ip_blocker: 'Palvelimen suojaus IP-osoitesuodattimella',
|
||||||
security: 'Turvallisuus',
|
security: 'Turvallisuus',
|
||||||
security_tools: 'Turvallisuus työkalut',
|
security_tools: 'Turvallisuus työkalut',
|
||||||
block_access_hint: 'Estä pääsy IP-osoitteen perusteella',
|
block_access_hint: 'Estä pääsy IP-osoitteen perusteella',
|
||||||
allow_access_hint: 'Salli pääsy IP-osoitteen perusteella (ohittaa estot)',
|
allow_access_hint: 'Salli pääsy IP-osoitteen perusteella (ohittaa estot)',
|
||||||
enter_ip: 'Anna IP ja paina +',
|
enter_ip: 'Anna IP ja paina +',
|
||||||
rate_limiter: 'Toiston rajoitin',
|
rate_limiter: 'Toiston rajoitin',
|
||||||
callback_url_rules: 'Callback URL säännöt',
|
callback_url_rules: 'Callback URL -säännöt',
|
||||||
enter_callback_url_rule: 'Anna URL säätö regex-muodossa ja paina enter',
|
enter_callback_url_rule: 'Anna URL-sääntö regex-muodossa ja paina enter',
|
||||||
callback_url_rule_hint:
|
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.',
|
'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_limiter: 'Lompakon käyttörajoitin',
|
||||||
wallet_config: 'Wallet Config',
|
wallet_config: 'Wallet Config',
|
||||||
wallet_charts: 'Wallet Charts',
|
wallet_charts: 'Wallet Charts',
|
||||||
wallet_limit_max_withdraw_per_day:
|
wallet_limit_max_withdraw_per_day:
|
||||||
'Päivittäinen lompakosta nostettavissa oleva määrä satosheissa (0 poistaa käytöstä)',
|
'Päivittäin nostettavissa sat maksimi (0 poistaa käytöstä)',
|
||||||
wallet_max_ballance: 'Lompakon maksimisaldo satosheina (0 poistaa käytöstä)',
|
wallet_max_ballance: 'Maksimisaldo (sat) (0 poistaa käytöstä)',
|
||||||
wallet_limit_secs_between_trans:
|
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: 'Only incoming payments allowed',
|
only_incoming_payments_allowed: 'Vain saapuvat maksut sallittuna',
|
||||||
disable_outgoing_payments: 'Disable outgoing payments',
|
disable_outgoing_payments: 'Poista lähtevät maksut käytöstä',
|
||||||
number_of_requests: 'Pyyntöjen lukumäärä',
|
number_of_requests: 'Pyyntöjen lukumäärä',
|
||||||
time_unit: 'aikayksikkö',
|
time_unit: 'aikayksikkö',
|
||||||
minute: 'minuutti',
|
minute: 'minuutti',
|
||||||
settings: 'Settings',
|
settings: 'Asetukset',
|
||||||
second: 'sekunti',
|
second: 'sekunti',
|
||||||
hour: 'tunti',
|
hour: 'tunti',
|
||||||
disable_server_log: 'Poista palvelimen loki käytöstä',
|
disable_server_log: 'Piilota palvelimen loki',
|
||||||
enable_server_log: 'Ota palvelimen loki käyttöön',
|
enable_server_log: 'Näytä palvelimen loki',
|
||||||
coming_soon: 'Ominaisuus on tulossa pian',
|
coming_soon: 'Ominaisuus on tulossa pian',
|
||||||
session_has_expired: 'Käyttämätön sessio on vanhentunut. Kirjaudu uudelleen.',
|
session_has_expired: 'Käyttämätön sessio on vanhentunut. Kirjaudu uudelleen.',
|
||||||
instant_access_question: 'tai kirjaudu suoraan',
|
instant_access_question: 'perinteinen kirjautuminen',
|
||||||
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
|
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
|
||||||
or: 'tai',
|
or: 'tai',
|
||||||
create_new_wallet: 'Avaa uusi lompakko',
|
create_new_wallet: 'Avaa uusi lompakko',
|
||||||
@@ -293,11 +314,13 @@ window.localisation.fi = {
|
|||||||
password: 'Anna uusi salasana',
|
password: 'Anna uusi salasana',
|
||||||
password_config: 'Salasanan määritys',
|
password_config: 'Salasanan määritys',
|
||||||
password_repeat: 'Toista uusi salasana',
|
password_repeat: 'Toista uusi salasana',
|
||||||
|
update_password: 'Päivitä salasana',
|
||||||
change_password: 'Vaihda salasana',
|
change_password: 'Vaihda salasana',
|
||||||
update_credentials: 'Päivitä käyttöoikeustiedot',
|
update_credentials: 'Päivitä käyttöoikeustiedot',
|
||||||
update_pubkey: 'Päivitä julkinen avain',
|
update_pubkey: 'Päivitä julkinen avain',
|
||||||
set_password: 'Aseta salasana',
|
set_password: 'Aseta salasana',
|
||||||
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
|
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
|
||||||
|
invalid_password_repeat: 'Salasanat eivät täsmää',
|
||||||
login: 'Kirjaudu',
|
login: 'Kirjaudu',
|
||||||
register: 'Rekisteröidy',
|
register: 'Rekisteröidy',
|
||||||
username: 'Käyttäjänimi',
|
username: 'Käyttäjänimi',
|
||||||
@@ -312,7 +335,8 @@ window.localisation.fi = {
|
|||||||
update_account: 'Päivitä tiliä',
|
update_account: 'Päivitä tiliä',
|
||||||
invalid_username: 'Virheellinen käyttäjänimi',
|
invalid_username: 'Virheellinen käyttäjänimi',
|
||||||
auth_provider: 'Tunnistamisen toimittaja',
|
auth_provider: 'Tunnistamisen toimittaja',
|
||||||
my_account: 'Minun tili',
|
my_account: 'Tilini',
|
||||||
|
existing_account_question: 'Onkohan sinulla jo tili?',
|
||||||
background_image: 'Taustakuva',
|
background_image: 'Taustakuva',
|
||||||
back: 'Takaisin',
|
back: 'Takaisin',
|
||||||
logout: 'Poistu',
|
logout: 'Poistu',
|
||||||
@@ -358,7 +382,7 @@ window.localisation.fi = {
|
|||||||
welcome_lnbits: 'Tervetuloa LNbits-palveluun',
|
welcome_lnbits: 'Tervetuloa LNbits-palveluun',
|
||||||
setup_su_account: 'Määritä Superuser-tili alta.',
|
setup_su_account: 'Määritä Superuser-tili alta.',
|
||||||
create_ticker_converter: 'Luo valuuttamuuntimen Ticker',
|
create_ticker_converter: 'Luo valuuttamuuntimen Ticker',
|
||||||
enable_audit: 'Ota auditointi käyttöön',
|
enable_audit: 'Ota seuranta käyttöön',
|
||||||
recommended: 'Suositeltu',
|
recommended: 'Suositeltu',
|
||||||
audit_desc: 'Tallenna HTTP-pyyntöjä seuraavien suodattimien mukaisesti',
|
audit_desc: 'Tallenna HTTP-pyyntöjä seuraavien suodattimien mukaisesti',
|
||||||
audit_record_req: 'Tallenna pyynnön Body',
|
audit_record_req: 'Tallenna pyynnön Body',
|
||||||
@@ -366,16 +390,16 @@ window.localisation.fi = {
|
|||||||
audit_record_req_warning_1:
|
audit_record_req_warning_1:
|
||||||
'Luottamukselliset tiedot (kuten salasanat) tallennetaan.',
|
'Luottamukselliset tiedot (kuten salasanat) tallennetaan.',
|
||||||
audit_record_req_warning_2: 'Body-datamäätä voi olla iso.',
|
audit_record_req_warning_2: 'Body-datamäätä voi olla iso.',
|
||||||
audit_record_use: 'Käytä sitä varoen.',
|
audit_record_use: 'Käytä varoen!',
|
||||||
audit_ip: 'Tallenna IP-osoite',
|
audit_ip: 'Tallenna IP-osoite',
|
||||||
audit_ip_desc: 'Tallenna asiakkaan IP-osoite',
|
audit_ip_desc: 'Tallenna asiakkaan IP-osoite',
|
||||||
audit_path_params: 'Tallenna Path-parametrit',
|
audit_path_params: 'Tallenna Path-parametrit',
|
||||||
audit_query_params: 'Tallenna Query-parametrit',
|
audit_query_params: 'Tallenna Query-parametrit',
|
||||||
audit_http_methods: 'Sisällytä HTTP-menetelmät',
|
audit_http_methods: 'Tallenna HTTP-menetelmät',
|
||||||
audit_http_methods_hint:
|
audit_http_methods_hint:
|
||||||
'Luettelo mukaan otettavista HTTP-menetelmistä. Tyhjä luettelo tallettaa kaikki.',
|
'Luettelo mukaan otettavista HTTP-menetelmistä. Tyhjä luettelo tallettaa kaikki.',
|
||||||
audit_http_methods_label: 'HTTP-metodit',
|
audit_http_methods_label: 'HTTP-metodit',
|
||||||
audit_resp_codes: 'Sisällytä HTTP-vastauskoodit',
|
audit_resp_codes: 'Tallenna HTTP-vastauskoodit',
|
||||||
audit_resp_codes_hint:
|
audit_resp_codes_hint:
|
||||||
'HTTP-koodien lista, jotka sisällytetään (regex-match). Tyhjä luettelo tallettaa kaikki. Esim: 4.*, 5.*',
|
'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_resp_codes_label: 'HTTP-vastauskoodi (säännöllinen lauseke)',
|
||||||
@@ -410,7 +434,7 @@ window.localisation.fi = {
|
|||||||
funding_reserve_percent: 'Omavaraisuusaste: {percent} %',
|
funding_reserve_percent: 'Omavaraisuusaste: {percent} %',
|
||||||
node_managment: 'Solmun hallinta',
|
node_managment: 'Solmun hallinta',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Valittu rahoituslähde ei mahdollista solmun hallintaan.',
|
'Solmun hallinta ei ole mahdollista valitun rahoituslähteen kanssa.',
|
||||||
toggle_node_ui: 'Solmun käyttöliittymä',
|
toggle_node_ui: 'Solmun käyttöliittymä',
|
||||||
toggle_public_node_ui: 'Julkinen näkymä solmun tietoihin',
|
toggle_public_node_ui: 'Julkinen näkymä solmun tietoihin',
|
||||||
toggle_transactions_node_ui:
|
toggle_transactions_node_ui:
|
||||||
@@ -418,17 +442,21 @@ window.localisation.fi = {
|
|||||||
invoice_expiry: 'Laskun vanhenemisaika',
|
invoice_expiry: 'Laskun vanhenemisaika',
|
||||||
invoice_expiry_label: 'Laskun vanhentuminen (sekunteina)',
|
invoice_expiry_label: 'Laskun vanhentuminen (sekunteina)',
|
||||||
fee_reserve: 'Kuluvaraus',
|
fee_reserve: 'Kuluvaraus',
|
||||||
fee_reserve_msats: 'Kuluvaraus milli-sat',
|
|
||||||
fee_reserve_percent: 'Kuluvaraus prosentteina',
|
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',
|
server_management: 'Palvelimen hallinta',
|
||||||
base_url: 'Perus-URL',
|
base_url: 'Palvelimen URL-osoite',
|
||||||
base_url_label: 'Staattinen/pohjan URL palvelimelle',
|
base_url_label: 'Palvelun staattinen pohja-URL',
|
||||||
authentication: 'Todennus',
|
authentication: 'Käyttäjän todennus',
|
||||||
auth_token_expiry_label: 'Tunnuksen vanhentumisaika minuutteina',
|
auth_token_expiry_label: 'Kirjautumisen vanhentumisaika minuutteina',
|
||||||
auth_token_expiry_hint: 'Aika minuuteissa, kunnes tunnus vanhenee',
|
auth_token_expiry_hint: 'Aika minuuteissa, jossa kirjautuminen vanhenee',
|
||||||
auth_allowed_methods_label: 'Sallitut kirjautumismenetelmät',
|
auth_allowed_methods_label: 'Sallitut kirjautumismenetelmät',
|
||||||
auth_allowed_methods_hint: 'Valitse kirjautumismenetelmät',
|
auth_allowed_methods_hint: 'Valitse kirjautumismenetelmät',
|
||||||
auth_nostr_label: 'Nostr Request URL',
|
auth_nostr_label: 'Nostr kutsujen URL',
|
||||||
auth_nostr_hint:
|
auth_nostr_hint:
|
||||||
'Asiakkaiden kirjautumiseen käyttämä absoluuttinen URL-osoite.',
|
'Asiakkaiden kirjautumiseen käyttämä absoluuttinen URL-osoite.',
|
||||||
auth_google_ci_label: 'Google-asiakastunnus',
|
auth_google_ci_label: 'Google-asiakastunnus',
|
||||||
@@ -446,27 +474,33 @@ window.localisation.fi = {
|
|||||||
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
||||||
currency_settings: 'Valuutta-asetukset',
|
currency_settings: 'Valuutta-asetukset',
|
||||||
allowed_currencies: 'Käytettävät valuutat',
|
allowed_currencies: 'Käytettävät valuutat',
|
||||||
allowed_currencies_hint:
|
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
||||||
'Rajoita käytettävissä olevien fiat-valuuttojen määrää',
|
|
||||||
default_account_currency: 'Tilin oletusvaluutta',
|
default_account_currency: 'Tilin oletusvaluutta',
|
||||||
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
|
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
|
||||||
service_fee: 'Palvelumaksu',
|
|
||||||
|
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_label: 'Palvelumaksu (%)',
|
||||||
service_fee_hint: 'Tapahtumastakohtainen palvelumaksu (%)',
|
service_fee_hint: 'Tapahtumastakohtainen palvelumaksu (%)',
|
||||||
service_fee_max: 'Palvelumaksun enimmäismäärä',
|
service_fee_max: 'Palvelumaksun enimmäismäärä',
|
||||||
service_fee_max_label: 'Palvelumaksu enintään (sat)',
|
service_fee_max_label: 'Palvelumaksu max (sat)',
|
||||||
service_fee_max_hint: 'Suurin veloitettava palvelumaksu (sat)',
|
service_fee_max_hint: 'Suurin veloitettava palvelumaksu (sat)',
|
||||||
fee_wallet: 'Palvelumaksujen lompakko',
|
fee_wallet: 'Palvelumaksujen lompakko',
|
||||||
fee_wallet_label: 'Palvelumaksujen lompakko (lompakon tunnus)',
|
fee_wallet_label: 'Palvelumaksujen tilityslompakko (lompakon tunnus)',
|
||||||
fee_wallet_hint: 'Lompakon tunnus, johon lähetetään maksut lähtetetään',
|
fee_wallet_hint: 'Lompakon tunnus, johon palvelumaksut tilitetään',
|
||||||
disable_fee: 'Poista maksu käytöstä',
|
disable_fee: 'Poista maksu käytöstä',
|
||||||
disable_fee_internal: 'Poista palvelumaksu sisäisiltä maksuilta',
|
disable_fee_internal: 'Poista palvelumaksu sisäisiltä maksuilta',
|
||||||
disable_fee_internal_desc: 'Poista palvelumaksu sisäisiltä salamamaksuilta',
|
disable_fee_internal_desc: 'Poista palvelumaksu sisäisiltä salamaksuilta',
|
||||||
ui_management: 'Käyttöliittymän hallinta',
|
ui_management: 'Käyttöliittymän hallinta',
|
||||||
ui_site_title: 'Sivuston nimi',
|
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_site_tagline: 'Sivuston iskulause',
|
||||||
ui_elements_enable: 'Ota käyttöön elementit etusivulla',
|
ui_elements_enable: 'Ota käyttöön elementit etusivulla/alareunassa',
|
||||||
ui_elements_disable: 'Poista elementit käytöstä etusivulla',
|
ui_elements_disable: 'Poista elementit käytöstä etusivulla/alareunassa',
|
||||||
ui_toggle_elements_tip: "Poista kotisivuelementit kuten 'toimii' jne.",
|
ui_toggle_elements_tip: "Poista kotisivuelementit kuten 'toimii' jne.",
|
||||||
ui_site_description: 'Sivuston kuvaus',
|
ui_site_description: 'Sivuston kuvaus',
|
||||||
ui_site_description_hint:
|
ui_site_description_hint:
|
||||||
@@ -476,10 +510,11 @@ window.localisation.fi = {
|
|||||||
lnbits_wallet: 'LNbits-lompakko',
|
lnbits_wallet: 'LNbits-lompakko',
|
||||||
denomination: 'Valuutan nimi',
|
denomination: 'Valuutan nimi',
|
||||||
denomination_hint: 'FakeWallet-lompakon valuutan nimi',
|
denomination_hint: 'FakeWallet-lompakon valuutan nimi',
|
||||||
ui_qr_code_logo: 'QR-koodin logo',
|
denomination_error: 'Valuutta tunnisssa on oltava 3 merkkiä, tai `sat`',
|
||||||
|
ui_qr_code_logo: 'QR- ja Favicon-logo',
|
||||||
ui_qr_code_logo_hint:
|
ui_qr_code_logo_hint:
|
||||||
'Anna QR-koodissa käytettävää logo-kuvaan osoittava URL',
|
'Anna QR-koodissa ja Faviconissa käytettävän logo-kuvan URL',
|
||||||
ui_custom_image: 'Yksilöyty kuva',
|
ui_custom_image: 'Yksilöity kuva',
|
||||||
ui_custom_image_label: 'Anna yksilöidyn kuvan URL-osoite',
|
ui_custom_image_label: 'Anna yksilöidyn kuvan URL-osoite',
|
||||||
ui_custom_image_hint:
|
ui_custom_image_hint:
|
||||||
'Yksilöity kuva näytetään aloitus- ja kirjautumissivuilla',
|
'Yksilöity kuva näytetään aloitus- ja kirjautumissivuilla',
|
||||||
@@ -507,7 +542,8 @@ window.localisation.fi = {
|
|||||||
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
|
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
|
||||||
allowed_users_label: 'Käyttäjätunnus',
|
allowed_users_label: 'Käyttäjätunnus',
|
||||||
allow_creation_user: 'Salli uusien käyttäjien luominen',
|
allow_creation_user: 'Salli uusien käyttäjien luominen',
|
||||||
allow_creation_user_desc: 'Salli uusien käyttäjien luominen etusivulla',
|
allow_creation_user_desc: 'Etusivulta on mahdollisuus luoda uusia käyttäjiä',
|
||||||
|
new_user_not_allowed: 'Tunnusten luonti on estetty.',
|
||||||
components: 'Komponentit',
|
components: 'Komponentit',
|
||||||
long_running_endpoints: 'Top 5 pisimpään yhteydessä ollutta päätepistettä',
|
long_running_endpoints: 'Top 5 pisimpään yhteydessä ollutta päätepistettä',
|
||||||
http_request_methods: 'HTTP-pyynnön menetelmät',
|
http_request_methods: 'HTTP-pyynnön menetelmät',
|
||||||
@@ -535,6 +571,9 @@ window.localisation.fi = {
|
|||||||
reset_wallet_keys: 'Uusi API-avaimet',
|
reset_wallet_keys: 'Uusi API-avaimet',
|
||||||
reset_wallet_keys_desc:
|
reset_wallet_keys_desc:
|
||||||
'Tämän lompakon API-avaimet uusitaan. Edelliset API-avaimet lakkaavat toimimasta ja uudet luodaan niiden tilalle..',
|
'Tämän lompakon API-avaimet uusitaan. Edelliset API-avaimet lakkaavat toimimasta ja uudet luodaan niiden tilalle..',
|
||||||
view_list: 'Näytä lompakot listana',
|
view_list: 'Näytä lompakot allekain',
|
||||||
view_column: 'Näytä lompakot riveinä'
|
view_column: 'Näytä lompakot rinnakkain',
|
||||||
|
filter_payments: 'Suodata maksuja',
|
||||||
|
filter_date: 'Suodata päiväyksellä',
|
||||||
|
websocket_example: 'Websocket esimerkki'
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
+104
-2
@@ -4,6 +4,7 @@ window.AdminPageLogic = {
|
|||||||
return {
|
return {
|
||||||
settings: {},
|
settings: {},
|
||||||
logs: [],
|
logs: [],
|
||||||
|
library_images: [],
|
||||||
serverlogEnabled: false,
|
serverlogEnabled: false,
|
||||||
lnbits_theme_options: [
|
lnbits_theme_options: [
|
||||||
'classic',
|
'classic',
|
||||||
@@ -53,8 +54,10 @@ window.AdminPageLogic = {
|
|||||||
chartReady: false,
|
chartReady: false,
|
||||||
formAddAdmin: '',
|
formAddAdmin: '',
|
||||||
formAddUser: '',
|
formAddUser: '',
|
||||||
|
hideInputToggle: true,
|
||||||
formAddExtensionsManifest: '',
|
formAddExtensionsManifest: '',
|
||||||
nostrNotificationIdentifier: '',
|
nostrNotificationIdentifier: '',
|
||||||
|
emailNotificationAddress: '',
|
||||||
formAllowedIPs: '',
|
formAllowedIPs: '',
|
||||||
formCallbackUrlRule: '',
|
formCallbackUrlRule: '',
|
||||||
formBlockedIPs: '',
|
formBlockedIPs: '',
|
||||||
@@ -138,6 +141,7 @@ window.AdminPageLogic = {
|
|||||||
async created() {
|
async created() {
|
||||||
await this.getSettings()
|
await this.getSettings()
|
||||||
await this.getAudit()
|
await this.getAudit()
|
||||||
|
await this.getUploadedImages()
|
||||||
this.balance = +'{{ balance|safe }}'
|
this.balance = +'{{ balance|safe }}'
|
||||||
const hash = window.location.hash.replace('#', '')
|
const hash = window.location.hash.replace('#', '')
|
||||||
if (hash === 'exchange_providers') {
|
if (hash === 'exchange_providers') {
|
||||||
@@ -270,6 +274,23 @@ window.AdminPageLogic = {
|
|||||||
m => m !== identifer
|
m => m !== identifer
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
addEmailNotificationAddress() {
|
||||||
|
const email = this.emailNotificationAddress.trim()
|
||||||
|
const emails = this.formData.lnbits_email_notifications_to_emails
|
||||||
|
if (email && email.length && !emails.includes(email)) {
|
||||||
|
this.formData.lnbits_email_notifications_to_emails = [...emails, email]
|
||||||
|
this.emailNotificationAddress = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeEmailNotificationAddress(email) {
|
||||||
|
const emails = this.formData.lnbits_email_notifications_to_emails
|
||||||
|
this.formData.lnbits_email_notifications_to_emails = emails.filter(
|
||||||
|
m => m !== email
|
||||||
|
)
|
||||||
|
},
|
||||||
|
hideInputsToggle() {
|
||||||
|
this.hideInputToggle = !this.hideInputToggle
|
||||||
|
},
|
||||||
async toggleServerLog() {
|
async toggleServerLog() {
|
||||||
this.serverlogEnabled = !this.serverlogEnabled
|
this.serverlogEnabled = !this.serverlogEnabled
|
||||||
if (this.serverlogEnabled) {
|
if (this.serverlogEnabled) {
|
||||||
@@ -419,7 +440,7 @@ window.AdminPageLogic = {
|
|||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/admin/api/v1/restart/')
|
.request('GET', '/admin/api/v1/restart/')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
Quasar.Notify.create({
|
this.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Success! Restarted Server',
|
message: 'Success! Restarted Server',
|
||||||
icon: null
|
icon: null
|
||||||
@@ -431,7 +452,29 @@ window.AdminPageLogic = {
|
|||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
return moment(date * 1000).fromNow()
|
return moment(date * 1000).fromNow()
|
||||||
},
|
},
|
||||||
|
sendTestEmail() {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/admin/api/v1/testemail',
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'error') {
|
||||||
|
throw new Error(response.data.message)
|
||||||
|
}
|
||||||
|
this.$q.notify({
|
||||||
|
message: 'Test email sent!',
|
||||||
|
color: 'positive'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$q.notify({
|
||||||
|
message: error.message,
|
||||||
|
color: 'negative'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
getAudit() {
|
getAudit() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
||||||
@@ -511,6 +554,65 @@ window.AdminPageLogic = {
|
|||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onImageInput(e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
this.uploadImage(file)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uploadImage(file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'POST',
|
||||||
|
'/admin/api/v1/images',
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
formData,
|
||||||
|
{headers: {'Content-Type': 'multipart/form-data'}}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Image uploaded!',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
this.getUploadedImages()
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
},
|
||||||
|
getUploadedImages() {
|
||||||
|
LNbits.api
|
||||||
|
.request('GET', '/admin/api/v1/images', this.g.user.wallets[0].inkey)
|
||||||
|
.then(response => {
|
||||||
|
this.library_images = response.data.map(image => ({
|
||||||
|
...image,
|
||||||
|
url: `${window.origin}/${image.directory}/${image.filename}`
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
},
|
||||||
|
deleteImage(filename) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this image?')
|
||||||
|
.onOk(() => {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
`/admin/api/v1/images/${filename}`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Image deleted!',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
this.getUploadedImages()
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
})
|
||||||
|
},
|
||||||
downloadBackup() {
|
downloadBackup() {
|
||||||
window.open('/admin/api/v1/backup', '_blank')
|
window.open('/admin/api/v1/backup', '_blank')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ window.LNbits = {
|
|||||||
})
|
})
|
||||||
obj.walletOptions = obj.wallets.map(obj => {
|
obj.walletOptions = obj.wallets.map(obj => {
|
||||||
return {
|
return {
|
||||||
label: [obj.name, ' - ', obj.id].join(''),
|
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
||||||
value: obj.id
|
value: obj.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -357,9 +357,10 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
prepareFilterQuery(tableConfig, props) {
|
prepareFilterQuery(tableConfig, props) {
|
||||||
|
tableConfig.filter = tableConfig.filter || {}
|
||||||
if (props) {
|
if (props) {
|
||||||
tableConfig.pagination = props.pagination
|
tableConfig.pagination = props.pagination
|
||||||
tableConfig.filter = {...tableConfig.filter, ...props.filter}
|
Object.assign(tableConfig.filter, props.filter)
|
||||||
}
|
}
|
||||||
const pagination = tableConfig.pagination
|
const pagination = tableConfig.pagination
|
||||||
tableConfig.loading = true
|
tableConfig.loading = true
|
||||||
@@ -580,9 +581,10 @@ window.windowMixin = {
|
|||||||
query: {wal: this.g.wallet.id}
|
query: {wal: this.g.wallet.id}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const url = new URL(window.location.href)
|
this.$router.replace({
|
||||||
url.searchParams.set('wal', this.g.wallet.id)
|
path: '/wallet',
|
||||||
window.history.replaceState({}, '', url.toString())
|
query: {wal: this.g.wallet.id}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formatBalance(amount) {
|
formatBalance(amount) {
|
||||||
@@ -842,6 +844,7 @@ window.windowMixin = {
|
|||||||
? this.$q.localStorage.getItem('lnbits.darkMode')
|
? this.$q.localStorage.getItem('lnbits.darkMode')
|
||||||
: true
|
: true
|
||||||
)
|
)
|
||||||
|
Chart.defaults.color = this.$q.dark.isActive ? '#fff' : '#000'
|
||||||
this.changeTheme(this.themeChoice)
|
this.changeTheme(this.themeChoice)
|
||||||
this.applyBorder()
|
this.applyBorder()
|
||||||
if (this.$q.dark.isActive) {
|
if (this.$q.dark.isActive) {
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ window.app.component('username-password', {
|
|||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.$emit('update:resetKey', this.reset_key)
|
this.$emit('update:resetKey', this.reset_key)
|
||||||
this.$emit('update:passeord_1', this.password)
|
this.$emit('update:password_1', this.password)
|
||||||
this.$emit('update:password_2', this.passwordRepeat)
|
this.$emit('update:password_2', this.passwordRepeat)
|
||||||
this.$emit('reset')
|
this.$emit('reset')
|
||||||
},
|
},
|
||||||
@@ -595,7 +595,6 @@ window.app.component('username-password', {
|
|||||||
},
|
},
|
||||||
async signInWithNostr() {
|
async signInWithNostr() {
|
||||||
try {
|
try {
|
||||||
console.log('### signInWithNostr')
|
|
||||||
const nostrToken = await this.createNostrToken()
|
const nostrToken = await this.createNostrToken()
|
||||||
if (!nostrToken) {
|
if (!nostrToken) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
'FakeWallet',
|
'FakeWallet',
|
||||||
'Fake Wallet',
|
'Fake Wallet',
|
||||||
{
|
{
|
||||||
fake_wallet_secret: 'Secret'
|
fake_wallet_secret: 'Secret',
|
||||||
|
lnbits_denomination: '"sats" or 3 Letter Custom Denomination'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ window.app.component('payment-list', {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
denomination: LNBITS_DENOMINATION,
|
denomination: LNBITS_DENOMINATION,
|
||||||
failedPaymentsToggle: false,
|
|
||||||
payments: [],
|
payments: [],
|
||||||
paymentsTable: {
|
paymentsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
@@ -39,6 +38,13 @@ window.app.component('payment-list', {
|
|||||||
loading: false
|
loading: false
|
||||||
},
|
},
|
||||||
searchDate: {from: null, to: null},
|
searchDate: {from: null, to: null},
|
||||||
|
searchStatus: {
|
||||||
|
success: true,
|
||||||
|
pending: true,
|
||||||
|
failed: false,
|
||||||
|
incoming: true,
|
||||||
|
outgoing: true
|
||||||
|
},
|
||||||
exportTagName: '',
|
exportTagName: '',
|
||||||
exportPaymentTagList: [],
|
exportPaymentTagList: [],
|
||||||
paymentsCSV: {
|
paymentsCSV: {
|
||||||
@@ -172,6 +178,26 @@ window.app.component('payment-list', {
|
|||||||
return LNbits.map.payment(obj)
|
return LNbits.map.payment(obj)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.paymentsTable.loading = false
|
||||||
|
if (g.user.admin) {
|
||||||
|
this.fetchPaymentsAsAdmin(this.currentWallet.id, params)
|
||||||
|
} else {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fetchPaymentsAsAdmin(walletId, params) {
|
||||||
|
params = (params || '') + '&wallet_id=' + walletId
|
||||||
|
return LNbits.api
|
||||||
|
.request('GET', '/api/v1/payments/all/paginated?' + params)
|
||||||
|
.then(response => {
|
||||||
|
this.paymentsTable.loading = false
|
||||||
|
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||||
|
this.payments = response.data.data.map(obj => {
|
||||||
|
return LNbits.map.payment(obj)
|
||||||
|
})
|
||||||
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
this.paymentsTable.loading = false
|
this.paymentsTable.loading = false
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
@@ -263,18 +289,40 @@ window.app.component('payment-list', {
|
|||||||
console.error(e)
|
console.error(e)
|
||||||
return `${amount} ???`
|
return `${amount} ???`
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
handleFilterChanged() {
|
||||||
|
const {success, pending, failed, incoming, outgoing} = this.searchStatus
|
||||||
|
|
||||||
|
delete this.paymentsTable.filter['status[ne]']
|
||||||
|
delete this.paymentsTable.filter['status[eq]']
|
||||||
|
if (success && pending && failed) {
|
||||||
|
// No status filter
|
||||||
|
} else if (success && pending) {
|
||||||
|
this.paymentsTable.filter['status[ne]'] = 'failed'
|
||||||
|
} else if (success && failed) {
|
||||||
|
this.paymentsTable.filter['status[ne]'] = 'pending'
|
||||||
|
} else if (failed && pending) {
|
||||||
|
this.paymentsTable.filter['status[ne]'] = 'success'
|
||||||
|
} else if (success) {
|
||||||
|
this.paymentsTable.filter['status[eq]'] = 'success'
|
||||||
|
} else if (pending) {
|
||||||
|
this.paymentsTable.filter['status[eq]'] = 'pending'
|
||||||
|
} else if (failed) {
|
||||||
|
this.paymentsTable.filter['status[eq]'] = 'failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.paymentsTable.filter['amount[ge]']
|
||||||
|
delete this.paymentsTable.filter['amount[le]']
|
||||||
|
if (incoming && outgoing) {
|
||||||
|
// do nothing
|
||||||
|
} else if (incoming) {
|
||||||
|
this.paymentsTable.filter['amount[ge]'] = 0
|
||||||
|
} else if (outgoing) {
|
||||||
|
this.paymentsTable.filter['amount[le]'] = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
failedPaymentsToggle(newVal) {
|
|
||||||
if (newVal === false) {
|
|
||||||
this.paymentsTable.filter['status[ne]'] = 'failed'
|
|
||||||
} else {
|
|
||||||
delete this.paymentsTable.filter['status[ne]']
|
|
||||||
}
|
|
||||||
this.paymentsTable.pagination.page = 1
|
|
||||||
this.fetchPayments()
|
|
||||||
},
|
|
||||||
'paymentsTable.search': {
|
'paymentsTable.search': {
|
||||||
handler() {
|
handler() {
|
||||||
const props = {}
|
const props = {}
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ window.app = Vue.createApp({
|
|||||||
description: ''
|
description: ''
|
||||||
},
|
},
|
||||||
isUserAuthorized: false,
|
isUserAuthorized: false,
|
||||||
authAction: Quasar.LocalStorage.getItem('lnbits.disclaimerShown')
|
authAction: 'login',
|
||||||
? 'login'
|
|
||||||
: 'register',
|
|
||||||
authMethod: 'username-password',
|
authMethod: 'username-password',
|
||||||
usr: '',
|
usr: '',
|
||||||
username: '',
|
username: '',
|
||||||
@@ -104,6 +102,12 @@ window.app = Vue.createApp({
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.description = SITE_DESCRIPTION
|
this.description = SITE_DESCRIPTION
|
||||||
|
this.allowedRegister = ALLOWED_REGISTER
|
||||||
|
this.authAction =
|
||||||
|
!this.allowedRegister ||
|
||||||
|
Quasar.LocalStorage.getItem('lnbits.disclaimerShown')
|
||||||
|
? 'login'
|
||||||
|
: 'register'
|
||||||
this.isUserAuthorized = !!this.$q.cookies.get('is_lnbits_user_authorized')
|
this.isUserAuthorized = !!this.$q.cookies.get('is_lnbits_user_authorized')
|
||||||
const _acccess_cookies_for_safari_refresh_do_not_delete = document.cookie
|
const _acccess_cookies_for_safari_refresh_do_not_delete = document.cookie
|
||||||
if (this.isUserAuthorized) {
|
if (this.isUserAuthorized) {
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ window.UsersPageLogic = {
|
|||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
fetchWallets(userId) {
|
fetchWallets(userId) {
|
||||||
LNbits.api
|
return LNbits.api
|
||||||
.request('GET', `/users/api/v1/user/${userId}/wallet`)
|
.request('GET', `/users/api/v1/user/${userId}/wallet`)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.wallets = res.data
|
this.wallets = res.data
|
||||||
@@ -352,9 +352,6 @@ window.UsersPageLogic = {
|
|||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
exportUsers() {
|
|
||||||
console.log('export users')
|
|
||||||
},
|
|
||||||
async showAccountPage(user_id) {
|
async showAccountPage(user_id) {
|
||||||
this.activeUser.showPassword = false
|
this.activeUser.showPassword = false
|
||||||
this.activeUser.showUserId = false
|
this.activeUser.showUserId = false
|
||||||
@@ -381,8 +378,13 @@ window.UsersPageLogic = {
|
|||||||
this.activeUser.show = false
|
this.activeUser.show = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showPayments(wallet_id) {
|
async showWalletPayments(walletId) {
|
||||||
this.paymentsWallet = this.wallets.find(wallet => wallet.id === wallet_id)
|
this.activeUser.show = false
|
||||||
|
await this.fetchWallets(this.users[0].id)
|
||||||
|
await this.showPayments(walletId)
|
||||||
|
},
|
||||||
|
showPayments(walletId) {
|
||||||
|
this.paymentsWallet = this.wallets.find(wallet => wallet.id === walletId)
|
||||||
this.paymentPage.show = true
|
this.paymentPage.show = true
|
||||||
},
|
},
|
||||||
searchUserBy(fieldName) {
|
searchUserBy(fieldName) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ window.WalletPageLogic = {
|
|||||||
return {
|
return {
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
||||||
|
websocketUrl: `${'http:' ? 'ws://' : 'wss://'}${window.location.host}/api/v1/ws`,
|
||||||
parse: {
|
parse: {
|
||||||
show: false,
|
show: false,
|
||||||
invoice: null,
|
invoice: null,
|
||||||
@@ -854,7 +855,9 @@ window.WalletPageLogic = {
|
|||||||
handleFilterChange(value = {}) {
|
handleFilterChange(value = {}) {
|
||||||
if (
|
if (
|
||||||
this.paymentsFilter['time[ge]'] !== value['time[ge]'] ||
|
this.paymentsFilter['time[ge]'] !== value['time[ge]'] ||
|
||||||
this.paymentsFilter['time[le]'] !== value['time[le]']
|
this.paymentsFilter['time[le]'] !== value['time[le]'] ||
|
||||||
|
this.paymentsFilter['amount[ge]'] !== value['amount[ge]'] ||
|
||||||
|
this.paymentsFilter['amount[le]'] !== value['amount[le]']
|
||||||
) {
|
) {
|
||||||
this.refreshCharts()
|
this.refreshCharts()
|
||||||
}
|
}
|
||||||
@@ -884,6 +887,19 @@ window.WalletPageLogic = {
|
|||||||
filterChartData(data) {
|
filterChartData(data) {
|
||||||
const timeFrom = this.paymentsFilter['time[ge]'] + 'T00:00:00'
|
const timeFrom = this.paymentsFilter['time[ge]'] + 'T00:00:00'
|
||||||
const timeTo = this.paymentsFilter['time[le]'] + 'T23:59:59'
|
const timeTo = this.paymentsFilter['time[le]'] + 'T23:59:59'
|
||||||
|
|
||||||
|
let totalBalance = 0
|
||||||
|
data = data.map(p => {
|
||||||
|
if (this.paymentsFilter['amount[ge]'] !== undefined) {
|
||||||
|
totalBalance += p.balance_in
|
||||||
|
return {...p, balance: totalBalance, balance_out: 0, count_out: 0}
|
||||||
|
}
|
||||||
|
if (this.paymentsFilter['amount[le]'] !== undefined) {
|
||||||
|
totalBalance -= p.balance_out
|
||||||
|
return {...p, balance: totalBalance, balance_in: 0, count_in: 0}
|
||||||
|
}
|
||||||
|
return {...p}
|
||||||
|
})
|
||||||
data = data.filter(p => {
|
data = data.filter(p => {
|
||||||
if (
|
if (
|
||||||
this.paymentsFilter['time[ge]'] &&
|
this.paymentsFilter['time[ge]'] &&
|
||||||
@@ -899,6 +915,7 @@ window.WalletPageLogic = {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
const labels = data.map(s =>
|
const labels = data.map(s =>
|
||||||
new Date(s.date).toLocaleString('default', {
|
new Date(s.date).toLocaleString('default', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@@ -1103,7 +1120,6 @@ window.WalletPageLogic = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'g.updatePayments'(newVal, oldVal) {
|
'g.updatePayments'(newVal, oldVal) {
|
||||||
console.log('updatePayments changed:', {newVal, oldVal})
|
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
if (this.receive.paymentHash === this.g.updatePaymentsHash) {
|
if (this.receive.paymentHash === this.g.updatePaymentsHash) {
|
||||||
this.receive.show = false
|
this.receive.show = false
|
||||||
@@ -1129,8 +1145,12 @@ window.WalletPageLogic = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'g.wallet': {
|
'g.wallet': {
|
||||||
handler(newWallet) {
|
handler() {
|
||||||
this.createdTasks()
|
try {
|
||||||
|
this.createdTasks()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Chart creation failed: ${error}`)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ $themes: (
|
|||||||
marginal-text: #fff
|
marginal-text: #fff
|
||||||
),
|
),
|
||||||
'bitcoin': (
|
'bitcoin': (
|
||||||
primary: #ff9853,
|
primary: #ea611d,
|
||||||
secondary: #ff7353,
|
secondary: #e56f35,
|
||||||
dark: #2d293b,
|
dark: #2d293b,
|
||||||
info: #333646,
|
info: #333646,
|
||||||
marginal-bg: #2d293b,
|
marginal-bg: #000000,
|
||||||
marginal-text: #fff
|
marginal-text: #fff
|
||||||
),
|
),
|
||||||
'freedom': (
|
'freedom': (
|
||||||
|
|||||||
Vendored
+45
-85
@@ -1,4 +1,4 @@
|
|||||||
// Axios v1.7.7 Copyright (c) 2024 Matt Zabriskie and contributors
|
/*! Axios v1.8.2 Copyright (c) 2025 Matt Zabriskie and contributors */
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
@@ -1266,23 +1266,6 @@
|
|||||||
var toFiniteNumber = function toFiniteNumber(value, defaultValue) {
|
var toFiniteNumber = function toFiniteNumber(value, defaultValue) {
|
||||||
return value != null && Number.isFinite(value = +value) ? value : defaultValue;
|
return value != null && Number.isFinite(value = +value) ? value : defaultValue;
|
||||||
};
|
};
|
||||||
var ALPHA = 'abcdefghijklmnopqrstuvwxyz';
|
|
||||||
var DIGIT = '0123456789';
|
|
||||||
var ALPHABET = {
|
|
||||||
DIGIT: DIGIT,
|
|
||||||
ALPHA: ALPHA,
|
|
||||||
ALPHA_DIGIT: ALPHA + ALPHA.toUpperCase() + DIGIT
|
|
||||||
};
|
|
||||||
var generateString = function generateString() {
|
|
||||||
var size = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 16;
|
|
||||||
var alphabet = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ALPHABET.ALPHA_DIGIT;
|
|
||||||
var str = '';
|
|
||||||
var length = alphabet.length;
|
|
||||||
while (size--) {
|
|
||||||
str += alphabet[Math.random() * length | 0];
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the thing is a FormData object, return true, otherwise return false.
|
* If the thing is a FormData object, return true, otherwise return false.
|
||||||
@@ -1399,8 +1382,6 @@
|
|||||||
findKey: findKey,
|
findKey: findKey,
|
||||||
global: _global,
|
global: _global,
|
||||||
isContextDefined: isContextDefined,
|
isContextDefined: isContextDefined,
|
||||||
ALPHABET: ALPHABET,
|
|
||||||
generateString: generateString,
|
|
||||||
isSpecCompliantForm: isSpecCompliantForm,
|
isSpecCompliantForm: isSpecCompliantForm,
|
||||||
toJSONObject: toJSONObject,
|
toJSONObject: toJSONObject,
|
||||||
isAsyncFn: isAsyncFn,
|
isAsyncFn: isAsyncFn,
|
||||||
@@ -1735,7 +1716,7 @@
|
|||||||
*
|
*
|
||||||
* @param {string} url The base of the url (e.g., http://www.google.com)
|
* @param {string} url The base of the url (e.g., http://www.google.com)
|
||||||
* @param {object} [params] The params to be appended
|
* @param {object} [params] The params to be appended
|
||||||
* @param {?object} options
|
* @param {?(object|Function)} options
|
||||||
*
|
*
|
||||||
* @returns {string} The formatted url
|
* @returns {string} The formatted url
|
||||||
*/
|
*/
|
||||||
@@ -1745,6 +1726,11 @@
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
var _encode = options && options.encode || encode;
|
var _encode = options && options.encode || encode;
|
||||||
|
if (utils$1.isFunction(options)) {
|
||||||
|
options = {
|
||||||
|
serialize: options
|
||||||
|
};
|
||||||
|
}
|
||||||
var serializeFn = options && options.serialize;
|
var serializeFn = options && options.serialize;
|
||||||
var serializedParams;
|
var serializedParams;
|
||||||
if (serializeFn) {
|
if (serializeFn) {
|
||||||
@@ -2642,60 +2628,14 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
var isURLSameOrigin = platform.hasStandardBrowserEnv ?
|
var isURLSameOrigin = platform.hasStandardBrowserEnv ? function (origin, isMSIE) {
|
||||||
// Standard browser envs have full support of the APIs needed to test
|
return function (url) {
|
||||||
// whether the request URL is of the same origin as current location.
|
url = new URL(url, platform.origin);
|
||||||
function standardBrowserEnv() {
|
return origin.protocol === url.protocol && origin.host === url.host && (isMSIE || origin.port === url.port);
|
||||||
var msie = platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent);
|
|
||||||
var urlParsingNode = document.createElement('a');
|
|
||||||
var originURL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a URL to discover its components
|
|
||||||
*
|
|
||||||
* @param {String} url The URL to be parsed
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
function resolveURL(url) {
|
|
||||||
var href = url;
|
|
||||||
if (msie) {
|
|
||||||
// IE needs attribute set twice to normalize properties
|
|
||||||
urlParsingNode.setAttribute('href', href);
|
|
||||||
href = urlParsingNode.href;
|
|
||||||
}
|
|
||||||
urlParsingNode.setAttribute('href', href);
|
|
||||||
|
|
||||||
// urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
|
|
||||||
return {
|
|
||||||
href: urlParsingNode.href,
|
|
||||||
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
|
|
||||||
host: urlParsingNode.host,
|
|
||||||
search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
|
|
||||||
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
|
|
||||||
hostname: urlParsingNode.hostname,
|
|
||||||
port: urlParsingNode.port,
|
|
||||||
pathname: urlParsingNode.pathname.charAt(0) === '/' ? urlParsingNode.pathname : '/' + urlParsingNode.pathname
|
|
||||||
};
|
|
||||||
}
|
|
||||||
originURL = resolveURL(window.location.href);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a URL shares the same origin as the current location
|
|
||||||
*
|
|
||||||
* @param {String} requestURL The URL to test
|
|
||||||
* @returns {boolean} True if URL shares the same origin, otherwise false
|
|
||||||
*/
|
|
||||||
return function isURLSameOrigin(requestURL) {
|
|
||||||
var parsed = utils$1.isString(requestURL) ? resolveURL(requestURL) : requestURL;
|
|
||||||
return parsed.protocol === originURL.protocol && parsed.host === originURL.host;
|
|
||||||
};
|
};
|
||||||
}() :
|
}(new URL(platform.origin), platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent)) : function () {
|
||||||
// Non standard browser envs (web workers, react-native) lack needed support.
|
return true;
|
||||||
function nonStandardBrowserEnv() {
|
};
|
||||||
return function isURLSameOrigin() {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}();
|
|
||||||
|
|
||||||
var cookies = platform.hasStandardBrowserEnv ?
|
var cookies = platform.hasStandardBrowserEnv ?
|
||||||
// Standard browser envs support document.cookie
|
// Standard browser envs support document.cookie
|
||||||
@@ -2761,8 +2701,9 @@
|
|||||||
*
|
*
|
||||||
* @returns {string} The combined full path
|
* @returns {string} The combined full path
|
||||||
*/
|
*/
|
||||||
function buildFullPath(baseURL, requestedURL) {
|
function buildFullPath(baseURL, requestedURL, allowAbsoluteUrls) {
|
||||||
if (baseURL && !isAbsoluteURL(requestedURL)) {
|
var isRelativeUrl = !isAbsoluteURL(requestedURL);
|
||||||
|
if (baseURL && isRelativeUrl || allowAbsoluteUrls == false) {
|
||||||
return combineURLs(baseURL, requestedURL);
|
return combineURLs(baseURL, requestedURL);
|
||||||
}
|
}
|
||||||
return requestedURL;
|
return requestedURL;
|
||||||
@@ -2785,7 +2726,7 @@
|
|||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
config2 = config2 || {};
|
config2 = config2 || {};
|
||||||
var config = {};
|
var config = {};
|
||||||
function getMergedValue(target, source, caseless) {
|
function getMergedValue(target, source, prop, caseless) {
|
||||||
if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
|
if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
|
||||||
return utils$1.merge.call({
|
return utils$1.merge.call({
|
||||||
caseless: caseless
|
caseless: caseless
|
||||||
@@ -2799,11 +2740,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
function mergeDeepProperties(a, b, caseless) {
|
function mergeDeepProperties(a, b, prop, caseless) {
|
||||||
if (!utils$1.isUndefined(b)) {
|
if (!utils$1.isUndefined(b)) {
|
||||||
return getMergedValue(a, b, caseless);
|
return getMergedValue(a, b, prop, caseless);
|
||||||
} else if (!utils$1.isUndefined(a)) {
|
} else if (!utils$1.isUndefined(a)) {
|
||||||
return getMergedValue(undefined, a, caseless);
|
return getMergedValue(undefined, a, prop, caseless);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2860,8 +2801,8 @@
|
|||||||
socketPath: defaultToConfig2,
|
socketPath: defaultToConfig2,
|
||||||
responseEncoding: defaultToConfig2,
|
responseEncoding: defaultToConfig2,
|
||||||
validateStatus: mergeDirectKeys,
|
validateStatus: mergeDirectKeys,
|
||||||
headers: function headers(a, b) {
|
headers: function headers(a, b, prop) {
|
||||||
return mergeDeepProperties(headersToObject(a), headersToObject(b), true);
|
return mergeDeepProperties(headersToObject(a), headersToObject(b), prop, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
utils$1.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
|
utils$1.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
|
||||||
@@ -3717,7 +3658,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var VERSION = "1.7.7";
|
var VERSION = "1.8.2";
|
||||||
|
|
||||||
var validators$1 = {};
|
var validators$1 = {};
|
||||||
|
|
||||||
@@ -3756,6 +3697,13 @@
|
|||||||
return validator ? validator(value, opt, opts) : true;
|
return validator ? validator(value, opt, opts) : true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
validators$1.spelling = function spelling(correctSpelling) {
|
||||||
|
return function (value, opt) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("".concat(opt, " is likely a misspelling of ").concat(correctSpelling));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert object's properties type
|
* Assert object's properties type
|
||||||
@@ -3838,7 +3786,8 @@
|
|||||||
_context.prev = 6;
|
_context.prev = 6;
|
||||||
_context.t0 = _context["catch"](0);
|
_context.t0 = _context["catch"](0);
|
||||||
if (_context.t0 instanceof Error) {
|
if (_context.t0 instanceof Error) {
|
||||||
Error.captureStackTrace ? Error.captureStackTrace(dummy = {}) : dummy = new Error();
|
dummy = {};
|
||||||
|
Error.captureStackTrace ? Error.captureStackTrace(dummy) : dummy = new Error();
|
||||||
|
|
||||||
// slice off the Error: ... line
|
// slice off the Error: ... line
|
||||||
stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
|
stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
|
||||||
@@ -3901,6 +3850,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set config.allowAbsoluteUrls
|
||||||
|
if (config.allowAbsoluteUrls !== undefined) ; else if (this.defaults.allowAbsoluteUrls !== undefined) {
|
||||||
|
config.allowAbsoluteUrls = this.defaults.allowAbsoluteUrls;
|
||||||
|
} else {
|
||||||
|
config.allowAbsoluteUrls = true;
|
||||||
|
}
|
||||||
|
validator.assertOptions(config, {
|
||||||
|
baseUrl: validators.spelling('baseURL'),
|
||||||
|
withXsrfToken: validators.spelling('withXSRFToken')
|
||||||
|
}, true);
|
||||||
|
|
||||||
// Set config.method
|
// Set config.method
|
||||||
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
|
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
|
||||||
|
|
||||||
@@ -3968,7 +3928,7 @@
|
|||||||
key: "getUri",
|
key: "getUri",
|
||||||
value: function getUri(config) {
|
value: function getUri(config) {
|
||||||
config = mergeConfig(this.defaults, config);
|
config = mergeConfig(this.defaults, config);
|
||||||
var fullPath = buildFullPath(config.baseURL, config.url);
|
var fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
|
||||||
return buildURL(fullPath, config.params, config.paramsSerializer);
|
return buildURL(fullPath, config.params, config.paramsSerializer);
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
+3
-3
File diff suppressed because one or more lines are too long
@@ -1,9 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
from http import HTTPStatus
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
Coroutine,
|
Coroutine,
|
||||||
@@ -13,11 +11,8 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from py_vapid import Vapid
|
|
||||||
from pywebpush import WebPushException, webpush
|
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
from lnbits.core.crud import (
|
||||||
delete_webpush_subscriptions,
|
|
||||||
get_payments,
|
get_payments,
|
||||||
get_standalone_payment,
|
get_standalone_payment,
|
||||||
update_payment,
|
update_payment,
|
||||||
@@ -216,28 +211,3 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
|
|||||||
for name, send_chan in invoice_listeners.items():
|
for name, send_chan in invoice_listeners.items():
|
||||||
logger.trace(f"invoice listeners: sending to `{name}`")
|
logger.trace(f"invoice listeners: sending to `{name}`")
|
||||||
await send_chan.put(payment)
|
await send_chan.put(payment)
|
||||||
|
|
||||||
|
|
||||||
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}"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -89,11 +89,11 @@
|
|||||||
>
|
>
|
||||||
{% if LNBITS_SERVICE_FEE_MAX > 0 %}
|
{% if LNBITS_SERVICE_FEE_MAX > 0 %}
|
||||||
<span
|
<span
|
||||||
v-text='$t("service_fee_max", { amount: "{{ LNBITS_SERVICE_FEE }}", max: "{{ LNBITS_SERVICE_FEE_MAX }}"})'
|
v-text='$t("service_fee_max_badge", { amount: "{{ LNBITS_SERVICE_FEE }}", max: "{{ LNBITS_SERVICE_FEE_MAX }}", denom: "{{ LNBITS_DENOMINATION }}"})'
|
||||||
></span>
|
></span>
|
||||||
{%else%}
|
{%else%}
|
||||||
<span
|
<span
|
||||||
v-text='$t("service_fee", { amount: "{{ LNBITS_SERVICE_FEE }}" })'
|
v-text='$t("service_fee_badge", { amount: "{{ LNBITS_SERVICE_FEE}}"})'
|
||||||
></span>
|
></span>
|
||||||
{%endif%}
|
{%endif%}
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
@@ -343,6 +343,7 @@
|
|||||||
{% endblock %} {% block footer %}
|
{% endblock %} {% block footer %}
|
||||||
|
|
||||||
<q-footer
|
<q-footer
|
||||||
|
v-if="'{{ SITE_TITLE }}' == 'LNbits' && '{{ LNBITS_SHOW_HOME_PAGE_ELEMENTS }}' == 'True'"
|
||||||
class="bg-transparent q-px-lg q-py-md"
|
class="bg-transparent q-px-lg q-py-md"
|
||||||
:class="{'text-dark': !$q.dark.isActive}"
|
:class="{'text-dark': !$q.dark.isActive}"
|
||||||
>
|
>
|
||||||
|
|||||||
+122
-62
@@ -320,7 +320,26 @@
|
|||||||
<q-item v-if="payment.preimage">
|
<q-item v-if="payment.preimage">
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-text="$t('payment_proof')"></q-item-label>
|
<q-item-label v-text="$t('payment_proof')"></q-item-label>
|
||||||
<q-item-label caption v-text="payment.preimage"></q-item-label>
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="
|
||||||
|
`${payment.preimage.slice(0, 12)}...${payment.preimage.slice(-12)}`
|
||||||
|
"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-item-label>
|
||||||
|
<q-icon
|
||||||
|
name="content_copy"
|
||||||
|
@click="copyText(payment.preimage)"
|
||||||
|
size="1em"
|
||||||
|
color="grey"
|
||||||
|
class="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</q-item-label>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="payment.preimage"></span>
|
||||||
|
</q-tooltip>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
@@ -419,7 +438,6 @@
|
|||||||
dense
|
dense
|
||||||
use-input
|
use-input
|
||||||
use-chips
|
use-chips
|
||||||
multiple
|
|
||||||
hide-dropdown-icon
|
hide-dropdown-icon
|
||||||
></q-select>
|
></q-select>
|
||||||
<div v-else-if="o.type === 'bool'">
|
<div v-else-if="o.type === 'bool'">
|
||||||
@@ -639,16 +657,100 @@
|
|||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="gt-sm col-auto">
|
<div class="gt-sm col-auto">
|
||||||
|
<q-btn icon="event" flat color="grey">
|
||||||
|
<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-badge>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="$t('filter_date')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn color="grey" icon="filter_alt" flat>
|
||||||
|
<q-menu>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="searchStatus.success"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Success Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="searchStatus.pending"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Pending Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="searchStatus.failed"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Failed Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="searchStatus.incoming"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Incoming Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="searchStatus.outgoing"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Outgoing Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
</q-menu>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="$t('filter_payments')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
<q-btn-dropdown
|
<q-btn-dropdown
|
||||||
|
dense
|
||||||
outline
|
outline
|
||||||
persistent
|
persistent
|
||||||
dense
|
icon="archive"
|
||||||
|
split
|
||||||
class="q-mr-sm"
|
class="q-mr-sm"
|
||||||
color="grey"
|
color="grey"
|
||||||
label="Export"
|
|
||||||
split
|
|
||||||
@click="exportCSV(false)"
|
@click="exportCSV(false)"
|
||||||
>
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="$t('export_csv')"></span>
|
||||||
|
</q-tooltip>
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
@@ -688,59 +790,12 @@
|
|||||||
outline
|
outline
|
||||||
color="grey"
|
color="grey"
|
||||||
@click="exportCSV(true)"
|
@click="exportCSV(true)"
|
||||||
label="Export to CSV with details"
|
:label="$t('export_csv_details')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
<q-btn icon="event" outline flat color="grey">
|
|
||||||
<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-checkbox
|
|
||||||
v-model="failedPaymentsToggle"
|
|
||||||
checked-icon="warning"
|
|
||||||
unchecked-icon="warning_off"
|
|
||||||
:color="failedPaymentsToggle ? 'yellow' : 'grey'"
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="`Include failed payments`"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-my-md"></div>
|
<div class="row q-my-md"></div>
|
||||||
@@ -1021,7 +1076,16 @@
|
|||||||
|
|
||||||
<template id="lnbits-funding-sources">
|
<template id="lnbits-funding-sources">
|
||||||
<div class="funding-sources">
|
<div class="funding-sources">
|
||||||
<h6 class="q-mt-xl q-mb-md">Funding Sources</h6>
|
<h6 class="q-my-none q-mb-sm">
|
||||||
|
<span v-text="$t('funding_sources')"></span>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click="this.hideInput = !this.hideInput"
|
||||||
|
:icon="this.hideInput ? 'visibility_off' : 'visibility'"
|
||||||
|
></q-btn>
|
||||||
|
</h6>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<p>Active Funding<small> (Requires server restart)</small></p>
|
<p>Active Funding<small> (Requires server restart)</small></p>
|
||||||
@@ -1059,13 +1123,6 @@
|
|||||||
:label="prop.label"
|
:label="prop.label"
|
||||||
:hint="prop.hint"
|
:hint="prop.hint"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
:name="hideInput ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="this.hideInput = !this.hideInput"
|
|
||||||
></q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1223,7 +1280,7 @@
|
|||||||
</q-form>
|
</q-form>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<!-- REGISTER -->
|
<!-- REGISTER -->
|
||||||
<q-card-section v-if="authAction === 'register'">
|
<q-card-section v-if="allowed_new_users && authAction === 'register'">
|
||||||
<q-form @submit="register" class="q-gutter-sm">
|
<q-form @submit="register" class="q-gutter-sm">
|
||||||
<q-input
|
<q-input
|
||||||
dense
|
dense
|
||||||
@@ -1266,6 +1323,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
<q-card-section v-else-if="!allowed_new_users && authAction === 'register'">
|
||||||
|
<h5 class="text-center" v-text="$t('new_user_not_allowed')"></h5>
|
||||||
|
</q-card-section>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<!-- RESET -->
|
<!-- RESET -->
|
||||||
<q-card-section v-if="authAction === 'reset'">
|
<q-card-section v-if="authAction === 'reset'">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any, NamedTuple, Optional
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class Cache:
|
|||||||
self.interval = interval
|
self.interval = interval
|
||||||
self._values: dict[Any, Cached] = {}
|
self._values: dict[Any, Cached] = {}
|
||||||
|
|
||||||
def get(self, key: str, default=None) -> Optional[Any]:
|
def get(self, key: str, default=None) -> Any | None:
|
||||||
cached = self._values.get(key)
|
cached = self._values.get(key)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
if cached.expiry > time():
|
if cached.expiry > time():
|
||||||
@@ -35,7 +35,7 @@ class Cache:
|
|||||||
def set(self, key: str, value: Any, expiry: float = 10):
|
def set(self, key: str, value: Any, expiry: float = 10):
|
||||||
self._values[key] = Cached(value, time() + expiry)
|
self._values[key] = Cached(value, time() + expiry)
|
||||||
|
|
||||||
def pop(self, key: str, default=None) -> Optional[Any]:
|
def pop(self, key: str, default=None) -> Any | None:
|
||||||
cached = self._values.pop(key, None)
|
cached = self._values.pop(key, None)
|
||||||
if cached and cached.expiry > time():
|
if cached and cached.expiry > time():
|
||||||
return cached.value
|
return cached.value
|
||||||
|
|||||||
+11
-5
@@ -63,10 +63,15 @@ class AESCipher:
|
|||||||
final_key += key
|
final_key += key
|
||||||
return final_key[:output]
|
return final_key[:output]
|
||||||
|
|
||||||
def decrypt(self, encrypted: str) -> str:
|
def decrypt(self, encrypted: str, urlsafe: bool = False) -> str:
|
||||||
"""Decrypts a string using AES-256-CBC."""
|
"""Decrypts a string using AES-256-CBC."""
|
||||||
passphrase = self.passphrase
|
passphrase = self.passphrase
|
||||||
encrypted_bytes = base64.b64decode(encrypted)
|
|
||||||
|
if urlsafe:
|
||||||
|
encrypted_bytes = base64.urlsafe_b64decode(encrypted)
|
||||||
|
else:
|
||||||
|
encrypted_bytes = base64.b64decode(encrypted)
|
||||||
|
|
||||||
assert encrypted_bytes[0:8] == b"Salted__"
|
assert encrypted_bytes[0:8] == b"Salted__"
|
||||||
salt = encrypted_bytes[8:16]
|
salt = encrypted_bytes[8:16]
|
||||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||||
@@ -78,13 +83,14 @@ class AESCipher:
|
|||||||
except UnicodeDecodeError as exc:
|
except UnicodeDecodeError as exc:
|
||||||
raise ValueError("Wrong passphrase") from exc
|
raise ValueError("Wrong passphrase") from exc
|
||||||
|
|
||||||
def encrypt(self, message: bytes) -> str:
|
def encrypt(self, message: bytes, urlsafe: bool = False) -> str:
|
||||||
passphrase = self.passphrase
|
passphrase = self.passphrase
|
||||||
salt = Random.new().read(8)
|
salt = Random.new().read(8)
|
||||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||||
key = key_iv[:32]
|
key = key_iv[:32]
|
||||||
iv = key_iv[32:]
|
iv = key_iv[32:]
|
||||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||||
return base64.b64encode(
|
encoded = b"Salted__" + salt + aes.encrypt(self.pad(message))
|
||||||
b"Salted__" + salt + aes.encrypt(self.pad(message))
|
return (
|
||||||
|
base64.urlsafe_b64encode(encoded) if urlsafe else base64.b64encode(encoded)
|
||||||
).decode()
|
).decode()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -176,7 +177,7 @@ currencies = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def allowed_currencies():
|
def allowed_currencies() -> list[str]:
|
||||||
if len(settings.lnbits_allowed_currencies) > 0:
|
if len(settings.lnbits_allowed_currencies) > 0:
|
||||||
return [
|
return [
|
||||||
item
|
item
|
||||||
@@ -187,6 +188,9 @@ def allowed_currencies():
|
|||||||
|
|
||||||
|
|
||||||
async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
||||||
|
if currency.upper() not in allowed_currencies():
|
||||||
|
raise ValueError(f"Currency '{currency}' not allowed.")
|
||||||
|
|
||||||
def replacements(ticker: str):
|
def replacements(ticker: str):
|
||||||
return {
|
return {
|
||||||
"FROM": "BTC",
|
"FROM": "BTC",
|
||||||
@@ -209,7 +213,7 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
|||||||
try:
|
try:
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
r = await client.get(url, timeout=0.5)
|
r = await client.get(url, timeout=3)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
if not provider.path:
|
if not provider.path:
|
||||||
@@ -227,11 +231,10 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# OK to be in squence: fetch_price times out after 0.5 seconds
|
calls = [
|
||||||
results = [
|
fetch_price(provider) for provider in settings.lnbits_exchange_rate_providers
|
||||||
await fetch_price(provider)
|
|
||||||
for provider in settings.lnbits_exchange_rate_providers
|
|
||||||
]
|
]
|
||||||
|
results = await asyncio.gather(*calls)
|
||||||
|
|
||||||
return [r for r in results if r is not None]
|
return [r for r in results if r is not None]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from lnbits.nodes import set_node_class
|
from lnbits.nodes import set_node_class
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -33,7 +32,7 @@ from .void import VoidWallet
|
|||||||
from .zbd import ZBDWallet
|
from .zbd import ZBDWallet
|
||||||
|
|
||||||
|
|
||||||
def set_funding_source(class_name: Optional[str] = None):
|
def set_funding_source(class_name: str | None = None):
|
||||||
backend_wallet_class = class_name or settings.lnbits_backend_wallet_class
|
backend_wallet_class = class_name or settings.lnbits_backend_wallet_class
|
||||||
funding_source_constructor = getattr(wallets_module, backend_wallet_class)
|
funding_source_constructor = getattr(wallets_module, backend_wallet_class)
|
||||||
global funding_source
|
global funding_source
|
||||||
|
|||||||
+17
-17
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TYPE_CHECKING, AsyncGenerator, Coroutine, NamedTuple, Optional
|
from typing import TYPE_CHECKING, AsyncGenerator, Coroutine, NamedTuple
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -11,15 +11,15 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class StatusResponse(NamedTuple):
|
class StatusResponse(NamedTuple):
|
||||||
error_message: Optional[str]
|
error_message: str | None
|
||||||
balance_msat: int
|
balance_msat: int
|
||||||
|
|
||||||
|
|
||||||
class InvoiceResponse(NamedTuple):
|
class InvoiceResponse(NamedTuple):
|
||||||
ok: bool
|
ok: bool
|
||||||
checking_id: Optional[str] = None # payment_hash, rpc_id
|
checking_id: str | None = None # payment_hash, rpc_id
|
||||||
payment_request: Optional[str] = None
|
payment_request: str | None = None
|
||||||
error_message: Optional[str] = None
|
error_message: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def success(self) -> bool:
|
def success(self) -> bool:
|
||||||
@@ -36,11 +36,11 @@ class InvoiceResponse(NamedTuple):
|
|||||||
|
|
||||||
class PaymentResponse(NamedTuple):
|
class PaymentResponse(NamedTuple):
|
||||||
# when ok is None it means we don't know if this succeeded
|
# when ok is None it means we don't know if this succeeded
|
||||||
ok: Optional[bool] = None
|
ok: bool | None = None
|
||||||
checking_id: Optional[str] = None # payment_hash, rcp_id
|
checking_id: str | None = None # payment_hash, rcp_id
|
||||||
fee_msat: Optional[int] = None
|
fee_msat: int | None = None
|
||||||
preimage: Optional[str] = None
|
preimage: str | None = None
|
||||||
error_message: Optional[str] = None
|
error_message: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def success(self) -> bool:
|
def success(self) -> bool:
|
||||||
@@ -56,9 +56,9 @@ class PaymentResponse(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class PaymentStatus(NamedTuple):
|
class PaymentStatus(NamedTuple):
|
||||||
paid: Optional[bool] = None
|
paid: bool | None = None
|
||||||
fee_msat: Optional[int] = None
|
fee_msat: int | None = None
|
||||||
preimage: Optional[str] = None
|
preimage: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def success(self) -> bool:
|
def success(self) -> bool:
|
||||||
@@ -94,7 +94,7 @@ class PaymentPendingStatus(PaymentStatus):
|
|||||||
|
|
||||||
class Wallet(ABC):
|
class Wallet(ABC):
|
||||||
|
|
||||||
__node_cls__: Optional[type[Node]] = None
|
__node_cls__: type[Node] | None = None
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.pending_invoices: list[str] = []
|
self.pending_invoices: list[str] = []
|
||||||
@@ -111,9 +111,9 @@ class Wallet(ABC):
|
|||||||
def create_invoice(
|
def create_invoice(
|
||||||
self,
|
self,
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: str | None = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: bytes | None = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: bytes | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Coroutine[None, None, InvoiceResponse]:
|
) -> Coroutine[None, None, InvoiceResponse]:
|
||||||
pass
|
pass
|
||||||
|
|||||||
+10
-6
@@ -2,7 +2,7 @@ import asyncio
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from os import urandom
|
from os import urandom
|
||||||
from typing import AsyncGenerator, Dict, Optional, Set
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
from bolt11 import (
|
from bolt11 import (
|
||||||
Bolt11,
|
Bolt11,
|
||||||
@@ -34,8 +34,8 @@ class FakeWallet(Wallet):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
self.payment_secrets: Dict[str, str] = {}
|
self.payment_secrets: dict[str, str] = {}
|
||||||
self.paid_invoices: Set[str] = set()
|
self.paid_invoices: set[str] = set()
|
||||||
self.secret = settings.fake_wallet_secret
|
self.secret = settings.fake_wallet_secret
|
||||||
self.privkey = fake_privkey(self.secret)
|
self.privkey = fake_privkey(self.secret)
|
||||||
|
|
||||||
@@ -80,11 +80,12 @@ class FakeWallet(Wallet):
|
|||||||
secret = urandom(32).hex()
|
secret = urandom(32).hex()
|
||||||
tags.add(TagChar.payment_secret, secret)
|
tags.add(TagChar.payment_secret, secret)
|
||||||
|
|
||||||
payment_hash = sha256(secret.encode()).hexdigest()
|
preimage = urandom(32)
|
||||||
|
payment_hash = sha256(preimage).hexdigest()
|
||||||
|
|
||||||
tags.add(TagChar.payment_hash, payment_hash)
|
tags.add(TagChar.payment_hash, payment_hash)
|
||||||
|
|
||||||
self.payment_secrets[payment_hash] = secret
|
self.payment_secrets[payment_hash] = preimage.hex()
|
||||||
|
|
||||||
bolt11 = Bolt11(
|
bolt11 = Bolt11(
|
||||||
currency="bc",
|
currency="bc",
|
||||||
@@ -96,7 +97,10 @@ class FakeWallet(Wallet):
|
|||||||
payment_request = encode(bolt11, self.privkey)
|
payment_request = encode(bolt11, self.privkey)
|
||||||
|
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
ok=True, checking_id=payment_hash, payment_request=payment_request
|
ok=True,
|
||||||
|
checking_id=payment_hash,
|
||||||
|
payment_request=payment_request,
|
||||||
|
# preimage=preimage.hex(),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
|
||||||
|
|||||||
@@ -87,13 +87,15 @@ class LNbitsWallet(Wallet):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if r.is_error or "bolt11" not in data:
|
# Backwards compatibility for pre-v1 which used the key "payment_request"
|
||||||
|
payment_str = data.get("bolt11") or data.get("payment_request")
|
||||||
|
if r.is_error or not payment_str:
|
||||||
error_message = data["detail"] if "detail" in data else r.text
|
error_message = data["detail"] if "detail" in data else r.text
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Server error: '{error_message}'"
|
False, None, None, f"Server error: '{error_message}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return InvoiceResponse(True, data["checking_id"], data["bolt11"], None)
|
return InvoiceResponse(True, data["checking_id"], payment_str, None)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'invalid json response'"
|
False, None, None, "Server error: 'invalid json response'"
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class PhoenixdWallet(Wallet):
|
|||||||
return PaymentResponse(None, None, None, None, error_message)
|
return PaymentResponse(None, None, None, None, error_message)
|
||||||
|
|
||||||
checking_id = data["paymentHash"]
|
checking_id = data["paymentHash"]
|
||||||
fee_msat = -int(data["routingFeeSat"])
|
fee_msat = -int(data["routingFeeSat"]) * 1000
|
||||||
preimage = data["paymentPreimage"]
|
preimage = data["paymentPreimage"]
|
||||||
|
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||||
|
"spaces": 2,
|
||||||
|
"generator-cli": {
|
||||||
|
"version": "7.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1637
-26
File diff suppressed because it is too large
Load Diff
+6
-4
@@ -10,9 +10,11 @@
|
|||||||
"vendor_minify_css": "./node_modules/.bin/minify ./lnbits/static/bundle.css > ./lnbits/static/bundle.min.css",
|
"vendor_minify_css": "./node_modules/.bin/minify ./lnbits/static/bundle.css > ./lnbits/static/bundle.min.css",
|
||||||
"vendor_minify_js": "./node_modules/.bin/minify ./lnbits/static/bundle.js > ./lnbits/static/bundle.min.js",
|
"vendor_minify_js": "./node_modules/.bin/minify ./lnbits/static/bundle.js > ./lnbits/static/bundle.min.js",
|
||||||
"vendor_minify_components": "./node_modules/.bin/minify ./lnbits/static/bundle-components.js > ./lnbits/static/bundle-components.min.js",
|
"vendor_minify_components": "./node_modules/.bin/minify ./lnbits/static/bundle-components.js > ./lnbits/static/bundle-components.min.js",
|
||||||
"bundle": "npm run sass && npm run vendor_copy && npm run vendor_json && npm run vendor_bundle_css && npm run vendor_bundle_js && npm run vendor_bundle_components && npm run vendor_minify_css && npm run vendor_minify_js && npm run vendor_minify_components"
|
"bundle": "npm run sass && npm run vendor_copy && npm run vendor_json && npm run vendor_bundle_css && npm run vendor_bundle_js && npm run vendor_bundle_components && npm run vendor_minify_css && npm run vendor_minify_js && npm run vendor_minify_components",
|
||||||
|
"generate": "openapi-generator-cli generate -i openapi.json -g typescript-fetch -o ./generated"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@openapitools/openapi-generator-cli": "^2.18.4",
|
||||||
"concat": "^1.0.3",
|
"concat": "^1.0.3",
|
||||||
"minify": "^9.2.0",
|
"minify": "^9.2.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
@@ -20,16 +22,16 @@
|
|||||||
"sass": "^1.78.0"
|
"sass": "^1.78.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.8.2",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"nostr-tools": "^2.7.2",
|
||||||
"qrcode.vue": "^3.4.1",
|
"qrcode.vue": "^3.4.1",
|
||||||
"quasar": "2.17.0",
|
"quasar": "2.17.0",
|
||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"nostr-tools": "^2.7.2",
|
|
||||||
"underscore": "^1.13.7",
|
"underscore": "^1.13.7",
|
||||||
"vue": "3.5.8",
|
"vue": "3.5.8",
|
||||||
"vue-i18n": "^10.0.5",
|
"vue-i18n": "^10.0.6",
|
||||||
"vue-qrcode-reader": "^5.5.10",
|
"vue-qrcode-reader": "^5.5.10",
|
||||||
"vue-router": "4.4.5",
|
"vue-router": "4.4.5",
|
||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
|
|||||||
Generated
+13
-21
@@ -1083,6 +1083,17 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1
|
|||||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
|
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
|
||||||
typing = ["typing-extensions (>=4.8)"]
|
typing = ["typing-extensions (>=4.8)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetype"
|
||||||
|
version = "1.2.0"
|
||||||
|
description = "Infer file type and MIME type of any file/buffer. No external dependencies."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25"},
|
||||||
|
{file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.0.3"
|
version = "3.0.3"
|
||||||
@@ -1395,9 +1406,6 @@ files = [
|
|||||||
{file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"},
|
{file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||||
testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"]
|
testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"]
|
||||||
@@ -2916,7 +2924,6 @@ files = [
|
|||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
anyio = ">=3.4.0,<5"
|
anyio = ">=3.4.0,<5"
|
||||||
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
||||||
@@ -3400,26 +3407,11 @@ files = [
|
|||||||
{file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"},
|
{file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zipp"
|
|
||||||
version = "3.19.2"
|
|
||||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
|
|
||||||
{file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
|
||||||
test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
|
||||||
|
|
||||||
[extras]
|
[extras]
|
||||||
breez = ["breez-sdk"]
|
breez = ["breez-sdk"]
|
||||||
liquid = ["wallycore"]
|
liquid = ["wallycore"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.13 | ^3.12 | ^3.11 | ^3.10 | ^3.9"
|
python-versions = "~3.12 | ~3.11 | ~3.10"
|
||||||
content-hash = "e263865649975ea7e977b3cbf6cb453c3653de115523d026e3863605ab48a463"
|
content-hash = "f56154a228bfd11ca92c1818dd2b7d71ff67b218225103f4b701e6523ba499ed"
|
||||||
|
|||||||
+6
-3
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.0.0-rc8"
|
version = "1.0.0"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -12,7 +12,7 @@ packages = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.13 | ^3.12 | ^3.11 | ^3.10 | ^3.9"
|
python = "~3.12 | ~3.11 | ~3.10"
|
||||||
bech32 = "1.2.0"
|
bech32 = "1.2.0"
|
||||||
click = "8.1.7"
|
click = "8.1.7"
|
||||||
ecdsa = "0.19.0"
|
ecdsa = "0.19.0"
|
||||||
@@ -63,6 +63,8 @@ breez-sdk = {version = "0.6.6", optional = true}
|
|||||||
jsonpath-ng = "^1.7.0"
|
jsonpath-ng = "^1.7.0"
|
||||||
pynostr = "^0.6.2"
|
pynostr = "^0.6.2"
|
||||||
python-multipart = "^0.0.20"
|
python-multipart = "^0.0.20"
|
||||||
|
filetype = "^1.2.0"
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
breez = ["breez-sdk"]
|
breez = ["breez-sdk"]
|
||||||
liquid = ["wallycore"]
|
liquid = ["wallycore"]
|
||||||
@@ -145,6 +147,7 @@ module = [
|
|||||||
"fastapi_sso.sso.*",
|
"fastapi_sso.sso.*",
|
||||||
"json5.*",
|
"json5.*",
|
||||||
"jsonpath_ng.*",
|
"jsonpath_ng.*",
|
||||||
|
"filetype.*",
|
||||||
]
|
]
|
||||||
ignore_missing_imports = "True"
|
ignore_missing_imports = "True"
|
||||||
|
|
||||||
@@ -192,7 +195,7 @@ extend-exclude = [
|
|||||||
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
||||||
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
|
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
|
||||||
# RUF012: mutable-class-default
|
# RUF012: mutable-class-default
|
||||||
ignore = ["UP007", "RUF012"]
|
ignore = ["RUF012"]
|
||||||
|
|
||||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||||
fixable = ["ALL"]
|
fixable = ["ALL"]
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from lnbits.helpers import safe_upload_file_path
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filepath",
|
||||||
|
[
|
||||||
|
"test.txt",
|
||||||
|
"test/test.txt",
|
||||||
|
"test/test/test.txt",
|
||||||
|
"test/../test.txt",
|
||||||
|
"*/test.txt",
|
||||||
|
"test/**/test.txt",
|
||||||
|
"./test.txt",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_safe_upload_file_path(filepath: str):
|
||||||
|
safe_path = safe_upload_file_path(filepath)
|
||||||
|
assert safe_path.name == "test.txt"
|
||||||
|
|
||||||
|
# check if subdirectories got removed
|
||||||
|
images_folder = Path(settings.lnbits_data_folder) / "images"
|
||||||
|
assert images_folder.resolve() / "test.txt" == safe_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filepath",
|
||||||
|
[
|
||||||
|
"../test.txt",
|
||||||
|
"test/../../test.txt",
|
||||||
|
"../../test.txt",
|
||||||
|
"test/../../../test.txt",
|
||||||
|
"../../../test.txt",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_unsafe_upload_file_path(filepath: str):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
safe_upload_file_path(filepath)
|
||||||
@@ -1170,7 +1170,7 @@
|
|||||||
"pending": false,
|
"pending": false,
|
||||||
"failed": false,
|
"failed": false,
|
||||||
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||||
"fee_msat": 50,
|
"fee_msat": 30000,
|
||||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
|
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
},
|
},
|
||||||
"mocks": {
|
"mocks": {
|
||||||
@@ -1187,8 +1187,8 @@
|
|||||||
"response": {
|
"response": {
|
||||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||||
"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000",
|
"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
"msatoshi": 21000,
|
"msatoshi": 210000,
|
||||||
"msatoshi_sent": 21050,
|
"msatoshi_sent": 240000,
|
||||||
"status": "paid"
|
"status": "paid"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1206,7 +1206,7 @@
|
|||||||
"response": {
|
"response": {
|
||||||
"payment_hash": "41UmpD0E6YVZTA36uEiBT1JLHHhlmOyaY77dstcmrJY=",
|
"payment_hash": "41UmpD0E6YVZTA36uEiBT1JLHHhlmOyaY77dstcmrJY=",
|
||||||
"payment_route": {
|
"payment_route": {
|
||||||
"total_fees_msat": 50
|
"total_fees_msat": 30000
|
||||||
},
|
},
|
||||||
"payment_preimage": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
"payment_preimage": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||||
}
|
}
|
||||||
@@ -1224,7 +1224,7 @@
|
|||||||
"response_type": "json",
|
"response_type": "json",
|
||||||
"response": {
|
"response": {
|
||||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||||
"fee": 50,
|
"fee": 30000,
|
||||||
"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000"
|
"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1257,7 @@
|
|||||||
{
|
{
|
||||||
"status": {
|
"status": {
|
||||||
"type": "sent",
|
"type": "sent",
|
||||||
"feesPaid": -50,
|
"feesPaid": -30000,
|
||||||
"paymentPreimage": "0000000000000000000000000000000000000000000000000000000000000000"
|
"paymentPreimage": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1287,7 +1287,7 @@
|
|||||||
"paid": true,
|
"paid": true,
|
||||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
|
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
"details": {
|
"details": {
|
||||||
"fee": 50
|
"fee": 30000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1303,7 +1303,7 @@
|
|||||||
"response_type": "json",
|
"response_type": "json",
|
||||||
"response": {
|
"response": {
|
||||||
"paymentHash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
"paymentHash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||||
"routingFeeSat": -50,
|
"routingFeeSat": -30,
|
||||||
"paymentPreimage": "0000000000000000000000000000000000000000000000000000000000000000"
|
"paymentPreimage": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1690,7 +1690,7 @@
|
|||||||
"response_type": "json",
|
"response_type": "json",
|
||||||
"response": {
|
"response": {
|
||||||
"paymentHash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
"paymentHash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||||
"routingFeeSat": -50
|
"routingFeeSat": -30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user