Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab619d11a3 | ||
|
|
bbad4a91ae | ||
|
|
fcebb7e28c | ||
|
|
ce5aa4c8a7 | ||
|
|
75bae67446 | ||
|
|
8c184356ef | ||
|
|
efc0547271 | ||
|
|
7a393b11fd | ||
|
|
fae3eca3c7 | ||
|
|
3398070dd5 | ||
|
|
7d98c6b080 | ||
|
|
5f4e889d3a | ||
|
|
fe774ae1a1 | ||
|
|
21e02cb20e | ||
|
|
a3d7b463ae | ||
|
|
e1e2112461 | ||
|
|
92fc9a130f | ||
|
|
e15f3cd207 | ||
|
|
041b358310 | ||
|
|
cbd3de88c4 | ||
|
|
156ab10ad6 | ||
|
|
b77c103020 | ||
|
|
1f34357ab7 | ||
|
|
d9eda48298 | ||
|
|
1be2d1a680 | ||
|
|
b55ae9daaf | ||
|
|
1f98aa2cdb | ||
|
|
55ec3f1812 | ||
|
|
7880044483 | ||
|
|
28b3f79d9b | ||
|
|
4c704305ac | ||
|
|
c47fd2a4d1 | ||
|
|
1255df43e6 | ||
|
|
6ccc10d327 | ||
|
|
8bcf5e4f70 | ||
|
|
04ec643c93 |
@@ -94,7 +94,7 @@ AUTH_SECRET_KEY=""
|
||||
######################################
|
||||
|
||||
AUTH_TOKEN_EXPIRE_MINUTES=525600
|
||||
# Possible authorization methods: user-id-only, username-password, nostr-auth-nip98, google-auth, github-auth, keycloak-auth
|
||||
# Possible authorization methods: user-id-only, username-password, nostr-auth-nip98, google-auth, github-auth, keycloak-auth, oidc-auth
|
||||
AUTH_ALLOWED_METHODS="user-id-only, username-password"
|
||||
# Set this flag if HTTP is used for OAuth
|
||||
# OAUTHLIB_INSECURE_TRANSPORT="1"
|
||||
@@ -191,7 +191,14 @@ BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
||||
# HEXSTRING instead of path also possible
|
||||
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
||||
|
||||
# TODO: add Spark
|
||||
# SparkL2Wallet (external sidecar: https://github.com/lnbits/spark_sidecar)
|
||||
SPARK_L2_NETWORK=MAINNET
|
||||
SPARK_L2_EXTERNAL_ENDPOINT=http://127.0.0.1:8765
|
||||
SPARK_L2_EXTERNAL_API_KEY=
|
||||
# optional tuning
|
||||
# SPARK_L2_PAY_WAIT_MS=4000
|
||||
# SPARK_L2_PAY_POLL_MS=500
|
||||
# SPARK_L2_STREAM_KEEPALIVE_MS=15000
|
||||
|
||||
# StrikeWallet
|
||||
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
||||
@@ -264,6 +271,50 @@ KEYCLOAK_DISCOVERY_URL=""
|
||||
KEYCLOAK_CLIENT_CUSTOM_ORG=""
|
||||
KEYCLOAK_CLIENT_CUSTOM_ICON=""
|
||||
|
||||
# OIDC OAuth Config
|
||||
# Generic OIDC provider configuration
|
||||
# Make sure that the redirect URI in your OIDC provider is set to: https://{domain}/api/v1/auth/oidc/token
|
||||
# Required scopes: openid, email, profile
|
||||
# The discovery URL must be accessible from your LNbits server
|
||||
# Always use HTTPS in production environments
|
||||
# The CUSTOM_ORG and CUSTOM_ICON settings allow you to customize the login button
|
||||
# For example: "Login via Zitadel" with the Zitadel logo
|
||||
OIDC_DISCOVERY_URL=""
|
||||
OIDC_CLIENT_ID=""
|
||||
OIDC_CLIENT_SECRET=""
|
||||
OIDC_CLIENT_CUSTOM_ORG=""
|
||||
OIDC_CLIENT_CUSTOM_ICON=""
|
||||
|
||||
# Example OIDC configurations for various providers:
|
||||
#
|
||||
# ZITADEL:
|
||||
# OIDC_DISCOVERY_URL=https://login.yourdomain.de/.well-known/openid-configuration
|
||||
# OIDC_CLIENT_ID=your-zitadel-client-id@project-id
|
||||
# OIDC_CLIENT_SECRET=your-zitadel-client-secret
|
||||
# OIDC_CLIENT_CUSTOM_ORG=Zitadel
|
||||
# OIDC_CLIENT_CUSTOM_ICON=/static/images/zitadel.png
|
||||
#
|
||||
# AUTHENTIK:
|
||||
# OIDC_DISCOVERY_URL=https://authentik.yourdomain.com/application/o/lnbits/.well-known/openid-configuration
|
||||
# OIDC_CLIENT_ID=your-authentik-client-id
|
||||
# OIDC_CLIENT_SECRET=your-authentik-client-secret
|
||||
# OIDC_CLIENT_CUSTOM_ORG=Authentik
|
||||
# OIDC_CLIENT_CUSTOM_ICON=/static/images/authentik.png
|
||||
#
|
||||
# AUTHELIA:
|
||||
# OIDC_DISCOVERY_URL=https://auth.yourdomain.com/.well-known/openid-configuration
|
||||
# OIDC_CLIENT_ID=your-authelia-client-id
|
||||
# OIDC_CLIENT_SECRET=your-authelia-client-secret
|
||||
# OIDC_CLIENT_CUSTOM_ORG=Authelia
|
||||
# OIDC_CLIENT_CUSTOM_ICON=/static/images/authelia.png
|
||||
#
|
||||
# OKTA:
|
||||
# OIDC_DISCOVERY_URL=https://your-domain.okta.com/.well-known/openid-configuration
|
||||
# OIDC_CLIENT_ID=your-okta-client-id
|
||||
# OIDC_CLIENT_SECRET=your-okta-client-secret
|
||||
# OIDC_CLIENT_CUSTOM_ORG=Okta
|
||||
# OIDC_CLIENT_CUSTOM_ICON=/static/images/okta.png
|
||||
|
||||
|
||||
######################################
|
||||
|
||||
@@ -319,10 +370,12 @@ LNBITS_SERVICE_FEE=0.0
|
||||
# disable fees for internal transactions
|
||||
# LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true
|
||||
|
||||
# value in millisats
|
||||
# The minimum fee reserved per payment (millisats)
|
||||
LNBITS_RESERVE_FEE_MIN=2000
|
||||
# value in percent
|
||||
# The percentage of the payment amount to reserve for routing fees (percent)
|
||||
LNBITS_RESERVE_FEE_PERCENT=1.0
|
||||
# The default time to wait to for status response from the funding source when paying an invoice
|
||||
LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS=5
|
||||
|
||||
# limit the maximum balance for each wallet
|
||||
# throw an error if the wallet attempts to create a new invoice
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.12"]
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.12"]
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.12"]
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.12"]
|
||||
uses: ./.github/workflows/migration.yml
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
uses: ./.github/workflows/regtest.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
python-version: ["3.12"]
|
||||
backend-wallet-class:
|
||||
- BoltzWallet
|
||||
- LndRestWallet
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
python-version: ["3.12"]
|
||||
uses: ./.github/workflows/jmeter.yml
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -22,6 +22,7 @@ jobs:
|
||||
- name: run LNbits
|
||||
env:
|
||||
LNBITS_ADMIN_UI: true
|
||||
AUTH_HTTPS_ONLY: false
|
||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||
run: |
|
||||
|
||||
@@ -6,39 +6,23 @@ jobs:
|
||||
|
||||
black:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
with:
|
||||
make: checkblack
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
ruff:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
with:
|
||||
make: checkruff
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
mypy:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
with:
|
||||
make: mypy
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
pyright:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
with:
|
||||
make: pyright
|
||||
python-version: ${{ matrix.python-version }}
|
||||
npm: true
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ on:
|
||||
python-version:
|
||||
description: "python version"
|
||||
type: string
|
||||
default: "3.10"
|
||||
default: "3.12"
|
||||
|
||||
jobs:
|
||||
make:
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os-version: ["ubuntu-24.04"]
|
||||
node-version: ["18.x"]
|
||||
node-version: ["24.x"]
|
||||
runs-on: ${{ matrix.os-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
python-version:
|
||||
default: "3.10"
|
||||
default: "3.12"
|
||||
type: string
|
||||
os-version:
|
||||
default: "ubuntu-24.04"
|
||||
|
||||
@@ -33,6 +33,7 @@ jobs:
|
||||
|
||||
docker:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
@@ -40,6 +41,16 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
docker-latest-rc:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
tag: latest-rc
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
pypi:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
@@ -52,6 +52,16 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
docker-latest-rc:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
tag: latest-rc
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
pypi:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
python-version:
|
||||
default: "3.10"
|
||||
default: "3.12"
|
||||
type: string
|
||||
os-version:
|
||||
default: "ubuntu-24.04"
|
||||
|
||||
@@ -14,11 +14,11 @@ repos:
|
||||
- id: mixed-line-ending
|
||||
- id: check-case-conflict
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 25.1.0
|
||||
rev: 26.3.1
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.10
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [ --fix, --exit-non-zero-on-fix ]
|
||||
|
||||
@@ -18,7 +18,7 @@ RUN uv sync --all-extras
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
|
||||
ENV FUNDING_SOURCE_MAX_RETRIES=10
|
||||
ENV FUNDING_SOURCE_MAX_RETRIES="10"
|
||||
|
||||
# Boltzd
|
||||
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
|
||||
|
||||
@@ -26,8 +26,10 @@ password = "boltz"
|
||||
host = "0.0.0.0"
|
||||
port = 9005
|
||||
|
||||
[swap]
|
||||
deferredClaimSymbols = ["BTC", "L-BTC"]
|
||||
|
||||
[[pairs]]
|
||||
isLegacy = true
|
||||
base = "BTC"
|
||||
quote = "BTC"
|
||||
rate = 1
|
||||
@@ -44,7 +46,6 @@ minSwapAmount = 50_000
|
||||
swapTaproot = 10080
|
||||
|
||||
[[pairs]]
|
||||
isLegacy = true
|
||||
base = "L-BTC"
|
||||
quote = "BTC"
|
||||
fee = 0.25
|
||||
@@ -83,6 +84,7 @@ maxZeroConfAmount = 0
|
||||
zmqpubrawblock = "tcp://bitcoind:29001"
|
||||
port = 18_443
|
||||
cookie = "/root/.bitcoin/regtest/.cookie"
|
||||
feeFloor = 0.2
|
||||
|
||||
wallet = "lnbits"
|
||||
|
||||
@@ -101,6 +103,7 @@ maxSwapAmount = 40_294_967
|
||||
minSwapAmount = 10_000
|
||||
maxZeroConfAmount = 40_294_967
|
||||
|
||||
|
||||
[liquid.chain]
|
||||
host = "elementsd"
|
||||
port = 18884
|
||||
|
||||
@@ -28,7 +28,7 @@ services:
|
||||
- lnd-2
|
||||
- boltz-postgres
|
||||
restart: always
|
||||
image: boltz/boltz:latest
|
||||
image: boltz/boltz:v3.12.1
|
||||
ports:
|
||||
- 9000:9000
|
||||
entrypoint: "sh -c 'sleep 30; /boltz-backend/bin/boltzd'"
|
||||
|
||||
@@ -2,15 +2,17 @@ ARG LNBITS_TAG=latest
|
||||
|
||||
FROM lnbits/lnbits:${LNBITS_TAG}
|
||||
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
|
||||
RUN apt-get update && apt-get -y upgrade
|
||||
RUN apt-get install -y netcat-openbsd npm nodejs git
|
||||
RUN apt-get install -y ca-certificates curl gnupg netcat-openbsd git nodejs
|
||||
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
# install sparksidecar
|
||||
RUN git clone https://github.com/lnbits/spark_sidecar
|
||||
RUN cd spark_sidecar && npm install
|
||||
RUN cd spark_sidecar && npm ci
|
||||
|
||||
# Reinstall dependencies for lnbits just in case (needed for CMD usage)
|
||||
RUN uv sync --all-extras
|
||||
@@ -19,13 +21,16 @@ RUN uv sync --all-extras
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
ENV LNBITS_BACKEND_WALLET_CLASS="SparkL2Wallet"
|
||||
ENV FUNDING_SOURCE_MAX_RETRIES=10
|
||||
ENV LNBITS_RESERVE_FEE_MIN="20000"
|
||||
ENV LNBITS_RESERVE_FEE_PERCENT="1"
|
||||
ENV LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS="20"
|
||||
ENV FUNDING_SOURCE_MAX_RETRIES="10"
|
||||
|
||||
# spark sidecar
|
||||
ENV SPARK_MNEMONIC="bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom"
|
||||
ENV SPARK_NETWORK="MAINNET"
|
||||
ENV SPARK_SIDECAR_PORT="8765"
|
||||
ENV SPARK_PAY_WAIT_MS="20000"
|
||||
ENV SPARK_MULTIPLICITY="3"
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
|
||||
| **Blink** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; focuses on mobile integrations. |
|
||||
| **ZBD** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
|
||||
| **Spark (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
|
||||
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | Medium | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
|
||||
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
|
||||
| **Cliche Wallet** | Self-custodial | ❌ | Medium | ❌ | Medium | Manual | Moderate | Moderate | Minimal fees | Medium | Lightweight wallet; suitable for embedded systems. |
|
||||
| **Strike** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
||||
| **LNPay** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
||||
@@ -60,7 +60,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
|
||||
|
||||
## Spark (L2)
|
||||
|
||||
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. You can let LNbits run the sidecar internally or run your own instance (see `https://github.com/lnbits/spark_sidecar`).
|
||||
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. Sidecar repo `https://github.com/lnbits/spark_sidecar`.
|
||||
|
||||
### Notes for readers
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# Generic OIDC Authentication Configuration
|
||||
|
||||
This document explains how to configure generic OIDC authentication for LNbits, which allows integration with various OIDC-compliant authentication providers such as Zitadel, Authentik, and others.
|
||||
|
||||
## Overview
|
||||
|
||||
The generic OIDC provider (`oidc`) complements the existing Keycloak provider and allows you to integrate any OIDC-compliant authentication service. You can customize the login button with your own organization name and icon.
|
||||
|
||||
## Configuration
|
||||
|
||||
Add the following environment variables to your `.env` file or system environment:
|
||||
|
||||
### Required Settings
|
||||
|
||||
```bash
|
||||
# Enable OIDC authentication
|
||||
LNBITS_AUTH_ALLOWED_METHODS=oidc-auth
|
||||
|
||||
# OIDC Discovery URL (well-known endpoint)
|
||||
LNBITS_OIDC_DISCOVERY_URL=https://your-oidc-provider-domain/.well-known/openid-configuration
|
||||
|
||||
# Client credentials from your OIDC provider
|
||||
LNBITS_OIDC_CLIENT_ID=your-client-id
|
||||
LNBITS_OIDC_CLIENT_SECRET=your-client-secret
|
||||
```
|
||||
|
||||
### Optional Settings - Customize the Login Button
|
||||
|
||||
You can customize how the OIDC login button appears to your users:
|
||||
|
||||
```bash
|
||||
# Custom organization name (displayed on the login button)
|
||||
# Example: "Login via Zitadel" or "Login via Authentik"
|
||||
LNBITS_OIDC_CLIENT_CUSTOM_ORG="Zitadel"
|
||||
|
||||
# Custom icon URL (displayed on the login button)
|
||||
# Can be a full URL or a path to a local image
|
||||
LNBITS_OIDC_CLIENT_CUSTOM_ICON=https://zitadel.com/favicon.svg
|
||||
```
|
||||
|
||||
If not set, the button will display "Login via OIDC" with a generic lock icon.
|
||||
|
||||
## Zitadel Configuration Example
|
||||
|
||||
For Zitadel, configure as follows:
|
||||
|
||||
1. Create a new application in Zitadel
|
||||
2. Choose "Web" application type
|
||||
3. Configure the redirect URI: `https://your-lnbits-domain/api/v1/auth/oidc/token`
|
||||
4. Save the Client ID and Client Secret
|
||||
5. Use these environment variables:
|
||||
|
||||
```bash
|
||||
LNBITS_AUTH_ALLOWED_METHODS=oidc-auth
|
||||
LNBITS_OIDC_DISCOVERY_URL=https://your-oidc-provider-domain/.well-known/openid-configuration
|
||||
LNBITS_OIDC_CLIENT_ID=your-zitadel-client-id
|
||||
LNBITS_OIDC_CLIENT_SECRET=your-zitadel-client-secret
|
||||
# Customize the button to show "Login via Zitadel" with Zitadel's logo
|
||||
LNBITS_OIDC_CLIENT_CUSTOM_ORG="Zitadel"
|
||||
LNBITS_OIDC_CLIENT_CUSTOM_ICON="https://zitadel.com/favicon.svg"
|
||||
```
|
||||
|
||||
**Result**: The login page will display a button with the text "Login via Zitadel" and the Zitadel logo.
|
||||
|
||||
## Authentik Configuration Example
|
||||
|
||||
For Authentik:
|
||||
|
||||
1. Create a new OAuth2/OpenID Provider
|
||||
2. Set the redirect URI: `https://your-lnbits-domain/api/v1/auth/oidc/token`
|
||||
3. Configure scopes: `openid`, `email`, `profile`
|
||||
4. Get the Client ID and Client Secret
|
||||
|
||||
```bash
|
||||
LNBITS_AUTH_ALLOWED_METHODS=oidc-auth
|
||||
LNBITS_OIDC_DISCOVERY_URL=https://authentik.yourdomain.com/application/o/your-app/.well-known/openid-configuration
|
||||
LNBITS_OIDC_CLIENT_ID=your-authentik-client-id
|
||||
LNBITS_OIDC_CLIENT_SECRET=your-authentik-client-secret
|
||||
LNBITS_OIDC_CLIENT_CUSTOM_ORG="Authentik"
|
||||
```
|
||||
|
||||
## Multiple Auth Methods
|
||||
|
||||
You can enable multiple authentication methods simultaneously:
|
||||
|
||||
```bash
|
||||
LNBITS_AUTH_ALLOWED_METHODS=username-password,oidc-auth,keycloak-auth
|
||||
```
|
||||
|
||||
## Discovery Endpoint Requirements
|
||||
|
||||
Your OIDC provider must expose a standard discovery endpoint (`.well-known/openid-configuration`) that includes:
|
||||
|
||||
- `authorization_endpoint`
|
||||
- `token_endpoint`
|
||||
- `userinfo_endpoint`
|
||||
- `jwks_uri` (JSON Web Key Set)
|
||||
|
||||
The OIDC implementation will automatically fetch these endpoints from the discovery URL.
|
||||
|
||||
## User Mapping
|
||||
|
||||
The OIDC provider maps user information from the OIDC userinfo endpoint:
|
||||
|
||||
- `sub` → User ID
|
||||
- `email` → Email address
|
||||
- `given_name` → First name
|
||||
- `family_name` → Last name
|
||||
- `name` or `preferred_username` → Display name
|
||||
- `picture` → Profile picture URL
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication fails
|
||||
|
||||
1. Verify the discovery URL is accessible
|
||||
2. Check that Client ID and Client Secret are correct
|
||||
3. Ensure redirect URI in your OIDC provider matches: `https://your-lnbits-domain/api/v1/auth/oidc/token`
|
||||
4. Check LNbits logs for detailed error messages
|
||||
|
||||
### User info not populated
|
||||
|
||||
Some OIDC providers may use different claim names. If user information is not correctly populated, check your provider's userinfo endpoint response format and adjust the provider class if needed.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Always use HTTPS in production
|
||||
- Keep client secrets secure and never commit them to version control
|
||||
- Use environment variables or secure configuration management
|
||||
- Regularly rotate client secrets
|
||||
- Review OIDC provider's security best practices
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The OIDC provider is implemented in `lnbits/core/models/sso/oidc.py` and extends the `fastapi_sso` library's `SSOBase` class. It uses the standard OpenID Connect flow with:
|
||||
|
||||
- Scopes: `openid`, `email`, `profile`
|
||||
- Response type: `code` (authorization code flow)
|
||||
- Discovery document for automatic endpoint resolution
|
||||
@@ -41,8 +41,8 @@ A backend wallet is selected and configured entirely through LNbits environment
|
||||
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
|
||||
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
|
||||
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
|
||||
| [Spark (L2)](#spark-l2) | | |
|
||||
| [Cliche Wallet](#cliche-wallet) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
|
||||
| [Spark L2](#spark-l2) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
|
||||
| [Cliche Wallet](#cliche-wallet) | | |
|
||||
| [Breez SDK](#breez-sdk) | [Breez Liquid SDK](#breez-liquid-sdk) | [Nostr Wallet Connect](#nostr-wallet-connect-nwc) |
|
||||
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
|
||||
| [Fake Wallet](#fake-wallet) | | |
|
||||
@@ -128,32 +128,18 @@ Old REST interface using [RTL c-lightning-REST](https://github.com/Ride-The-Ligh
|
||||
- `SPARK_URL`: `http://10.147.17.230:9737/rpc`
|
||||
- `SPARK_TOKEN`: `secret_access_key`
|
||||
|
||||
## Spark (L2)
|
||||
## Spark L2
|
||||
|
||||
Spark L2 is a self-custodial funding source that uses a small Node.js sidecar to talk to the Spark network and expose an HTTP API. You can let LNbits run the sidecar internally, or run it yourself and point LNbits at it.
|
||||
Self-custodial funding source using the [Spark L2](https://docs.spark.money/start/overview) network. Requires a Node.js [sidecar](https://github.com/lnbits/spark_sidecar) that bridges lnbits talking to Spark. Works in addition with any Spark-compatible seed (Wallet of Satoshi, BuhoGO, BlitzWallet).
|
||||
If the sidecar is started with a `mnemonic` then that mnemonic will be used. Otherwhise if a mnemonic is set for the `Spark L2` LNbits funding source then that mnemonic will be used.
|
||||
|
||||
**Required env vars**
|
||||
### Optional tuning
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `SparkL2Wallet`
|
||||
- `SPARK_L2_PAY_WAIT_MS`: `4000` _(payment timeout in ms)_
|
||||
- `SPARK_L2_PAY_POLL_MS`: `500` _(polling interval in ms)_
|
||||
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000` _(SSE keepalive in ms)_
|
||||
|
||||
**Internal sidecar (LNbits-managed)**
|
||||
|
||||
- `SPARK_L2_INTERNAL_SIDECAR_VERSION`: `0.1.1`
|
||||
- `SPARK_L2_MNEMONIC`: `...` (optional; auto-generated if blank)
|
||||
- `SPARK_L2_NETWORK`: `MAINNET`
|
||||
|
||||
Optional tuning:
|
||||
|
||||
- `SPARK_L2_PAY_WAIT_MS`: `4000`
|
||||
- `SPARK_L2_PAY_POLL_MS`: `500`
|
||||
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000`
|
||||
|
||||
**External sidecar**
|
||||
|
||||
- `SPARK_L2_EXTERNAL_ENDPOINT`: `http://127.0.0.1:8765`
|
||||
- `SPARK_L2_EXTERNAL_API_KEY`: `...` (if your sidecar requires auth)
|
||||
|
||||
Example: run the sidecar manually (external mode)
|
||||
### Example: run the sidecar
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lnbits/spark_sidecar.git
|
||||
@@ -167,7 +153,7 @@ SPARK_PAY_WAIT_MS=20000 \
|
||||
node server.mjs
|
||||
```
|
||||
|
||||
For testing, you can generate a 12-word mnemonic at `https://iancoleman.io/bip39/`. Store it securely — it controls your funds. Then select **Spark (L2)** as the funding source in LNbits.
|
||||
For testing, you can generate a 12-word mnemonic at https://iancoleman.io/bip39/. Store it securely — it controls your funds. Then select Spark (L2) as the funding source in LNbits.
|
||||
|
||||
## LND (REST)
|
||||
|
||||
|
||||
@@ -468,7 +468,12 @@ def register_async_tasks() -> None:
|
||||
create_permanent_task(wait_for_audit_data)
|
||||
create_permanent_task(wait_notification_messages)
|
||||
|
||||
create_permanent_task(run_interval(30 * 60, check_pending_payments))
|
||||
create_permanent_task(
|
||||
run_interval(
|
||||
settings.lnbits_funding_source_pending_interval_seconds,
|
||||
check_pending_payments,
|
||||
)
|
||||
)
|
||||
create_permanent_task(invoice_listener)
|
||||
create_permanent_task(internal_invoice_listener)
|
||||
create_permanent_task(cache.invalidate_forever)
|
||||
|
||||
@@ -149,14 +149,12 @@ async def get_payments_paginated( # noqa: C901
|
||||
f"(status = '{PaymentState.SUCCESS}' OR status = '{PaymentState.PENDING}')"
|
||||
)
|
||||
elif complete:
|
||||
clause.append(
|
||||
f"""
|
||||
clause.append(f"""
|
||||
(
|
||||
status = '{PaymentState.SUCCESS}'
|
||||
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||
)
|
||||
"""
|
||||
)
|
||||
""")
|
||||
elif pending:
|
||||
clause.append(f"status = '{PaymentState.PENDING}'")
|
||||
elif failed:
|
||||
@@ -346,14 +344,12 @@ async def get_payments_history(
|
||||
"wallet_id": wallet_id,
|
||||
}
|
||||
# count outgoing payments if they are still pending
|
||||
where = [
|
||||
f"""
|
||||
where = [f"""
|
||||
wallet_id = :wallet_id AND (
|
||||
status = '{PaymentState.SUCCESS}'
|
||||
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||
)
|
||||
"""
|
||||
]
|
||||
"""]
|
||||
clause = filters.where(where)
|
||||
transactions: list[dict] = await db.fetchall(
|
||||
# This query is safe from SQL injection:
|
||||
|
||||
@@ -4,7 +4,12 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||
from lnbits.core.crud.wallets import clear_wallet_cache, create_wallet, get_wallets
|
||||
from lnbits.core.crud.wallets import (
|
||||
clear_wallet_cache,
|
||||
create_wallet,
|
||||
get_standalone_wallet,
|
||||
get_wallets,
|
||||
)
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import UserAcls
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
@@ -52,17 +57,22 @@ async def get_accounts(
|
||||
) -> Page[AccountOverview]:
|
||||
where_clauses = []
|
||||
values: dict[str, Any] = {}
|
||||
filters = filters or Filters()
|
||||
|
||||
# Make wallet filter explicit
|
||||
wallet_filter = (
|
||||
next((f for f in filters.filters if f.field == "wallet_id"), None)
|
||||
if filters
|
||||
else None
|
||||
)
|
||||
if filters and wallet_filter and wallet_filter.values:
|
||||
where_clauses.append("wallets.id = :wallet_id")
|
||||
values = {**values, "wallet_id": next(iter(wallet_filter.values.values()))}
|
||||
filters.filters = [f for f in filters.filters if f.field != "wallet_id"]
|
||||
wallet_filter = filters.get_filter_by_field("wallet_id")
|
||||
|
||||
if wallet_filter and wallet_filter.values:
|
||||
wallet_id_value = next(iter(wallet_filter.values.values()), None)
|
||||
wallet = (
|
||||
await get_standalone_wallet(wallet_id_value, deleted=None, conn=conn)
|
||||
if wallet_id_value
|
||||
else None
|
||||
)
|
||||
if not wallet:
|
||||
return Page(data=[], total=0)
|
||||
where_clauses.append("accounts.id = :account_id")
|
||||
values = {**values, "account_id": wallet.user}
|
||||
filters.remove_filter_by_field("wallet_id")
|
||||
|
||||
return await (conn or db).fetch_page(
|
||||
"""
|
||||
|
||||
@@ -10,31 +10,26 @@ from lnbits.db import Connection
|
||||
|
||||
|
||||
async def m000_create_migrations_table(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS dbversions (
|
||||
db TEXT PRIMARY KEY,
|
||||
version INT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m001_initial(db: Connection):
|
||||
"""
|
||||
Initial LNbits tables.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT,
|
||||
pass TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS extensions (
|
||||
"user" TEXT NOT NULL,
|
||||
extension TEXT NOT NULL,
|
||||
@@ -42,10 +37,8 @@ async def m001_initial(db: Connection):
|
||||
|
||||
UNIQUE ("user", extension)
|
||||
);
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS wallets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
@@ -53,10 +46,8 @@ async def m001_initial(db: Connection):
|
||||
adminkey TEXT NOT NULL,
|
||||
inkey TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
f"""
|
||||
""")
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS apipayments (
|
||||
payhash TEXT NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
@@ -67,11 +58,9 @@ async def m001_initial(db: Connection):
|
||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
UNIQUE (wallet, payhash)
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT wallet, SUM(amount) AS s -- incoming
|
||||
@@ -85,8 +74,7 @@ async def m001_initial(db: Connection):
|
||||
GROUP BY wallet
|
||||
)x
|
||||
GROUP BY wallet;
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m002_add_fields_to_apipayments(db: Connection):
|
||||
@@ -149,8 +137,7 @@ async def m004_ensure_fees_are_always_negative(db: Connection):
|
||||
"""
|
||||
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT wallet, SUM(amount) AS s -- incoming
|
||||
@@ -164,8 +151,7 @@ async def m004_ensure_fees_are_always_negative(db: Connection):
|
||||
GROUP BY wallet
|
||||
)x
|
||||
GROUP BY wallet;
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m005_balance_check_balance_notify(db: Connection):
|
||||
@@ -174,8 +160,7 @@ async def m005_balance_check_balance_notify(db: Connection):
|
||||
LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
|
||||
"""
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS balance_check (
|
||||
wallet TEXT NOT NULL REFERENCES wallets (id),
|
||||
service TEXT NOT NULL,
|
||||
@@ -183,19 +168,16 @@ async def m005_balance_check_balance_notify(db: Connection):
|
||||
|
||||
UNIQUE(wallet, service)
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS balance_notify (
|
||||
wallet TEXT NOT NULL REFERENCES wallets (id),
|
||||
url TEXT NOT NULL,
|
||||
|
||||
UNIQUE(wallet, url)
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m006_add_invoice_expiry_to_apipayments(db: Connection):
|
||||
@@ -262,19 +244,16 @@ async def m007_set_invoice_expiries(db: Connection):
|
||||
|
||||
|
||||
async def m008_create_admin_settings_table(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
super_user TEXT,
|
||||
editable_settings TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m009_create_tinyurl_table(db: Connection):
|
||||
await db.execute(
|
||||
f"""
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS tiny_url (
|
||||
id TEXT PRIMARY KEY,
|
||||
url TEXT,
|
||||
@@ -282,13 +261,11 @@ async def m009_create_tinyurl_table(db: Connection):
|
||||
wallet TEXT,
|
||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m010_create_installed_extensions_table(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS installed_extensions (
|
||||
id TEXT PRIMARY KEY,
|
||||
version TEXT NOT NULL,
|
||||
@@ -299,8 +276,7 @@ async def m010_create_installed_extensions_table(db: Connection):
|
||||
active BOOLEAN DEFAULT false,
|
||||
meta TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m011_optimize_balances_view(db: Connection):
|
||||
@@ -309,23 +285,19 @@ async def m011_optimize_balances_view(db: Connection):
|
||||
over the payments table instead of 2.
|
||||
"""
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT wallet, SUM(amount - abs(fee)) AS balance
|
||||
FROM apipayments
|
||||
WHERE (pending = false AND amount > 0) OR amount < 0
|
||||
GROUP BY wallet
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m012_add_currency_to_wallet(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
ALTER TABLE wallets ADD COLUMN currency TEXT
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m013_add_deleted_to_wallets(db: Connection):
|
||||
@@ -345,15 +317,13 @@ async def m014_set_deleted_wallets(db: Connection):
|
||||
Sets deleted column to wallets.
|
||||
"""
|
||||
try:
|
||||
result = await db.execute(
|
||||
"""
|
||||
result = await db.execute("""
|
||||
SELECT *
|
||||
FROM wallets
|
||||
WHERE user LIKE 'del:%'
|
||||
AND adminkey LIKE 'del:%'
|
||||
AND inkey LIKE 'del:%'
|
||||
"""
|
||||
)
|
||||
""")
|
||||
rows = result.mappings().all()
|
||||
|
||||
for row in rows:
|
||||
@@ -386,8 +356,7 @@ async def m014_set_deleted_wallets(db: Connection):
|
||||
|
||||
|
||||
async def m015_create_push_notification_subscriptions_table(db: Connection):
|
||||
await db.execute(
|
||||
f"""
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
|
||||
endpoint TEXT NOT NULL,
|
||||
"user" TEXT NOT NULL,
|
||||
@@ -396,8 +365,7 @@ async def m015_create_push_notification_subscriptions_table(db: Connection):
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
PRIMARY KEY (endpoint, "user")
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m016_add_username_column_to_accounts(db: Connection):
|
||||
@@ -484,8 +452,7 @@ async def m018_balances_view_exclude_deleted(db: Connection):
|
||||
Make deleted wallets not show up in the balances view.
|
||||
"""
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT apipayments.wallet,
|
||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||
@@ -495,8 +462,7 @@ async def m018_balances_view_exclude_deleted(db: Connection):
|
||||
AND ((apipayments.pending = false AND apipayments.amount > 0)
|
||||
OR apipayments.amount < 0)
|
||||
GROUP BY wallet
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m019_balances_view_based_on_wallets(db: Connection):
|
||||
@@ -505,8 +471,7 @@ async def m019_balances_view_based_on_wallets(db: Connection):
|
||||
Important for querying whole lnbits balances.
|
||||
"""
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT apipayments.wallet,
|
||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||
@@ -516,8 +481,7 @@ async def m019_balances_view_based_on_wallets(db: Connection):
|
||||
AND ((apipayments.pending = false AND apipayments.amount > 0)
|
||||
OR apipayments.amount < 0)
|
||||
GROUP BY apipayments.wallet
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m020_add_column_column_to_user_extensions(db: Connection):
|
||||
@@ -536,8 +500,7 @@ async def m021_add_success_failed_to_apipayments(db: Connection):
|
||||
await db.execute("UPDATE apipayments SET status = 'success' WHERE NOT pending")
|
||||
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT apipayments.wallet,
|
||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||
@@ -549,8 +512,7 @@ async def m021_add_success_failed_to_apipayments(db: Connection):
|
||||
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
|
||||
)
|
||||
GROUP BY apipayments.wallet
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m022_add_pubkey_to_accounts(db: Connection):
|
||||
@@ -581,8 +543,7 @@ async def m024_drop_pending(db: Connection):
|
||||
|
||||
async def m025_refresh_view(db: Connection):
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT apipayments.wallet_id,
|
||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||
@@ -594,8 +555,7 @@ async def m025_refresh_view(db: Connection):
|
||||
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
|
||||
)
|
||||
GROUP BY apipayments.wallet_id
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m026_update_payment_table(db: Connection):
|
||||
@@ -658,8 +618,7 @@ async def m027_update_apipayments_data(db: Connection):
|
||||
|
||||
async def m028_update_settings(db: Connection):
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS system_settings (
|
||||
id TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
@@ -667,8 +626,7 @@ async def m028_update_settings(db: Connection):
|
||||
|
||||
UNIQUE (id, tag)
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
async def _insert_key_value(id_: str, value: Any):
|
||||
await db.execute(
|
||||
@@ -691,8 +649,7 @@ async def m028_update_settings(db: Connection):
|
||||
|
||||
|
||||
async def m029_create_audit_table(db: Connection):
|
||||
await db.execute(
|
||||
f"""
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS audit (
|
||||
component TEXT,
|
||||
ip_address TEXT,
|
||||
@@ -706,16 +663,13 @@ async def m029_create_audit_table(db: Connection):
|
||||
delete_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m030_add_user_api_tokens_column(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
ALTER TABLE accounts ADD COLUMN access_control_list TEXT
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m031_add_color_and_icon_to_wallets(db: Connection):
|
||||
@@ -738,32 +692,25 @@ async def m033_update_payment_table(db: Connection):
|
||||
|
||||
|
||||
async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m035_add_wallet_type_column(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
ALTER TABLE wallets ADD COLUMN wallet_type TEXT DEFAULT 'lightning'
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m036_add_shared_wallet_column(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m037_create_assets_table(db: Connection):
|
||||
await db.execute(
|
||||
f"""
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
@@ -776,16 +723,13 @@ async def m037_create_assets_table(db: Connection):
|
||||
data {db.blob} NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m038_add_labels_for_payments(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
ALTER TABLE apipayments ADD COLUMN labels TEXT
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m039_index_payments(db: Connection):
|
||||
@@ -804,11 +748,9 @@ async def m039_index_payments(db: Connection):
|
||||
]
|
||||
for index in indexes:
|
||||
logger.debug(f"Creating index idx_payments_{index}...")
|
||||
await db.execute(
|
||||
f"""
|
||||
await db.execute(f"""
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_{index} ON apipayments ({index});
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m040_index_wallets(db: Connection):
|
||||
@@ -825,11 +767,9 @@ async def m040_index_wallets(db: Connection):
|
||||
|
||||
for index in indexes:
|
||||
logger.debug(f"Creating index idx_wallets_{index}...")
|
||||
await db.execute(
|
||||
f"""
|
||||
await db.execute(f"""
|
||||
CREATE INDEX IF NOT EXISTS idx_wallets_{index} ON wallets ("{index}");
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m042_index_accounts(db: Connection):
|
||||
@@ -843,11 +783,9 @@ async def m042_index_accounts(db: Connection):
|
||||
|
||||
for index in indexes:
|
||||
logger.debug(f"Creating index idx_wallets_{index}...")
|
||||
await db.execute(
|
||||
f"""
|
||||
await db.execute(f"""
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m043_add_ui_customization_to_accounts(db: Connection):
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""SSO authentication providers for LNbits"""
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Generic OIDC SSO Login Helper"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
|
||||
|
||||
|
||||
class OidcSSO(SSOBase):
|
||||
"""Class providing login via Generic OIDC OAuth (e.g., Zitadel, Authentik, etc.)"""
|
||||
|
||||
provider = "oidc"
|
||||
scope = ["openid", "email", "profile"]
|
||||
discovery_url = ""
|
||||
|
||||
async def openid_from_response(
|
||||
self, response: dict, session: Optional["httpx.AsyncClient"] = None
|
||||
) -> OpenID:
|
||||
"""Return OpenID from user information provided by OIDC provider"""
|
||||
return OpenID(
|
||||
email=response.get("email", ""),
|
||||
provider=self.provider,
|
||||
id=response.get("sub"),
|
||||
first_name=response.get("given_name"),
|
||||
last_name=response.get("family_name"),
|
||||
display_name=response.get("name") or response.get("preferred_username"),
|
||||
picture=response.get("picture"),
|
||||
)
|
||||
|
||||
async def get_discovery_document(self) -> DiscoveryDocument:
|
||||
"""Get document containing handy urls"""
|
||||
async with httpx.AsyncClient() as session:
|
||||
response = await session.get(self.discovery_url)
|
||||
content = response.json()
|
||||
|
||||
return content
|
||||
@@ -154,9 +154,20 @@ async def impersonate_user(
|
||||
|
||||
max_age = settings.auth_token_expire_minutes * 60
|
||||
response.set_cookie(
|
||||
"admin_access_token", cookie_access_token, httponly=True, max_age=max_age
|
||||
"admin_access_token",
|
||||
cookie_access_token,
|
||||
httponly=True,
|
||||
secure=settings.auth_https_only,
|
||||
samesite="lax",
|
||||
max_age=max_age,
|
||||
)
|
||||
response.set_cookie(
|
||||
"is_lnbits_user_impersonated",
|
||||
"true",
|
||||
secure=settings.auth_https_only,
|
||||
samesite="lax",
|
||||
max_age=max_age,
|
||||
)
|
||||
response.set_cookie("is_lnbits_user_impersonated", "true", max_age=max_age)
|
||||
return response
|
||||
|
||||
|
||||
@@ -177,7 +188,12 @@ async def stop_impersonate_user(
|
||||
)
|
||||
max_age = settings.auth_token_expire_minutes * 60
|
||||
response.set_cookie(
|
||||
"cookie_access_token", admin_access_token, httponly=True, max_age=max_age
|
||||
"cookie_access_token",
|
||||
admin_access_token,
|
||||
httponly=True,
|
||||
secure=settings.auth_https_only,
|
||||
samesite="lax",
|
||||
max_age=max_age,
|
||||
)
|
||||
response.delete_cookie("admin_access_token")
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
@@ -560,7 +576,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
||||
id=uuid4().hex, email=email, extra=UserExtra(email_verified=True)
|
||||
)
|
||||
await create_user_account(account)
|
||||
return _auth_redirect_response(redirect_path, email)
|
||||
return _auth_redirect_response(redirect_path, account.id, email)
|
||||
|
||||
|
||||
def _auth_success_response(
|
||||
@@ -575,9 +591,20 @@ def _auth_success_response(
|
||||
max_age = settings.auth_token_expire_minutes * 60
|
||||
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
||||
response.set_cookie(
|
||||
"cookie_access_token", access_token, httponly=True, max_age=max_age
|
||||
"cookie_access_token",
|
||||
access_token,
|
||||
httponly=True,
|
||||
secure=settings.auth_https_only,
|
||||
samesite="lax",
|
||||
max_age=max_age,
|
||||
)
|
||||
response.set_cookie(
|
||||
"is_lnbits_user_authorized",
|
||||
"true",
|
||||
secure=settings.auth_https_only,
|
||||
samesite="lax",
|
||||
max_age=max_age,
|
||||
)
|
||||
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
|
||||
return response
|
||||
@@ -594,15 +621,28 @@ def _auth_api_token_response(
|
||||
)
|
||||
|
||||
|
||||
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
||||
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
|
||||
def _auth_redirect_response(path: str, user_id: str, email: str) -> RedirectResponse:
|
||||
payload = AccessTokenPayload(
|
||||
usr=user_id, sub="", email=email, auth_time=int(time())
|
||||
)
|
||||
access_token = create_access_token(data=payload.dict())
|
||||
max_age = settings.auth_token_expire_minutes * 60
|
||||
response = RedirectResponse(path)
|
||||
response.set_cookie(
|
||||
"cookie_access_token", access_token, httponly=True, max_age=max_age
|
||||
"cookie_access_token",
|
||||
access_token,
|
||||
httponly=True,
|
||||
secure=settings.auth_https_only,
|
||||
samesite="lax",
|
||||
max_age=max_age,
|
||||
)
|
||||
response.set_cookie(
|
||||
"is_lnbits_user_authorized",
|
||||
"true",
|
||||
secure=settings.auth_https_only,
|
||||
samesite="lax",
|
||||
max_age=max_age,
|
||||
)
|
||||
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
return response
|
||||
|
||||
@@ -622,7 +662,10 @@ def _new_sso(provider: str) -> SSOBase | None:
|
||||
|
||||
sso_provider_class = _find_auth_provider_class(provider)
|
||||
sso_provider = sso_provider_class(
|
||||
client_id, client_secret, None, allow_insecure_http=True
|
||||
client_id,
|
||||
client_secret,
|
||||
None,
|
||||
allow_insecure_http=not settings.auth_https_only,
|
||||
)
|
||||
if (
|
||||
discovery_url
|
||||
|
||||
@@ -36,6 +36,8 @@ lnurl_router = APIRouter(tags=["LNURL"])
|
||||
|
||||
async def _handle(lnurl: str) -> LnurlResponseModel:
|
||||
try:
|
||||
if "@" in lnurl: # lower case lightning addresses
|
||||
lnurl = lnurl.lower()
|
||||
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
|
||||
if isinstance(res, LnurlErrorResponse):
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason)
|
||||
|
||||
@@ -613,6 +613,12 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
for page_filter in self.filters:
|
||||
page_filter.table_name = table_name
|
||||
|
||||
def get_filter_by_field(self, field: str) -> Filter[TFilterModel] | None:
|
||||
return next((f for f in self.filters if f.field == field), None)
|
||||
|
||||
def remove_filter_by_field(self, field: str) -> None:
|
||||
self.filters = [f for f in self.filters if f.field != field]
|
||||
|
||||
|
||||
class DbJsonEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
|
||||
@@ -7,15 +7,14 @@ from typing import Any
|
||||
from urllib import request
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import jinja2
|
||||
import jwt
|
||||
import shortuuid
|
||||
from fastapi.routing import APIRoute
|
||||
from loguru import logger
|
||||
from packaging import version
|
||||
from pydantic.schema import field_schema
|
||||
from starlette.templating import Jinja2Templates
|
||||
|
||||
from lnbits.jinja2_templating import Jinja2Templates
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
from lnbits.utils.exchange_rates import currencies
|
||||
@@ -66,7 +65,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
for f in additional_folders
|
||||
]
|
||||
folders.extend(additional_folders)
|
||||
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
||||
t = Jinja2Templates(directory=folders)
|
||||
t.env.globals["static_url_for"] = static_url_for
|
||||
t.env.globals["normalize_path"] = normalize_path
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import typing
|
||||
|
||||
from jinja2 import BaseLoader, Environment, pass_context
|
||||
from starlette.datastructures import QueryParams
|
||||
from starlette.requests import Request
|
||||
from starlette.templating import Jinja2Templates as SuperJinja2Templates
|
||||
|
||||
|
||||
class Jinja2Templates(SuperJinja2Templates):
|
||||
def __init__(self, loader: BaseLoader) -> None:
|
||||
self.env = self.get_environment(loader)
|
||||
super().__init__(env=self.env)
|
||||
|
||||
def get_environment(self, loader: BaseLoader) -> Environment:
|
||||
@pass_context
|
||||
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
|
||||
request: Request = context["request"]
|
||||
return request.app.url_path_for(name, **path_params)
|
||||
|
||||
def url_params_update(init: QueryParams, **new: typing.Any) -> QueryParams:
|
||||
values = dict(init)
|
||||
values.update(new)
|
||||
return QueryParams(**values)
|
||||
|
||||
env = Environment(loader=loader, autoescape=True)
|
||||
env.globals["url_for"] = url_for
|
||||
env.globals["url_params_update"] = url_params_update
|
||||
return env
|
||||
@@ -76,7 +76,7 @@ class LndRestNode(Node):
|
||||
return response.json()
|
||||
|
||||
def get(self, path: str, **kwargs):
|
||||
return self.request("GET", path, **kwargs)
|
||||
return self.request("GET", path, timeout=30, **kwargs)
|
||||
|
||||
async def _get_id(self) -> str:
|
||||
info = await self.get("/v1/getinfo")
|
||||
@@ -99,11 +99,12 @@ class LndRestNode(Node):
|
||||
"perm": True,
|
||||
"timeout": 30,
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
async def disconnect_peer(self, peer_id: str):
|
||||
try:
|
||||
await self.request("DELETE", "/v1/peers/" + peer_id)
|
||||
await self.request("DELETE", "/v1/peers/" + peer_id, timeout=30)
|
||||
except HTTPException as exc:
|
||||
if "unable to disconnect" in exc.detail:
|
||||
raise HTTPException(
|
||||
@@ -141,6 +142,7 @@ class LndRestNode(Node):
|
||||
"local_funding_amount": local_amount,
|
||||
"push_sat": push_amount,
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
return ChannelPoint(
|
||||
# WHY IS THIS REVERSED?!
|
||||
@@ -214,6 +216,7 @@ class LndRestNode(Node):
|
||||
# 'min_htlc_msat': <uint64>,
|
||||
# 'inbound_fee': <InboundFee>,
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
||||
|
||||
@@ -292,13 +292,13 @@ class ThemesSettings(LNbitsSettings):
|
||||
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
|
||||
lnbits_apple_touch_icon: str | None = Field(default=None)
|
||||
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
||||
lnbits_default_theme: str = Field(default="salvador")
|
||||
lnbits_default_theme: str = Field(default="bitcoin")
|
||||
lnbits_default_border: str = Field(default="hard-border")
|
||||
lnbits_default_gradient: bool = Field(default=True)
|
||||
lnbits_default_bgimage: str | None = Field(default=None)
|
||||
lnbits_default_dark: bool = Field(default=True)
|
||||
lnbits_default_card_rounded: bool = Field(default=False)
|
||||
lnbits_default_card_gradient: bool = Field(default=False)
|
||||
lnbits_default_card_rounded: bool = Field(default=True)
|
||||
lnbits_default_card_gradient: bool = Field(default=True)
|
||||
lnbits_default_card_shadow: bool = Field(default=False)
|
||||
|
||||
|
||||
@@ -610,6 +610,7 @@ class SparkL2FundingSource(LNbitsSettings):
|
||||
spark_l2_network: str = Field(default="MAINNET")
|
||||
spark_l2_external_endpoint: str | None = Field(default="http://localhost:8765")
|
||||
spark_l2_external_api_key: str | None = Field(default=None)
|
||||
spark_l2_mnemonic: str | None = Field(default=None)
|
||||
spark_l2_pay_wait_ms: int = Field(default=4000, ge=0)
|
||||
spark_l2_pay_poll_ms: int = Field(default=500, ge=0)
|
||||
spark_l2_stream_keepalive_ms: int = Field(default=15000, ge=0)
|
||||
@@ -731,6 +732,7 @@ class FundingSourcesSettings(
|
||||
# How long to wait for the payment to be confirmed before returning a pending status
|
||||
# It will not fail the payment, it will make it return pending after the timeout
|
||||
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
|
||||
lnbits_funding_source_pending_interval_seconds: int = Field(default=1800, ge=0)
|
||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||
|
||||
|
||||
@@ -795,6 +797,7 @@ class AuthMethods(Enum):
|
||||
google_auth = "google-auth"
|
||||
github_auth = "github-auth"
|
||||
keycloak_auth = "keycloak-auth"
|
||||
oidc_auth = "oidc-auth"
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
@@ -805,6 +808,7 @@ class AuthMethods(Enum):
|
||||
AuthMethods.google_auth.value,
|
||||
AuthMethods.github_auth.value,
|
||||
AuthMethods.keycloak_auth.value,
|
||||
AuthMethods.oidc_auth.value,
|
||||
]
|
||||
|
||||
|
||||
@@ -850,6 +854,14 @@ class KeycloakAuthSettings(LNbitsSettings):
|
||||
keycloak_client_custom_icon: str | None = Field(default=None)
|
||||
|
||||
|
||||
class OidcAuthSettings(LNbitsSettings):
|
||||
oidc_discovery_url: str = Field(default="")
|
||||
oidc_client_id: str = Field(default="")
|
||||
oidc_client_secret: str = Field(default="")
|
||||
oidc_client_custom_org: str | None = Field(default=None)
|
||||
oidc_client_custom_icon: str | None = Field(default=None)
|
||||
|
||||
|
||||
class AuditSettings(LNbitsSettings):
|
||||
lnbits_audit_enabled: bool = Field(default=True)
|
||||
|
||||
@@ -957,6 +969,7 @@ class EditableSettings(
|
||||
GoogleAuthSettings,
|
||||
GitHubAuthSettings,
|
||||
KeycloakAuthSettings,
|
||||
OidcAuthSettings,
|
||||
):
|
||||
@validator(
|
||||
"lnbits_admin_users",
|
||||
@@ -992,6 +1005,9 @@ class EnvSettings(LNbitsSettings):
|
||||
debug: bool = Field(default=False)
|
||||
debug_database: bool = Field(default=False)
|
||||
bundle_assets: bool = Field(default=True)
|
||||
# When enabled, auth cookies require HTTPS and SSO will reject insecure HTTP.
|
||||
# Set to false for local/dev environments that run without TLS.
|
||||
auth_https_only: bool = Field(default=True)
|
||||
host: str = Field(default="127.0.0.1")
|
||||
port: int = Field(default=5000, gt=0)
|
||||
forwarded_allow_ips: str = Field(default="*")
|
||||
@@ -1173,6 +1189,8 @@ class PublicSettings(BaseModel):
|
||||
auth_methods: list[str] = Field(alias="authMethods")
|
||||
keycloak_org: str | None = Field(alias="keycloakOrg")
|
||||
keycloak_icon: str | None = Field(alias="keycloakIcon")
|
||||
oidc_org: str | None = Field(alias="oidcOrg")
|
||||
oidc_icon: str | None = Field(alias="oidcIcon")
|
||||
has_holdinvoice: bool = Field(alias="hasHoldinvoice")
|
||||
has_nodemanager: bool = Field(alias="hasNodemanager")
|
||||
show_nodemanager: bool = Field(alias="showNodemanager")
|
||||
@@ -1237,6 +1255,8 @@ class PublicSettings(BaseModel):
|
||||
authMethods=settings.auth_allowed_methods,
|
||||
keycloakOrg=settings.keycloak_client_custom_org,
|
||||
keycloakIcon=settings.keycloak_client_custom_icon,
|
||||
oidcOrg=settings.oidc_client_custom_org,
|
||||
oidcIcon=settings.oidc_client_custom_icon,
|
||||
hasHoldinvoice=settings.has_holdinvoice,
|
||||
hasNodemanager=settings.has_nodemanager,
|
||||
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
|
||||
|
||||
@@ -6,6 +6,7 @@ window.localisation.br = {
|
||||
funding: 'Financiamento',
|
||||
users: 'Usuários',
|
||||
audit: 'Auditoria',
|
||||
api_watch: 'Relógio da API',
|
||||
apps: 'Aplicativos',
|
||||
channels: 'Canais',
|
||||
transactions: 'Transações',
|
||||
@@ -21,13 +22,17 @@ window.localisation.br = {
|
||||
active_channels: 'Canais Ativos',
|
||||
connect_peer: 'Conectar Par',
|
||||
connect: 'Conectar',
|
||||
reconnect: 'Reconectar',
|
||||
open_channel: 'Canal Aberto',
|
||||
open: 'Abrir',
|
||||
clear: 'Limpar',
|
||||
close_channel: 'Fechar Canal',
|
||||
close: 'Fechar',
|
||||
restart: 'Reiniciar servidor',
|
||||
image_library: 'Biblioteca de Imagens',
|
||||
save: 'Salvar',
|
||||
save_tooltip: 'Salvar suas alterações',
|
||||
must_save: 'Você tem alterações não salvas',
|
||||
credit_debit: 'Crédito / Débito',
|
||||
credit_hint: 'Pressione Enter para creditar a conta',
|
||||
credit_label: '{denomination} para creditar',
|
||||
@@ -46,24 +51,47 @@ window.localisation.br = {
|
||||
export_to_phone: 'Exportar para o telefone com código QR',
|
||||
export_to_phone_desc:
|
||||
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
|
||||
access_wallet_on_mobile: 'Acesso Móvel',
|
||||
stored_paylinks: 'Links LNURL de pagamento armazenados',
|
||||
wallet: 'Carteira:',
|
||||
wallet_name: 'Nome da carteira',
|
||||
wallet_type: 'Tipo de carteira',
|
||||
shared_wallet: 'Carteira Compartilhada',
|
||||
share_wallet: 'Compartilhar Carteira',
|
||||
update_permissions: 'Atualizar Permissões',
|
||||
shared_wallet_id: 'ID da Carteira Compartilhada',
|
||||
shared_wallet_desc:
|
||||
'Você foi convidado(a) para ter acesso à carteira de outra pessoa.',
|
||||
wallets: 'Carteiras',
|
||||
exclude_wallets: 'Excluir Carteiras',
|
||||
add_wallet: 'Adicionar nova carteira',
|
||||
reject_wallet: 'Rejeitar carteira',
|
||||
add_new_wallet: 'Adicionar uma nova carteira',
|
||||
pin_wallet: 'Fixar carteira',
|
||||
delete_wallet: 'Excluir carteira',
|
||||
delete_wallet_desc:
|
||||
'Toda a carteira será excluída, os fundos serão IRRECUPERÁVEIS.',
|
||||
rename_wallet: 'Renomear carteira',
|
||||
update_name: 'Atualizar nome',
|
||||
fiat_tracking: 'Rastreamento Fiat',
|
||||
fiat_providers: 'Provedores fiat',
|
||||
fiat_warning_bitcoin:
|
||||
'Provedores Fiat podem ficar nervosos com qualquer coisa relacionada ao bitcoin, portanto, evite usar a palavra "bitcoin" nos seus memorandos!',
|
||||
currency: 'Moeda',
|
||||
update_currency: 'Atualizar moeda',
|
||||
press_to_claim: 'Pressione para solicitar bitcoin',
|
||||
claim_desc:
|
||||
'Parece que você tem um valor resgatável de bitcoin, mas ainda não tem uma carteira. Pressione o botão abaixo para reivindicá-lo. Isso criará uma nova carteira para você.',
|
||||
donate: 'Doar',
|
||||
view_github: 'Ver no GitHub',
|
||||
voidwallet_active: 'VoidWallet está ativo! Pagamentos desabilitados',
|
||||
use_with_caution: 'USE COM CAUTELA - a carteira {name} ainda está em BETA',
|
||||
service_fee: 'Taxa de serviço: {amount} % por transação',
|
||||
service_fee_max: 'Taxa de serviço: {amount} % por transação (máx {max} sats)',
|
||||
voidwallet_active_user:
|
||||
'Fonte de financiamento indisponível. Por favor, entre em contato com seu administrador para configurar.',
|
||||
voidwallet_active_admin:
|
||||
'Fonte de financiamento indisponível. Clique aqui para configurar.',
|
||||
service_fee_badge: 'Taxa de serviço: {amount} % por transação',
|
||||
service_fee_max_badge:
|
||||
'Taxa de serviço: {amount} % por transação (máximo {max} {denom})',
|
||||
service_fee_tooltip:
|
||||
'Taxa de serviço cobrada pelo administrador do servidor LNbits por transação de saída',
|
||||
toggle_darkmode: 'Alternar modo escuro',
|
||||
@@ -79,6 +107,7 @@ window.localisation.br = {
|
||||
create_invoice: 'Criar Fatura',
|
||||
camera_tooltip: 'Usar a câmara para escanear uma fatura / QR',
|
||||
export_csv: 'Exportar para CSV',
|
||||
export_csv_details: 'Exportar para CSV com detalhes',
|
||||
chart_tooltip: 'Mostrar gráfico',
|
||||
pending: 'Pendente',
|
||||
copy_invoice: 'Copiar fatura',
|
||||
@@ -86,15 +115,27 @@ window.localisation.br = {
|
||||
cancel: 'Cancelar',
|
||||
scan: 'Escanear',
|
||||
read: 'Ler',
|
||||
write: 'Escrever',
|
||||
pay: 'Pagar',
|
||||
memo: 'Memo',
|
||||
date: 'Data',
|
||||
path: 'Caminho',
|
||||
internal_memo: 'Memorando interno (opcional)',
|
||||
internal_memo_hint_receive:
|
||||
'Este memorando não é mostrado ao pagador, mas é armazenado na fatura para sua referência.',
|
||||
internal_memo_hint_pay:
|
||||
'Este memorando não é exibido ao beneficiário, mas é armazenado no pagamento para sua referência.',
|
||||
payment_processing: 'Processando pagamento...',
|
||||
payment_successful: 'Pagamento bem-sucedido!',
|
||||
payment_pending: 'Pagamento pendente...',
|
||||
payment_check: 'Cheque pagamento',
|
||||
not_enough_funds: 'Fundos insuficientes!',
|
||||
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
|
||||
search: 'Buscar',
|
||||
invoice_waiting: 'Fatura aguardando pagamento',
|
||||
payment_received: 'Pagamento Recebido',
|
||||
payment_sent: 'Pagamento Enviado',
|
||||
payment_failed: 'Pagamento Falhou',
|
||||
receive: 'receber',
|
||||
send: 'enviar',
|
||||
outgoing_payment_pending: 'Pagamento pendente de saída',
|
||||
@@ -112,7 +153,10 @@ window.localisation.br = {
|
||||
extensions: 'Extensões',
|
||||
no_extensions: 'Você não possui nenhuma extensão instalada :(',
|
||||
created: 'Criado',
|
||||
created_at: 'Criado em',
|
||||
updated_at: 'Atualizado em',
|
||||
search_extensions: 'Extensões de pesquisa',
|
||||
search_wallets: 'Pesquisar carteiras',
|
||||
extension_sources: 'Fontes de Extensão',
|
||||
ext_sources_hint: 'Repositórios de onde as extensões podem ser baixadas',
|
||||
ext_sources_label:
|
||||
@@ -121,10 +165,13 @@ window.localisation.br = {
|
||||
repository: 'Repositório',
|
||||
confirm_continue: 'Você tem certeza de que deseja continuar?',
|
||||
manage_extension_details: 'Instalar/desinstalar extensão',
|
||||
upload: 'Enviar',
|
||||
install: 'Instalar',
|
||||
uninstall: 'Desinstalar',
|
||||
drop_db: 'Remover Dados',
|
||||
enable: 'Ativar',
|
||||
enabled: 'Habilitado',
|
||||
disabled: 'Desativado',
|
||||
pay_to_enable: 'Pague para Habilitar',
|
||||
enable_extension_details: 'Ativar extensão para o usuário atual',
|
||||
disable: 'Desativar',
|
||||
@@ -132,6 +179,8 @@ window.localisation.br = {
|
||||
installed: 'Instalado',
|
||||
activated: 'Ativado',
|
||||
deactivated: 'Desativado',
|
||||
activate: 'Ativar',
|
||||
deactivate: 'Desativar',
|
||||
release_notes: 'Notas de Lançamento',
|
||||
activate_extension_details:
|
||||
'Tornar a extensão disponível/indisponível para usuários',
|
||||
@@ -139,8 +188,33 @@ window.localisation.br = {
|
||||
all: 'Tudo',
|
||||
only_admins_can_install:
|
||||
'Apenas contas de administrador podem instalar extensões.',
|
||||
only_admins_can_create_extensions:
|
||||
'Apenas contas de administrador podem criar extensões',
|
||||
admin_only: 'Apenas para Administração',
|
||||
make_user_admin: 'Tornar usuário administrador',
|
||||
revoke_admin: 'Revogar Admin',
|
||||
new_version: 'Nova Versão',
|
||||
reviews_url: 'URL de Avaliações',
|
||||
reviews_url_label: 'URL do servidor de avaliações',
|
||||
reviews_url_hint:
|
||||
'URL completa do PaidReviews incluindo o id das configurações (por exemplo, https://example.com/paidreviews/SETTINGS_ID)',
|
||||
reviews_open: 'Ver avaliações',
|
||||
reviews_leave: 'Deixe uma avaliação',
|
||||
reviews_name: 'Seu nome',
|
||||
reviews_comment: 'Sua avaliação',
|
||||
reviews_rating: 'Avaliação',
|
||||
reviews_submit: 'Enviar avaliação',
|
||||
reviews_loading: 'Carregando avaliações...',
|
||||
reviews_refresh: 'Atualizar avaliações',
|
||||
reviews_error_load: 'Não foi possível carregar as avaliações',
|
||||
reviews_url_not_configured: 'URL de avaliações não configurada',
|
||||
reviews_pay_invoice: 'Pagar fatura',
|
||||
reviews_invoice_paid: 'Fatura paga',
|
||||
reviews_invoice_title: 'Pague esta fatura para enviar sua avaliação',
|
||||
reviews_count: 'Avaliações',
|
||||
no_reviews: 'Ainda não há avaliações',
|
||||
extension_has_free_release: 'Tem lançamentos gratuitos',
|
||||
extension_has_paid_release: 'Tem lançamentos pagos',
|
||||
extension_depends_on: 'Depende de:',
|
||||
extension_rating_soon: 'Avaliações estarão disponíveis em breve',
|
||||
extension_installed_version: 'Versão instalada',
|
||||
@@ -155,26 +229,122 @@ window.localisation.br = {
|
||||
'Esta versão requer no mínimo a versão do LNbits',
|
||||
min_version: 'Mínimo (incluído)',
|
||||
max_version: 'Máximo (excluído)',
|
||||
preimage: 'Pré-imagem',
|
||||
preimage_hint: 'Pré-imagem para liquidar a fatura de retenção',
|
||||
hold_invoice: 'Reter Fatura',
|
||||
hold_invoice_description:
|
||||
'Esta fatura está em espera e requer uma pré-imagem para ser liquidada.',
|
||||
payment_hash: 'Hash de pagamento',
|
||||
invoice_cancelled: 'Fatura Cancelada',
|
||||
invoice_settled: 'Fatura Liquidada',
|
||||
hold_invoice_payment_hash:
|
||||
'Hash de pagamento para fatura em espera (opcional)',
|
||||
settle_invoice: 'Liquidar Fatura',
|
||||
cancel_invoice: 'Cancelar Fatura',
|
||||
fee: 'Taxa',
|
||||
amount: 'Quantidade',
|
||||
amount_limits: 'Limites de Quantia',
|
||||
amount_sats: 'Quantidade (sats)',
|
||||
faucest_wallet: 'Carteira de Torneira',
|
||||
faucest_wallet_desc_1:
|
||||
'Toda vez que um pagamento for confirmado pelo provedor {provider}, os fundos serão subtraídos desta carteira.',
|
||||
faucest_wallet_desc_2:
|
||||
'Isso ajuda a monitorar todos os pagamentos do {provider} e seu status.',
|
||||
faucest_wallet_desc_3:
|
||||
'Esta carteira deve ser recarregada com a quantia de sats que o administrador está disposto a oferecer em troca da moeda fiduciária.',
|
||||
faucest_wallet_desc_4:
|
||||
'Se esta carteira estiver configurada, mas estiver vazia, os pagamentos de {provider} não serão processados.',
|
||||
faucest_wallet_desc_5:
|
||||
'Esta carteira pode eventualmente ficar com saldo negativo se pagamentos fiduciários paralelos forem feitos.',
|
||||
faucest_wallet_id: 'ID da Carteira de Torneira (opcional)',
|
||||
faucest_wallet_id_hint:
|
||||
'ID da carteira a ser usado para a torneira. Será usado para enviar os fundos ao usuário.',
|
||||
tag: 'Etiqueta',
|
||||
unit: 'Unidade',
|
||||
description: 'Descrição',
|
||||
expiry: 'Validade',
|
||||
webhook: 'Webhook',
|
||||
webhook_url: 'URL do Webhook',
|
||||
webhook_url_hint:
|
||||
'URL de Webhook para enviar os detalhes do pagamento. Será chamada quando o pagamento for concluído.',
|
||||
copy_webhook_url: 'Copiar URL do webhook',
|
||||
webhook_events_list:
|
||||
'Os seguintes eventos devem ser suportados pelo webhook:',
|
||||
webhook_stripe_description:
|
||||
'No lado do Stripe, você deve configurar um webhook com um URL que aponta para o seu servidor LNbits.',
|
||||
payment_proof: 'Comprovante de pagamento',
|
||||
update: 'Atualizar',
|
||||
update_available: 'Atualização {version} disponível!',
|
||||
funding_sources: 'Fontes de Financiamento',
|
||||
latest_update: 'Você está na versão mais recente {version}.',
|
||||
notifications: 'Notificações',
|
||||
no_notifications: 'Sem notificações',
|
||||
notifications_disabled:
|
||||
'As notificações de status do LNbits estão desativadas.',
|
||||
enable_notifications: 'Ativar notificações',
|
||||
enable_notifications_desc:
|
||||
'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.',
|
||||
notifications_configure: 'Configurar Notificações',
|
||||
notifications_nostr_config: 'Configuração do Nostr',
|
||||
notifications_enable_nostr: 'Ativar Nostr',
|
||||
notifications_enable_nostr_desc: 'Enviar notificações pelo Nostr',
|
||||
notifications_nostr_private_key: 'Chave Privada Nostr',
|
||||
notifications_nostr_private_key_desc:
|
||||
'Chave privada (hex ou nsec) para assinar as mensagens enviadas para Nostr',
|
||||
notifications_nostr_identifier: 'Identificador Nostr',
|
||||
notifications_nostr_identifier_desc:
|
||||
'Identificador Nip5 para enviar notificações para',
|
||||
notifications_nostr_identifiers: 'Identificadores Nostr',
|
||||
notifications_nostr_identifiers_desc:
|
||||
'Lista de identificadores para enviar notificações.',
|
||||
notifications_telegram_config: 'Configuração do Telegram',
|
||||
notifications_enable_telegram: 'Ativar Telegram',
|
||||
notifications_enable_telegram_desc: 'Enviar notificações pelo Telegram',
|
||||
notifications_telegram_access_token: 'Token de Acesso',
|
||||
notifications_telegram_access_token_desc: 'Token de acesso para o bot',
|
||||
notifications_chat_id: 'ID de bate-papo do Telegram',
|
||||
notifications_chat_id_desc:
|
||||
'ID do chat do Telegram para enviar as notificações para',
|
||||
notifications_excluded_wallets_desc:
|
||||
'Não envie notificações para essas carteiras',
|
||||
notifications_email_config: 'Configuração de Email',
|
||||
notifications_enable_email: 'Habilitar Email',
|
||||
notifications_enable_email_desc: 'Enviar notificações por e-mail',
|
||||
notifications_send_test_email: 'Enviar e-mail de teste',
|
||||
notifications_send_email: 'Enviar e-mail',
|
||||
notifications_send_email_desc: 'Email que você enviará de',
|
||||
notifications_send_email_username: 'Nome de usuário',
|
||||
notifications_send_email_username_desc:
|
||||
'Nome de usuário, usará o e-mail se não estiver definido',
|
||||
notifications_send_email_password: 'Enviar senha de e-mail',
|
||||
notifications_send_email_password_desc:
|
||||
'Senha para o e-mail que você enviará de',
|
||||
notifications_send_email_server_port: 'Enviar e-mail porta SMTP',
|
||||
notifications_send_email_server_port_desc: 'Porta para o servidor SMTP',
|
||||
notifications_send_email_server: 'Enviar e-mail servidor SMTP',
|
||||
notifications_send_email_server_desc:
|
||||
'Servidor SMTP para o e-mail de que você enviará',
|
||||
notifications_send_to_emails: 'Emails para enviar para',
|
||||
notifications_send_to_emails_desc:
|
||||
'Notificações de e-mails serão enviadas para',
|
||||
notification_settings_update: 'Configurações atualizadas',
|
||||
notification_settings_update_desc:
|
||||
'Notificar quando as configurações do servidor forem atualizadas',
|
||||
notification_server_start_stop: 'Iniciar/Parar Servidor',
|
||||
notification_server_start_stop_desc:
|
||||
'Notificar quando o servidor tiver sido iniciado/parado',
|
||||
notification_watchdog_limit: 'Notificação de Limite do Watchdog',
|
||||
notification_watchdog_limit_desc:
|
||||
'Notifique quando o limite do watchdog for alcançado (não afeta a fonte de financiamento)',
|
||||
notification_server_status: 'Status do Servidor',
|
||||
notification_server_status_desc:
|
||||
'Enviar notificações regulares sobre o status do servidor (valor do intervalo em horas)',
|
||||
notification_incoming_payment: 'Pagamentos Recebidos',
|
||||
notification_incoming_payment_desc:
|
||||
'Notificar quando uma carteira tiver recebido um pagamento acima do valor especificado (sats)',
|
||||
notification_outgoing_payment: 'Pagamentos Saída',
|
||||
notification_outgoing_payment_desc:
|
||||
'Notificar quando uma carteira tiver enviado um pagamento acima do valor especificado (sats)',
|
||||
notification_credit_debit: 'Crédito / Débito',
|
||||
notification_credit_debit_desc:
|
||||
'Notificar quando uma carteira tiver sido creditada/debitada pelo superusuário',
|
||||
notification_balance_delta_changed: 'Mudança no Delta de Saldo',
|
||||
notification_balance_delta_changed_desc:
|
||||
'Notifique quando a diferença entre o saldo do nó e o saldo do LNbits tiver mudado mais do que a quantidade especificada (em sats). Defina como 0 para desativar. Isso é executado a cada minuto.',
|
||||
enable_watchdog: 'Ativar Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Se ativado, ele mudará automaticamente sua fonte de financiamento para VoidWallet se o seu saldo for inferior ao saldo do LNbits. Você precisará ativar manualmente após uma atualização.',
|
||||
@@ -189,6 +359,7 @@ window.localisation.br = {
|
||||
notification_source_label:
|
||||
'URL de origem (use apenas a fonte de status oficial do LNbits e fontes de confiança)',
|
||||
more: 'mais',
|
||||
more_count: 'Mais {count}',
|
||||
less: 'menos',
|
||||
releases: 'Lançamentos',
|
||||
watchdog: 'Cão de guarda',
|
||||
@@ -200,15 +371,24 @@ window.localisation.br = {
|
||||
allow_access_hint: 'Permitir acesso por IP (substituirá os IPs bloqueados)',
|
||||
enter_ip: 'Digite o IP e pressione enter',
|
||||
rate_limiter: 'Limitador de Taxa',
|
||||
callback_url_rules: 'Regras de URL de Retorno',
|
||||
enter_callback_url_rule: 'Digite a regra de URL como regex e pressione enter',
|
||||
callback_url_rule_hint:
|
||||
'URLs de retorno de chamada (como a de LNURL) serão validados conforme estas regras. Pelo menos uma regra deve corresponder. Nenhuma regra significa que todas as URLs são permitidas.',
|
||||
wallet_limiter: 'Limitador de Carteira',
|
||||
wallet_config: 'Configuração da Carteira',
|
||||
wallet_charts: 'Gráficos da Carteira',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Retirada máxima diária da carteira em sats (0 para desativar)',
|
||||
wallet_max_ballance: 'Saldo máximo da carteira em sats (0 para desativar)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minutos e segundos entre transações por carteira (0 para desativar)',
|
||||
only_incoming_payments_allowed: 'Apenas pagamentos recebidos são permitidos',
|
||||
disable_outgoing_payments: 'Desativar pagamentos de saída',
|
||||
number_of_requests: 'Número de solicitações',
|
||||
time_unit: 'Unidade de tempo',
|
||||
minute: 'minuto',
|
||||
settings: 'Configurações',
|
||||
second: 'segundo',
|
||||
hour: 'hora',
|
||||
disable_server_log: 'Desativar Log do Servidor',
|
||||
@@ -219,49 +399,108 @@ window.localisation.br = {
|
||||
login_with_user_id: 'Faça login com ID do usuário',
|
||||
or: 'ou',
|
||||
create_new_wallet: 'Criar Nova Carteira',
|
||||
delete_all_wallets: 'Excluir Todas as Carteiras',
|
||||
confirm_delete_all_wallets:
|
||||
'Tem certeza de que deseja excluir TODAS as carteiras deste usuário?',
|
||||
login_to_account: 'Faça login na sua conta',
|
||||
create_account: 'Criar conta',
|
||||
account_settings: 'Configurações da Conta',
|
||||
signin_with_oauth: 'Entrar com',
|
||||
signin_with_oauth_or: 'ou entre com',
|
||||
signin_with_nostr: 'Continuar com Nostr',
|
||||
signin_with_google: 'Entrar com o Google',
|
||||
signin_with_github: 'Entrar com GitHub',
|
||||
signin_with_keycloak: 'Entrar com Keycloak',
|
||||
signin_with_custom_org: 'Entrar com {custom_org}',
|
||||
username_or_email: 'Nome de usuário ou E-mail',
|
||||
password: 'Senha',
|
||||
password_config: 'Configuração de Senha',
|
||||
password_repeat: 'Repetição de senha',
|
||||
update_password: 'Atualizar Senha',
|
||||
change_password: 'Alterar Senha',
|
||||
update_credentials: 'Atualizar credenciais',
|
||||
update_pubkey: 'Atualizar Chave Pública',
|
||||
nostr_pubkey_tooltip:
|
||||
'Insira a chave pública Nostr deste usuário (valor hexadecimal)',
|
||||
set_password: 'Definir Senha',
|
||||
set_password_tooltip: 'Defina uma senha para este usuário',
|
||||
invalid_password: 'A senha deve ter pelo menos 8 caracteres',
|
||||
invalid_password_repeat: 'As senhas não coincidem',
|
||||
reset_key_generated: 'Uma chave de reinicialização foi gerada.',
|
||||
reset_key_copy:
|
||||
'Clique em OK para copiar o URL de redefinição para sua área de transferência.',
|
||||
login: 'Entrar',
|
||||
register: 'Registrar',
|
||||
username: 'Nome de usuário',
|
||||
pubkey: 'Chave Pública',
|
||||
user_id: 'ID do Usuário',
|
||||
id: 'ID',
|
||||
email: 'E-mail',
|
||||
first_name: 'Primeiro Nome',
|
||||
last_name: 'Sobrenome',
|
||||
picture: 'Foto',
|
||||
user_picture_desc:
|
||||
'URL para uma imagem a ser usada como foto de perfil. Você pode carregá-la como um ativo.',
|
||||
verify_email: 'Verifique o e-mail com',
|
||||
account: 'Conta',
|
||||
update_account: 'Atualizar Conta',
|
||||
invalid_username: 'Nome de usuário inválido',
|
||||
auth_provider: 'Provedor de Autenticação',
|
||||
external_id: 'ID Externo',
|
||||
my_account: 'Minha Conta',
|
||||
existing_account_question: 'Já tem uma conta?',
|
||||
background_image: 'Imagem de Fundo',
|
||||
back: 'Voltar',
|
||||
logout: 'Sair',
|
||||
look_and_feel: 'Aparência',
|
||||
endpoint: 'Ponto de extremidade',
|
||||
api: 'API',
|
||||
api_stripe: 'API',
|
||||
api_token: 'Token de API',
|
||||
api_tokens: 'Tokens da API',
|
||||
access_control_list: 'Lista de Controle de Acesso',
|
||||
access_control_list_admin_warning:
|
||||
'Esta é uma conta de administrador. Os tokens gerados terão privilégios de administrador.',
|
||||
new_api_acl: 'Nova Lista de Controle de Acesso',
|
||||
api_token_id: 'Id do Token',
|
||||
toggle_gradient: 'Alternar Gradiente',
|
||||
gradient_background: 'Fundo em Degradê',
|
||||
rounded_ui: 'Cartões e Botões Arredondados',
|
||||
toggle_rounded_ui: 'Alternar cantos arredondados para cartões e botões',
|
||||
card_gradient: 'Gradiente do Cartão',
|
||||
toggle_card_gradient: 'Alternar gradiente nos cartões',
|
||||
card_shadow: 'Sombra do Cartão',
|
||||
toggle_card_shadow: 'Alternar sombra nas cartas',
|
||||
language: 'Idioma',
|
||||
assets: 'Ativos',
|
||||
max_asset_size_mb: 'Tamanho Máximo do Ativo (MB)',
|
||||
max_asset_size_mb_desc:
|
||||
'O tamanho máximo permitido para uploads de ativos em megabytes (pode usar valores decimais).',
|
||||
assets_allowed_mime_types: 'Tipos MIME permitidos',
|
||||
assets_allowed_mime_types_desc:
|
||||
'Os tipos MIME permitidos para uploads de ativos. Nenhum valor significa que todos os uploads são permitidos.',
|
||||
thumbnail_width: 'Largura da Miniatura',
|
||||
thumbnail_width_desc: 'Largura da miniatura gerada em pixels.',
|
||||
thumbnail_height: 'Altura da miniatura',
|
||||
thumbnail_height_desc: 'Altura da miniatura gerada em pixels.',
|
||||
thumbnail_format: 'Formato da Miniatura',
|
||||
thumbnail_format_desc:
|
||||
'Formato de imagem da miniatura gerada (PNG, JPEG, etc.).',
|
||||
max_assets_per_user: 'Máximo de ativos por usuário',
|
||||
max_assets_per_user_desc:
|
||||
'O número máximo de ativos que um usuário pode fazer upload. Zero significa que o upload está proibido.',
|
||||
assets_no_limit_users: 'Usuários sem Limites de Ativos',
|
||||
assets_no_limit_users_desc:
|
||||
'Esses usuários podem enviar um número ilimitado de ativos (com base no ID do usuário).',
|
||||
color_scheme: 'Esquema de Cores',
|
||||
visible_wallet_count: 'Contagem de Carteiras Visíveis',
|
||||
admin_settings: 'Configurações do Administrador',
|
||||
extension_cost: 'Este lançamento requer um pagamento mínimo de {cost} sats.',
|
||||
extension_paid_sats: 'Você já pagou {paid_sats} sats.',
|
||||
create_extension: 'Criar Extensão',
|
||||
release_details_error: 'Não é possível obter os detalhes da versão.',
|
||||
pay_from_wallet: 'Pagar com a Carteira',
|
||||
pay_with: 'Pague com {provider}',
|
||||
select_payment_provider: 'Selecione o provedor de pagamento',
|
||||
wallet_required: 'Carteira *',
|
||||
show_qr: 'Exibir QR',
|
||||
retry_install: 'Repetir Instalação',
|
||||
@@ -274,6 +513,9 @@ window.localisation.br = {
|
||||
'A extensão {name} requer um pagamento mínimo de {amount} sats para habilitar.',
|
||||
hide_empty_wallets: 'Ocultar carteiras vazias',
|
||||
recheck: 'Verificar novamente',
|
||||
check: 'Verificar',
|
||||
check_connection: 'Verificar Conexão',
|
||||
check_webhook: 'Verificar Webhook',
|
||||
contributors: 'Contribuidores',
|
||||
license: 'Licença',
|
||||
reset_key: 'Redefinir Chave',
|
||||
@@ -286,6 +528,7 @@ window.localisation.br = {
|
||||
hash: 'Hash:',
|
||||
welcome_lnbits: 'Bem-vindo ao LNbits',
|
||||
setup_su_account: 'Configure a conta Superuser abaixo.',
|
||||
first_install_token: 'Primeiro Token de Instalação',
|
||||
create_ticker_converter: 'Criar Conversor de Ticker de Moeda',
|
||||
enable_audit: 'Habilitar Auditoria',
|
||||
recommended: 'Recomendado',
|
||||
@@ -326,9 +569,16 @@ window.localisation.br = {
|
||||
user_default_extensions_label: 'Extensões do usuário',
|
||||
user_default_extensions_hint:
|
||||
'Extensões que serão ativadas por padrão para os usuários.',
|
||||
extension_builder: 'Construtor de Extensão',
|
||||
extension_builder_manifest_url: 'URL do Manifesto do Criador de Extensões',
|
||||
extension_builder_manifest_url_hint:
|
||||
'URL para um arquivo JSON manifest com detalhes do extension builder',
|
||||
miscellanous: 'Diversos',
|
||||
misc_disable_extensions: 'Desativar extensões',
|
||||
misc_disable_extensions_label: 'Desativar todas as extensões',
|
||||
misc_disable_extensions_builder: 'Habilitar Extensions Builder',
|
||||
misc_disable_extensions_builder_label:
|
||||
'Habilitar Extensions Builder para usuários não administradores.',
|
||||
misc_hide_api: 'Ocultar API',
|
||||
misc_hide_api_label:
|
||||
'Oculta a API de carteira, extensões podem optar por honrar',
|
||||
@@ -346,16 +596,33 @@ window.localisation.br = {
|
||||
toggle_transactions_node_ui:
|
||||
'Guia de Transações (Desativar em nós grandes CLN)',
|
||||
invoice_expiry: 'Expiração da Fatura',
|
||||
invoice_expiry_label: 'Validade da fatura (segundos)',
|
||||
fee_reserve: 'Reserva de Taxa',
|
||||
fee_reserve_msats: 'Taxa de reserva em msats',
|
||||
fee_reserve_percent: 'Taxa de reserva em porcentagem',
|
||||
invoice_expiry_label: 'Validade da fatura',
|
||||
routing_fee_reserve_calculations: 'Cálculos de Reserva de Taxa de Roteamento',
|
||||
routing_fee_reserve_calculations_desc:
|
||||
'LNbits reserva um "valor de reserva" para cada pagamento para cobrir as taxas de roteamento. A taxa de roteamento máxima passada para a fonte de financiamento é a que for maior: a <strong>reserva mínima de taxa de roteamento</strong> ou a <strong>percentagem de reserva de taxa de roteamento</strong>.',
|
||||
millisats: 'milissats',
|
||||
fee_reserve: 'Reserva Mínima de Taxa de Encaminhamento',
|
||||
fee_reserve_percent: 'Porcentagem da Reserva de Taxa de Roteamento',
|
||||
fee_reserve_min_hint:
|
||||
'A taxa mínima reservada por pagamento.<br>Isso atua como um piso - a taxa de roteamento máxima nunca será inferior a este valor, independentemente do tamanho do pagamento.',
|
||||
fee_reserve_percent_hint:
|
||||
'A porcentagem do valor do pagamento a reservar para taxas de roteamento.',
|
||||
payment_timeouts: 'Tempos de Espera de Pagamento',
|
||||
payment_wait_time: 'Tempo de Espera do Pagamento',
|
||||
seconds: 'segundos',
|
||||
payment_wait_time_desc:
|
||||
'Tempo de espera antes de marcar um pagamento de saída como pendente. Padrão: 5s; aumentar para faturas de liquidação lenta.',
|
||||
payment_wait_time_tooltip:
|
||||
'Controla quanto tempo o LNbits espera para uma tentativa de pagamento de saída ser confirmada antes de marcá-la como pendente. Valores mais altos ajudam ao pagar faturas de liquidação lenta (por exemplo, faturas HODL, Boltz). O pagamento será verificado novamente mais tarde e atualizado automaticamente ou manualmente.',
|
||||
server_management: 'Gerenciamento de Servidor',
|
||||
base_url: 'URL base',
|
||||
base_url_label: 'URL estática/base para o servidor',
|
||||
authentication: 'Autenticação',
|
||||
auth_token_expiry_label: 'Minutos para expiração do token',
|
||||
auth_token_expiry_hint: 'Tempo em minutos até o token expirar',
|
||||
auth_authentication_cache_label: 'Tempo de cache (minutos)',
|
||||
auth_authentication_cache_hint:
|
||||
'Tempo em minutos para armazenar em cache a autenticação bem-sucedida (0 para desativar)',
|
||||
auth_allowed_methods_label: 'Métodos de autorização permitidos',
|
||||
auth_allowed_methods_hint: 'Selecione métodos de autorização',
|
||||
auth_nostr_label: 'URL de Solicitação Nostr',
|
||||
@@ -373,13 +640,36 @@ window.localisation.br = {
|
||||
auth_keycloak_ci_hint:
|
||||
'Certifique-se de que a URL de retorno de chamada de autorização esteja definida para https://{domain}/api/v1/auth/keycloak/token',
|
||||
auth_keycloak_cs_label: 'Segredo do Cliente Keycloak',
|
||||
auth_keycloak_custom_org_label: 'Organização Personalizada do Keycloak',
|
||||
auth_keycloak_custom_icon_label: 'Ícone Personalizado do Keycloak (URL)',
|
||||
auth_oidc_label: 'URL de Descoberta do OIDC',
|
||||
auth_oidc_ci_label: 'ID do Cliente OIDC',
|
||||
auth_oidc_ci_hint:
|
||||
'Certifique-se de que a URL de retorno de chamada de autorização esteja definida para https://{domain}/api/v1/auth/oidc/token',
|
||||
auth_oidc_cs_label: 'Segredo do Cliente OIDC',
|
||||
auth_oidc_custom_org_label:
|
||||
'Nome da Organização Personalizada OIDC (ex. Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'Ícone Personalizado do OIDC (URL)',
|
||||
auth_keycloak_custom_org_label: 'Keycloak Custom Organization',
|
||||
auth_keycloak_custom_icon_label: 'Ícone Personalizado do Keycloak (URL)',
|
||||
currency_settings: 'Configurações de Moeda',
|
||||
allowed_currencies: 'Moedas Permitidas',
|
||||
allowed_currencies_hint: 'Limite o número de moedas fiduciárias disponíveis',
|
||||
default_account_currency: 'Moeda Padrão da Conta',
|
||||
default_account_currency_hint: 'Moeda padrão para contabilidade',
|
||||
min_incoming_payment_amount: 'Quantia Mínima de Pagamento de Entrada',
|
||||
min_incoming_payment_amount_desc:
|
||||
'Quantidade mínima permitida para gerar uma fatura',
|
||||
max_incoming_payment_amount: 'Valor Máximo do Pagamento Recebido',
|
||||
max_incoming_payment_amount_desc:
|
||||
'Quantidade máxima permitida para gerar uma fatura',
|
||||
max_outgoing_payment_amount: 'Valor Máximo de Pagamento de Saída',
|
||||
max_outgoing_payment_amount_desc:
|
||||
'Valor máximo permitido para efetuar um pagamento',
|
||||
service_fee: 'Taxa de serviço: {amount} % por transação',
|
||||
service_fee_label: 'Taxa de serviço (%)',
|
||||
service_fee_hint: 'Taxa cobrada por tx (%)',
|
||||
service_fee_max: 'Taxa de serviço: {amount} % por transação (máx {max} sats)',
|
||||
service_fee_max_label: 'Taxa de serviço máx (sats)',
|
||||
service_fee_max_hint: 'Taxa máxima de serviço a cobrar em (sats)',
|
||||
fee_wallet: 'Carteira de Taxas',
|
||||
@@ -391,6 +681,8 @@ window.localisation.br = {
|
||||
'Desativar Taxa de Serviço para Pagamentos Internos Lightning',
|
||||
ui_management: 'Gerenciamento de UI',
|
||||
ui_site_title: 'Título do Site',
|
||||
ui_changing_remove_lnbits_elements:
|
||||
'(alterar removerá os elementos LNbits na página inicial e rodapé)',
|
||||
ui_site_tagline: 'Tagline do site',
|
||||
ui_elements_enable: 'Habilitar elementos na página inicial',
|
||||
ui_elements_disable: 'Desativar elementos na página inicial',
|
||||
@@ -399,11 +691,27 @@ window.localisation.br = {
|
||||
ui_site_description: 'Descrição do Site',
|
||||
ui_site_description_hint: 'Use texto simples, Markdown ou HTML bruto',
|
||||
ui_default_wallet_name: 'Nome Padrão da Carteira',
|
||||
ui_default_theme: 'Tema Padrão',
|
||||
wallet_featured_button_label: 'Carteira Rótulo do Botão em Destaque',
|
||||
wallet_featured_button_label_hint:
|
||||
'Mostrar botão em destaque na página inicial da carteira',
|
||||
wallet_featured_button_url: 'URL do Botão em Destaque',
|
||||
wallet_featured_button_url_hint:
|
||||
'Ao clicar, o botão abrirá este URL. Deixe em branco para ocultar o botão.',
|
||||
wallet_featured_button_icon: 'Ícone do Botão em Destaque',
|
||||
wallet_featured_button_icon_hint:
|
||||
'Ícone mostrado no botão de destaque (verifique os ícones quasar)',
|
||||
lnbits_wallet: 'Carteira LNbits',
|
||||
denomination: 'Denominação',
|
||||
denomination_hint: 'O nome para o token FakeWallet',
|
||||
denomination_error: 'A denominação deve ter 3 caracteres ou `sats`.',
|
||||
ui_qr_code_logo: 'Logo do QR Code',
|
||||
ui_qr_code_logo_hint: 'URL para imagem de logo no código QR',
|
||||
ui_apple_touch_icon: 'Ícone de Toque da Apple',
|
||||
ui_apple_touch_icon_hint: 'URL do ícone de toque da Apple',
|
||||
ui_custom_image: 'Imagem Personalizada',
|
||||
ui_custom_image_label: 'URL para imagem personalizada',
|
||||
ui_custom_image_hint: 'Imagem exibida na página inicial/login',
|
||||
ui_custom_badge: 'Distintivo Personalizado',
|
||||
ui_custom_badge_label:
|
||||
"Distintivo Personalizado 'USE COM CUIDADO - a carteira LNbits ainda está em BETA'",
|
||||
@@ -426,14 +734,95 @@ window.localisation.br = {
|
||||
admin_users_label: 'ID do Usuário',
|
||||
allowed_users: 'Usuários Permitidos',
|
||||
allowed_users_hint: 'Somente esses usuários podem usar o LNbits',
|
||||
allowed_users_hint_feature: 'Somente estes usuários podem usar {feature}',
|
||||
allowed_users_label: 'ID do Usuário',
|
||||
allow_creation_user: 'Permitir a criação de novos usuários',
|
||||
allow_creation_user_desc:
|
||||
'Permitir a criação de novos usuários na página de índice',
|
||||
require_user_activation: 'Requer ativação do usuário',
|
||||
require_user_activation_desc:
|
||||
'Novos usuários serão ativados somente após passarem por um dos métodos de confirmação. Administradores podem ativar usuários manualmente a partir do painel de administração.',
|
||||
reusable_activation_code: 'Código de ativação reutilizável',
|
||||
reusable_activation_code_label: 'Código de ativação reutilizável',
|
||||
reusable_activation_code_hint:
|
||||
'Este código de ativação pode ser usado várias vezes por diferentes usuários.',
|
||||
one_time_activation_code: 'Códigos de ativação única',
|
||||
one_time_activation_code_label: 'Adicionar código de ativação',
|
||||
one_time_activation_code_hint:
|
||||
'Lista de códigos de ativação únicos. Cada código pode ser usado apenas uma vez, depois será removido da lista.',
|
||||
invitation_code: 'Código de Convite',
|
||||
invitation_code_hint: 'O código de convite que você recebeu.',
|
||||
email_confirmation_hint:
|
||||
'Endereço de e-mail para enviar o código de confirmação.',
|
||||
nostr_identifier: 'Identificador Nostr',
|
||||
nostr_identifier_hint:
|
||||
'Identificador nostr nip5 ou <npub> para enviar o código de confirmação.',
|
||||
new_user_not_allowed: 'O registro está desativado.',
|
||||
start_user_impersonation: 'Personificar este usuário',
|
||||
stop_user_impersonation: 'Parar a Personificação do Usuário',
|
||||
components: 'Componentes',
|
||||
long_running_endpoints: 'Top 5 Endpoints de Longa Execução',
|
||||
http_request_methods: 'Métodos de Requisição HTTP',
|
||||
http_response_codes: 'Códigos de Resposta HTTP',
|
||||
request_details: 'Detalhes do Pedido',
|
||||
http_request_details: 'Detalhes da Requisição HTTP'
|
||||
http_request_details: 'Detalhes da Requisição HTTP',
|
||||
payment_details: 'Detalhes do Pagamento',
|
||||
payment_details_desc: 'Informações detalhadas sobre o pagamento',
|
||||
payments: 'Pagamentos',
|
||||
payment_show_internal: 'Mostrar Pagamentos Internos',
|
||||
payment_chart_flow: 'Fluxo de Pagamento Mensal',
|
||||
payment_chart_status: 'Status do Pagamento',
|
||||
payment_chart_tx_per_wallet: 'Transações por Carteira (saldo/contagem)',
|
||||
payment_details_back: 'Voltar para Pagamentos',
|
||||
payment_chart_tags: 'Pagamentos por Tags',
|
||||
payments_balance_in_out: 'Entradas/Saídas de Saldo',
|
||||
payments_count_in_out: 'Contar Entrada/Saída',
|
||||
payments_status_chart: 'Gráfico de Status',
|
||||
payments_tag_chart: 'Gráfico de Marcadores',
|
||||
payments_balance_chart: 'Gráfico de Saldo',
|
||||
payments_wallets_chart: 'Gráfico de Carteiras',
|
||||
payments_balance_in_out_chart: 'Gráfico de Entrada/Saída de Saldo',
|
||||
payments_count_in_out_chart: 'Gráfico de Entrada/Saída',
|
||||
reset_wallet_keys: 'Redefinir Chaves',
|
||||
reset_wallet_keys_desc:
|
||||
'Redefina as chaves de API para esta carteira. Isso invalidará as chaves atuais e gerará novas.',
|
||||
view_list: 'Visualizar carteiras como lista',
|
||||
view_column: 'Visualizar carteiras como linhas',
|
||||
filter_payments: 'Filtrar pagamentos',
|
||||
filter_labels: 'Rótulos de filtro',
|
||||
filter_date: 'Filtrar por data',
|
||||
websocket_example: 'Exemplo de Websocket',
|
||||
client_id: 'ID do Cliente',
|
||||
secret_key: 'Chave Secreta',
|
||||
signing_secret: 'Segredo de Assinatura',
|
||||
signing_secret_hint:
|
||||
'Segredo de assinatura para o webhook. As mensagens serão assinadas com este segredo.',
|
||||
webhook_id: 'ID do Webhook',
|
||||
webhook_id_hint:
|
||||
'ID do webhook do PayPal usado para verificar eventos recebidos.',
|
||||
webhook_paypal_description:
|
||||
'No lado do PayPal, configure um webhook apontando para o seu servidor LNbits.',
|
||||
callback_success_url: 'URL de Sucesso de Callback',
|
||||
callback_success_url_hint:
|
||||
'O usuário será redirecionado para este URL após o pagamento ser bem-sucedido.',
|
||||
connected: 'Conectado',
|
||||
not_connected: 'Não Conectado',
|
||||
free: 'Grátis',
|
||||
paid: 'Pago',
|
||||
funding_source_retries: 'Máximo de tentativas',
|
||||
funding_source_retries_desc:
|
||||
'Número máximo de tentativas para fontes de financiamento, antes de voltar para VoidWallet.',
|
||||
add_label: 'Adicionar Rótulo',
|
||||
label: 'Etiqueta',
|
||||
labels: 'Rótulos',
|
||||
label_filter: 'Filtro de Rótulo',
|
||||
no_labels_defined: 'Ainda não há rótulos definidos',
|
||||
manage_labels: 'Gerenciar Etiquetas',
|
||||
update_label: 'Atualizar Rótulo',
|
||||
delete_label: 'Excluir Rótulo',
|
||||
add_remove_labels: 'Adicionar ou Remover Rótulos',
|
||||
payment_labels_updated: 'Rótulos de pagamento atualizados',
|
||||
color: 'Cor',
|
||||
sort: 'Ordenar',
|
||||
sort_by: 'Ordenar por'
|
||||
}
|
||||
|
||||
@@ -353,6 +353,15 @@ window.localisation.cn = {
|
||||
auth_keycloak_ci_hint:
|
||||
'确保授权回调URL设置为https://{domain}/api/v1/auth/keycloak/token',
|
||||
auth_keycloak_cs_label: 'Keycloak客户端密钥',
|
||||
auth_keycloak_custom_org_label: 'Keycloak 自定义组织',
|
||||
auth_keycloak_custom_icon_label: 'Keycloak 自定义图标 (URL)',
|
||||
auth_oidc_label: 'OIDC 发现 URL',
|
||||
auth_oidc_ci_label: 'OIDC 客户端 ID',
|
||||
auth_oidc_ci_hint:
|
||||
'确保授权回调URL设置为https://{domain}/api/v1/auth/oidc/token',
|
||||
auth_oidc_cs_label: 'OIDC客户端密钥',
|
||||
auth_oidc_custom_org_label: 'OIDC 自定义组织名称(例如 Zitadel、Authentik)',
|
||||
auth_oidc_custom_icon_label: 'OIDC 自定义图标 (URL)',
|
||||
currency_settings: '货币设置',
|
||||
allowed_currencies: '允许的货币',
|
||||
allowed_currencies_hint: '限制可用法定货币的数量',
|
||||
|
||||
@@ -367,6 +367,16 @@ window.localisation.cs = {
|
||||
auth_keycloak_ci_hint:
|
||||
'Ujistěte se, že je autorizace callback URL nastavena na https://{domain}/api/v1/auth/keycloak/token',
|
||||
auth_keycloak_cs_label: 'Klíč k aplikaci Keycloak tajemství',
|
||||
auth_keycloak_custom_org_label: 'Vlastní organizace Keycloak',
|
||||
auth_keycloak_custom_icon_label: 'Vlastní ikona Keycloak (URL)',
|
||||
auth_oidc_label: 'URL pro zjištění OIDC',
|
||||
auth_oidc_ci_label: 'ID klienta OIDC',
|
||||
auth_oidc_ci_hint:
|
||||
'Ujistěte se, že je autorizace callback URL nastavena na https://{domain}/api/v1/auth/oidc/token',
|
||||
auth_oidc_cs_label: 'Klíč k aplikaci OIDC tajemství',
|
||||
auth_oidc_custom_org_label:
|
||||
'Název vlastní organizace OIDC (např. Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'Vlastní ikona OIDC (URL)',
|
||||
currency_settings: 'Nastavení měny',
|
||||
allowed_currencies: 'Povolené měny',
|
||||
allowed_currencies_hint: 'Omezte počet dostupných fiat měn',
|
||||
|
||||
@@ -377,6 +377,16 @@ window.localisation.de = {
|
||||
auth_keycloak_ci_hint:
|
||||
'Stellen Sie sicher, dass die Autorisierungs-Callback-URL auf https://{domain}/api/v1/auth/keycloak/token eingestellt ist.',
|
||||
auth_keycloak_cs_label: 'Keycloak-Client-Geheimnis',
|
||||
auth_keycloak_custom_org_label: 'Keycloak Benutzerdefinierte Organisation',
|
||||
auth_keycloak_custom_icon_label: 'Keycloak Benutzerdefiniertes Symbol (URL)',
|
||||
auth_oidc_label: 'OIDC Discovery-URL',
|
||||
auth_oidc_ci_label: 'OIDC-Client-ID',
|
||||
auth_oidc_ci_hint:
|
||||
'Stellen Sie sicher, dass die Autorisierungs-Callback-URL auf https://{domain}/api/v1/auth/oidc/token eingestellt ist.',
|
||||
auth_oidc_cs_label: 'OIDC-Client-Geheimnis',
|
||||
auth_oidc_custom_org_label:
|
||||
'OIDC Benutzerdefinierter Organisationsname (z.B. Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'OIDC Benutzerdefiniertes Symbol (URL)',
|
||||
currency_settings: 'Währungseinstellungen',
|
||||
allowed_currencies: 'Erlaubte Währungen',
|
||||
allowed_currencies_hint:
|
||||
|
||||
@@ -6,7 +6,7 @@ window.localisation.en = {
|
||||
funding: 'Funding',
|
||||
users: 'Users',
|
||||
audit: 'Audit',
|
||||
api_watch: 'Api Watch',
|
||||
api_watch: 'API Watch',
|
||||
apps: 'Apps',
|
||||
channels: 'Channels',
|
||||
transactions: 'Transactions',
|
||||
@@ -271,12 +271,16 @@ window.localisation.en = {
|
||||
update: 'Update',
|
||||
update_available: 'Update {version} available!',
|
||||
funding_sources: 'Funding Sources',
|
||||
funding_source: 'Funding Source',
|
||||
requires_server_restart:
|
||||
'Changing these settings requires a server restart to take effect.',
|
||||
funding_source_info: 'Select the active funding wallet',
|
||||
latest_update: 'You are on the latest version {version}.',
|
||||
notifications: 'Notifications',
|
||||
notifications_configure: 'Configure Notifications',
|
||||
notifications_nostr_config: 'Nostr Configuration',
|
||||
notifications_enable_nostr: 'Enable Nostr',
|
||||
notifications_enable_nostr_desc: 'Send notfications over Nostr',
|
||||
notifications_enable_nostr_desc: 'Send notifications over Nostr',
|
||||
notifications_nostr_private_key: 'Nostr Private Key',
|
||||
notifications_nostr_private_key_desc:
|
||||
'Private key (hex or nsec) to sign the messages sent to Nostr',
|
||||
@@ -289,7 +293,7 @@ window.localisation.en = {
|
||||
|
||||
notifications_telegram_config: 'Telegram Configuration',
|
||||
notifications_enable_telegram: 'Enable Telegram',
|
||||
notifications_enable_telegram_desc: 'Send notfications over Telegram',
|
||||
notifications_enable_telegram_desc: 'Send notifications over Telegram',
|
||||
notifications_telegram_access_token: 'Access Token',
|
||||
notifications_telegram_access_token_desc: 'Access token for the bot',
|
||||
notifications_chat_id: 'Telegram Chat ID',
|
||||
@@ -318,15 +322,15 @@ window.localisation.en = {
|
||||
|
||||
notification_settings_update: 'Settings updated',
|
||||
notification_settings_update_desc:
|
||||
'Notify when server settings have been updated',
|
||||
'Send a notification when server settings have been updated',
|
||||
|
||||
notification_server_start_stop: 'Server Start/Stop',
|
||||
notification_server_start_stop_desc:
|
||||
'Notify when the server has been started/stopped',
|
||||
'Send a notification when the server has been started/stopped',
|
||||
|
||||
notification_watchdog_limit: 'Watchdog Limit Notification',
|
||||
notification_watchdog_limit_desc:
|
||||
'Notify when the watchdog limit has been reached (does not affect the funding source)',
|
||||
'Send a notification when the watchdog limit has been reached (does not affect the funding source)',
|
||||
|
||||
notification_server_status: 'Server Status',
|
||||
notification_server_status_desc:
|
||||
@@ -334,29 +338,31 @@ window.localisation.en = {
|
||||
|
||||
notification_incoming_payment: 'Incoming Payments',
|
||||
notification_incoming_payment_desc:
|
||||
'Notify when a wallet has received a payment above the specified amount (sats)',
|
||||
'Send a notification when a wallet has received a payment above the specified amount (sats)',
|
||||
|
||||
notification_outgoing_payment: 'Outgoing Payments',
|
||||
notification_outgoing_payment_desc:
|
||||
'Notify when a wallet has sent a payment above the specified amount (sats)',
|
||||
'Send a notification when a wallet has sent a payment above the specified amount (sats)',
|
||||
|
||||
notification_credit_debit: 'Credit / Debit',
|
||||
notification_credit_debit_desc:
|
||||
'Notify when a wallet has been credited/debited by the superuser',
|
||||
'Send a notification when a wallet has been credited/debited by the superuser',
|
||||
|
||||
notification_balance_delta_changed: 'Balance Delta Changed',
|
||||
notification_balance_delta_changed_desc:
|
||||
'Notify when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
|
||||
'Send a notification when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
|
||||
|
||||
enable_watchdog: 'Enable Watchdog Switch',
|
||||
watchdog_introduction:
|
||||
"Watchdog is a feature that allows you to automatically switch the LNbits funding source to VoidWallet if your node balance is lower than the LNbits balance by a certain threshold. This can help prevent overspending and keep your node's funds safe.",
|
||||
enable_watchdog: 'Enable Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
|
||||
watchdog_interval: 'Watchdog Interval',
|
||||
'You will need to re-enable this manually after an update.',
|
||||
watchdog_interval: 'Watchdog Check Interval',
|
||||
watchdog_interval_desc:
|
||||
'How often the background task should check for a killswitch signal in the watchdog delta [node_balance - lnbits_balance] (in minutes).',
|
||||
watchdog_delta: 'Watchdog Delta',
|
||||
'How often LNbits should check for a killswitch signal in the watchdog threshold delta value [node_balance - lnbits_balance] (in minutes).',
|
||||
watchdog_delta: 'Watchdog Threshold Delta',
|
||||
watchdog_delta_desc:
|
||||
'Limit before killswitch changes funding source to VoidWallet [lnbits_balance - node_balance > delta]',
|
||||
"The LNbit's > Node balance delta threshold. If this threshold is exceeded, the funding source is changed to VoidWallet.",
|
||||
status: 'Status',
|
||||
notification_source: 'Notification Source',
|
||||
notification_source_label:
|
||||
@@ -367,12 +373,12 @@ window.localisation.en = {
|
||||
releases: 'Releases',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Server Logs',
|
||||
ip_blocker: 'IP Blocker',
|
||||
ip_blocker: 'IP Blacklist/Whitelist',
|
||||
security: 'Security',
|
||||
security_tools: 'Security tools',
|
||||
security_tools: 'Security Tools',
|
||||
block_access_hint: 'Block access by IP',
|
||||
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
|
||||
enter_ip: 'Enter IP and hit enter',
|
||||
enter_ip: 'Enter an IP address and press enter',
|
||||
rate_limiter: 'Rate Limiter',
|
||||
callback_url_rules: 'Callback URL Rules',
|
||||
enter_callback_url_rule: 'Enter URL rule as regex and hit enter',
|
||||
@@ -386,14 +392,16 @@ window.localisation.en = {
|
||||
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Min secs between transactions per wallet (0 to disable)',
|
||||
only_incoming_payments_allowed: 'Only incoming payments allowed',
|
||||
only_incoming_payments_allowed: 'Allow incoming payments only',
|
||||
disable_outgoing_payments: 'Disable outgoing payments',
|
||||
number_of_requests: 'Number of requests',
|
||||
number_of_requests: 'Number of requests to allow',
|
||||
number_of_requests_hint:
|
||||
'Number of requests to allow per "time unit" for the rate limiter. Set to 0 to disable.',
|
||||
time_unit: 'Time unit',
|
||||
minute: 'minute',
|
||||
minute: 'Minute',
|
||||
settings: 'Settings',
|
||||
second: 'second',
|
||||
hour: 'hour',
|
||||
second: 'Second',
|
||||
hour: 'Hour',
|
||||
disable_server_log: 'Disable Server Log',
|
||||
enable_server_log: 'Enable Server Log',
|
||||
coming_soon: 'Feature coming soon',
|
||||
@@ -533,33 +541,30 @@ window.localisation.en = {
|
||||
create_ticker_converter: 'Create Currency Ticker Converter',
|
||||
enable_audit: 'Enable Audit',
|
||||
recommended: 'Recommended',
|
||||
audit_desc: 'Record HTTP requests according with the specified filters',
|
||||
audit_record_req: 'Record Request Body',
|
||||
audit_record_warning: 'Warning: ',
|
||||
audit_record_req_warning_1:
|
||||
'confidential data (like passwords) will be logged.',
|
||||
audit_record_req_warning_2: 'the request body can have large size.',
|
||||
audit_record_use: 'Use it with caution.',
|
||||
audit_ip: 'Record IP Address',
|
||||
audit_ip_desc: 'Record the IP address of the client',
|
||||
audit_path_params: 'Record Path Parameters',
|
||||
audit_query_params: 'Record Query Parameters',
|
||||
audit_desc: 'Log HTTP requests according to the filters specified below.',
|
||||
audit_record_req: 'Log Request Body',
|
||||
audit_record_warning: 'Warning!',
|
||||
audit_record_req_warning_1: 'Sensitive data (like passwords) will be logged.',
|
||||
audit_record_req_warning_2:
|
||||
'The request body can be large. This can fill up your logs quickly.',
|
||||
audit_record_use: 'Use this with caution!',
|
||||
audit_ip: 'Log IP Address',
|
||||
audit_ip_desc: 'Log the IP address of users making requests to LNbits.',
|
||||
audit_path_params: 'Log Path Parameters',
|
||||
audit_query_params: 'Log Query Parameters',
|
||||
audit_http_methods: 'Include HTTP Methods',
|
||||
audit_http_methods_hint:
|
||||
'List of HTTP methods to be included. Empty lists means all.',
|
||||
audit_http_methods_label: 'HTTP Methods',
|
||||
audit_resp_codes: 'Include HTTP Response Codes',
|
||||
'List of HTTP methods to be logged. No value means all methods will be logged.',
|
||||
audit_http_methods_label: 'HTTP Methods to Log',
|
||||
audit_resp_codes_hint:
|
||||
'List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*',
|
||||
audit_resp_codes_label: 'HTTP Response code (regex)',
|
||||
audit_paths: 'Include Paths',
|
||||
audit_resp_codes_label: 'HTTP Response Codes to Log (regex)',
|
||||
audit_paths_hint:
|
||||
'List of paths to be included (regex match). Empty list means all.',
|
||||
audit_paths_label: 'HTTP Path (regex)',
|
||||
audit_paths_exclude: 'Exclude Paths',
|
||||
audit_paths_label: 'HTTP Paths to Log (regex)',
|
||||
audit_paths_exclude_hint:
|
||||
'List of paths to be excluded (regex match). Empty list means none.',
|
||||
audit_paths_exclude_label: 'HTTP Path (regex)',
|
||||
audit_paths_exclude_label: 'HTTP Paths to Exclude from Logging (regex)',
|
||||
exchange_providers: 'Exchange Providers',
|
||||
admin_extensions: 'Admin Extensions',
|
||||
admin_extensions_label: 'Admin extensions',
|
||||
@@ -579,41 +584,51 @@ window.localisation.en = {
|
||||
misc_disable_extensions_builder_label:
|
||||
'Enable Extensions Builder for non admin users.',
|
||||
misc_hide_api: 'Hide API',
|
||||
misc_hide_api_label: 'Hides wallet api, extensions can choose to honor',
|
||||
misc_hide_api_label: 'Hides wallet API, extensions can choose to honor',
|
||||
wallets_management: 'Wallets Management',
|
||||
funding_source_info: 'Funding Source Info',
|
||||
funding_source: 'Funding Source: {wallet_class}',
|
||||
node_balance: 'Node Balance: {balance} sats',
|
||||
lnbits_balance: 'LNbits Balance: {balance} sats',
|
||||
funding_reserve_percent: 'Reserve Percent: {percent} %',
|
||||
funding_source_info: 'Funding Source Information',
|
||||
funding_source: 'Funding source: {wallet_class}',
|
||||
node_balance: 'Node balance: {balance} sats',
|
||||
lnbits_balance: 'LNbits balance: {balance} sats',
|
||||
funding_reserve_percent: 'Funding reserve percentage: {percent} %',
|
||||
node_management: 'Node Management',
|
||||
node_management_not_supported:
|
||||
'Node Management not supported by active funding source',
|
||||
'Node management is not supported by the active funding source',
|
||||
toggle_node_ui: 'Node UI',
|
||||
toggle_public_node_ui: 'Public Node UI',
|
||||
toggle_transactions_node_ui: 'Transactions Tab (Disable on large CLN nodes)',
|
||||
invoice_expiry: 'Invoice Expiry',
|
||||
invoice_expiry_label: 'Invoice expiry (seconds)',
|
||||
fee_reserve: 'Fee Reserve',
|
||||
fee_reserve_percent: 'Fee Reserve Percent',
|
||||
fee_reserve_msats: 'Reserve fee in msats',
|
||||
reserve_fee_in_percent: 'Reserve fee in percent',
|
||||
payment_wait_time: 'Payment Wait Time (sec)',
|
||||
routing_fee_reserve_calculations: 'Routing Fee Reserve Calculations',
|
||||
routing_fee_reserve_calculations_desc:
|
||||
'LNbits sets aside a “reserve amount” for each payment to cover routing fees. The maximum routing fee passed to the funding source is whichever is higher: the <strong>minimum routing fee reserve</strong> or the <strong>routing fee reserve percentage</strong>.',
|
||||
millisats: 'millisats',
|
||||
fee_reserve: 'Minimum Routing Fee Reserve',
|
||||
fee_reserve_percent: 'Routing Fee Reserve Percentage',
|
||||
fee_reserve_min_hint:
|
||||
'The minimum fee reserved per payment.<br>This acts as a floor - the maximum allowed routing fee will never be lower than this value regardless of payment size.',
|
||||
fee_reserve_percent_hint:
|
||||
'The percentage of the payment amount to reserve for routing fees.',
|
||||
payment_timeouts: 'Payment Timeouts',
|
||||
payment_wait_time: 'Payment Wait Time',
|
||||
seconds: 'seconds',
|
||||
payment_pending_interval: 'Check payment interval (sec)',
|
||||
payment_pending_interval_desc: 'Interval to check pending payments',
|
||||
payment_pending_interval_tooltip:
|
||||
'Controls how often LNbits checks for pending payments to update their status. Higher values can reduce the load on the node and speed up the payment process, but it will take longer for pending payments to be updated.',
|
||||
payment_wait_time_desc:
|
||||
'Wait time before marking an outgoing payment as pending. Default: 5s; raise for slow-settling invoices.',
|
||||
payment_wait_time_tooltip:
|
||||
'Controls how long LNbits waits for an outgoing payment attempt to confirm before marking it as pending. Higher values help when paying slow-settling invoices (e.g., HODL invoices, Boltz). The payment will be rechecked later and updated automatically or manually.',
|
||||
server_management: 'Server Management',
|
||||
base_url: 'Base URL',
|
||||
base_url_label: 'Static/Base url for the server',
|
||||
base_url_label: 'Base URL of the server',
|
||||
authentication: 'Authentication',
|
||||
auth_token_expiry_label: 'Token expire minutes',
|
||||
auth_token_expiry_label: 'Token expiry (minutes)',
|
||||
auth_token_expiry_hint: 'Time in minutes until the token expires',
|
||||
auth_authentication_cache_label: 'Cache time (minutes)',
|
||||
auth_authentication_cache_hint:
|
||||
'Time in minutes to cache successful authentication (0 to disable)',
|
||||
auth_allowed_methods_label: 'Allowed authorization methods',
|
||||
auth_allowed_methods_hint: 'Select authorization methods',
|
||||
auth_allowed_methods_hint: 'Select allowed authorization methods',
|
||||
auth_nostr_label: 'Nostr Request URL',
|
||||
auth_nostr_hint: 'Absolute URL that the clients will use to login.',
|
||||
auth_google_ci_label: 'Google Client ID',
|
||||
@@ -631,33 +646,37 @@ window.localisation.en = {
|
||||
auth_keycloak_cs_label: 'Keycloak Client Secret',
|
||||
auth_keycloak_custom_org_label: 'Keycloak Custom Organization',
|
||||
auth_keycloak_custom_icon_label: 'Keycloak Custom Icon (URL)',
|
||||
auth_oidc_label: 'OIDC Discovery URL',
|
||||
auth_oidc_ci_label: 'OIDC Client ID',
|
||||
auth_oidc_ci_hint:
|
||||
'Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/oidc/token',
|
||||
auth_oidc_cs_label: 'OIDC Client Secret',
|
||||
auth_oidc_custom_org_label:
|
||||
'OIDC Custom Organization Name (e.g., Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'OIDC Custom Icon (URL)',
|
||||
currency_settings: 'Currency Settings',
|
||||
allowed_currencies: 'Allowed Currencies',
|
||||
allowed_currencies_hint: 'Limit the number of available fiat currencies',
|
||||
default_account_currency: 'Default Account Currency',
|
||||
default_account_currency_hint: 'Default currency for accounting',
|
||||
allowed_currencies_hint:
|
||||
'Set the allowed fiat currencies for the exchange features',
|
||||
default_account_currency: 'Default Accounting Currency',
|
||||
default_account_currency_hint:
|
||||
'The default currency to use for accounting features.',
|
||||
min_incoming_payment_amount: 'Min Incoming Payment Amount',
|
||||
min_incoming_payment_amount_desc:
|
||||
'Minimum amount allowed for generating an invoice',
|
||||
max_incoming_payment_amount: 'Max Incoming Payment Amount',
|
||||
max_incoming_payment_amount: 'Maximum Incoming Payment Amount',
|
||||
max_incoming_payment_amount_desc:
|
||||
'Maximum amount allowed for generating an invoice',
|
||||
max_outgoing_payment_amount: 'Max Outgoing Payment Amount',
|
||||
'Maximum amount allowed when generating an invoice',
|
||||
max_outgoing_payment_amount: 'Maximum Outgoing Payment Amount',
|
||||
max_outgoing_payment_amount_desc:
|
||||
'Maximum amount allowed for making a payment',
|
||||
'Maximum amount allowed when making a payment',
|
||||
service_fees: 'Service Fees',
|
||||
service_fee: 'Service Fee',
|
||||
service_fee_label: 'Service fee (%)',
|
||||
service_fee_hint: 'Fee charged per tx (%)',
|
||||
service_fee_max: 'Service Fee Max',
|
||||
service_fee_max_label: 'Service fee max (sats)',
|
||||
service_fee_max_hint: 'Max service fee to charge in (sats)',
|
||||
fee_wallet: 'Fee Wallet',
|
||||
fee_wallet_label: 'Fee wallet (wallet ID)',
|
||||
fee_wallet_hint: 'Wallet ID to send funds to',
|
||||
disable_fee: 'Disable Fee',
|
||||
disable_fee_internal: 'Disable Service Fee for Internal Payments',
|
||||
disable_fee_internal_desc:
|
||||
'Disable Service Fee for Internal Lightning Payments',
|
||||
service_fee_label: 'Service Fee Charged Per Transaction',
|
||||
service_fee_max_label: 'Maximum Service Fee Limit',
|
||||
fee_wallet_label: 'Service Fee Wallet ID',
|
||||
fee_wallet_hint: 'The ID of the wallet to which to send service funds',
|
||||
disable_fee: 'Disable Service Fees for Internal Payments',
|
||||
ui_management: 'UI Management',
|
||||
ui_site_title: 'Site Title',
|
||||
ui_changing_remove_lnbits_elements:
|
||||
@@ -670,6 +689,7 @@ window.localisation.en = {
|
||||
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
||||
ui_default_wallet_name: 'Default Wallet Name',
|
||||
ui_default_theme: 'Default Theme',
|
||||
wallet_featured_button_title: 'Wallet - Featured Button',
|
||||
wallet_featured_button_label: 'Wallet Featured Button Label',
|
||||
wallet_featured_button_label_hint:
|
||||
'Show featured button on the wallet homepage',
|
||||
@@ -678,33 +698,38 @@ window.localisation.en = {
|
||||
'On click the button will open this URL. Leave empty to hide the button.',
|
||||
wallet_featured_button_icon: 'Featured Button Icon',
|
||||
wallet_featured_button_icon_hint:
|
||||
'Icon shown on the featured button (check quasar icons)',
|
||||
'Icon shown on the featured button (Quasar icon name e.g. "bolt")',
|
||||
lnbits_wallet: 'LNbits wallet',
|
||||
denomination: 'Denomination',
|
||||
denomination_hint: 'The name for the FakeWallet token',
|
||||
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
||||
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
||||
ui_qr_code_logo_hint: 'QR code and favicon logo url',
|
||||
ui_qr_code_logo_hint: 'QR code icon and favicon logo URL',
|
||||
ui_apple_touch_icon: 'Apple Touch Icon',
|
||||
ui_apple_touch_icon_hint: 'Apple touch icon url',
|
||||
ui_apple_touch_icon_hint: 'Apple touch icon URL',
|
||||
ui_custom_image: 'Custom Image',
|
||||
ui_custom_image_label: 'URL to custom image',
|
||||
ui_custom_image_hint: 'Image showed at homepage/login',
|
||||
ui_custom_badge: 'Custom Badge',
|
||||
ui_custom_image_hint:
|
||||
'This image is shown on the LNbits homepage and login screen.',
|
||||
ui_custom_badge_title: 'Custom Badge Settings',
|
||||
ui_custom_badge_desc: 'Show a custom badge in the header of LNbits',
|
||||
ui_custom_badge: 'Custom Badge Text',
|
||||
ui_custom_badge_label: "Custom Badge 'USE WITH CAUTION'",
|
||||
ui_custom_badge_color_label: 'Custom Badge Color',
|
||||
themes: 'Themes',
|
||||
themes_hint: 'Choose themes available for users',
|
||||
custom_logo: 'Custom Logo',
|
||||
custom_logo_hint: 'URL to logo image',
|
||||
ad_space_title: 'Ad Space Title',
|
||||
ad_space_title_label: 'Supported by',
|
||||
ad_space_section_title: 'Advertisement Space',
|
||||
ad_space_section_desc:
|
||||
'Configure the advertisement space on the wallet sidebar.',
|
||||
ad_space_title: 'Advertisement Space Title',
|
||||
ad_space_title_hint: 'Title shown above the advertisement space',
|
||||
ad_slots: 'Advertisement Slots',
|
||||
ad_slots_hint:
|
||||
'Ad url and image filepaths in CSV format, extensions can choose to honor',
|
||||
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
|
||||
ads_enabled: 'Ads Enabled',
|
||||
ads_disabled: 'Ads Disabled',
|
||||
'Advertisement image filepaths in CSV format, extensions can choose to honor. Format: url;img_light_url;img_dark_url, url..',
|
||||
ads_enabled: 'Enable Advertisement',
|
||||
ads_disabled: 'Disabled Advertisement',
|
||||
user_management: 'User Management',
|
||||
admin_users: 'Admin Users',
|
||||
admin_users_hint: 'Users with admin privileges',
|
||||
|
||||
@@ -379,6 +379,16 @@ window.localisation.es = {
|
||||
auth_keycloak_ci_hint:
|
||||
'Asegúrate de que la URL de devolución de llamada de autorización esté configurada en https://{domain}/api/v1/auth/keycloak/token',
|
||||
auth_keycloak_cs_label: 'Secreto del Cliente de Keycloak',
|
||||
auth_keycloak_custom_org_label: 'Organización personalizada de Keycloak',
|
||||
auth_keycloak_custom_icon_label: 'Icono personalizado de Keycloak (URL)',
|
||||
auth_oidc_label: 'URL de descubrimiento de OIDC',
|
||||
auth_oidc_ci_label: 'ID de cliente de OIDC',
|
||||
auth_oidc_ci_hint:
|
||||
'Asegúrate de que la URL de devolución de llamada de autorización esté configurada en https://{domain}/api/v1/auth/oidc/token',
|
||||
auth_oidc_cs_label: 'Secreto del Cliente de OIDC',
|
||||
auth_oidc_custom_org_label:
|
||||
'Nombre de organización personalizada OIDC (ej. Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'Icono personalizado de OIDC (URL)',
|
||||
currency_settings: 'Configuración de moneda',
|
||||
allowed_currencies: 'Monedas permitidas',
|
||||
allowed_currencies_hint:
|
||||
|
||||
@@ -520,6 +520,14 @@ window.localisation.fi = {
|
||||
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
||||
auth_keycloak_custom_org_label: 'Valinnainen Keycloak-organisaatio',
|
||||
auth_keycloak_custom_icon_label: 'Valinnainen Keycloak-kuvake (URL)',
|
||||
auth_oidc_label: 'OIDC-discovery-URL',
|
||||
auth_oidc_ci_label: 'OIDC-asiakastunnus',
|
||||
auth_oidc_ci_hint:
|
||||
'Varmista, että valtuutuksen palautus-URL on asetettu muotoon https://{domain}/api/v1/auth/oidc/token',
|
||||
auth_oidc_cs_label: 'OIDC-asiakassalasana',
|
||||
auth_oidc_custom_org_label:
|
||||
'OIDC mukautetun organisaation nimi (esim. Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'Valinnainen OIDC-kuvake (URL)',
|
||||
currency_settings: 'Valuutta-asetukset',
|
||||
allowed_currencies: 'Käytettävät valuutat',
|
||||
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
||||
|
||||
@@ -381,6 +381,16 @@ window.localisation.fr = {
|
||||
auth_keycloak_ci_hint:
|
||||
"Assurez-vous que l'URL de rappel d'autorisation est définie sur https://{domain}/api/v1/auth/keycloak/token",
|
||||
auth_keycloak_cs_label: 'Secret client Keycloak',
|
||||
auth_keycloak_custom_org_label: 'Organisation personnalisée Keycloak',
|
||||
auth_keycloak_custom_icon_label: 'Icône personnalisée Keycloak (URL)',
|
||||
auth_oidc_label: 'URL de découverte OIDC',
|
||||
auth_oidc_ci_label: 'ID Client OIDC',
|
||||
auth_oidc_ci_hint:
|
||||
"Assurez-vous que l'URL de rappel d'autorisation est définie sur https://{domain}/api/v1/auth/oidc/token",
|
||||
auth_oidc_cs_label: 'Secret client OIDC',
|
||||
auth_oidc_custom_org_label:
|
||||
"Nom de l'organisation personnalisée OIDC (par ex. Zitadel, Authentik)",
|
||||
auth_oidc_custom_icon_label: 'Icône personnalisée OIDC (URL)',
|
||||
currency_settings: 'Paramètres de devise',
|
||||
allowed_currencies: 'Devises autorisées',
|
||||
allowed_currencies_hint:
|
||||
|
||||
@@ -378,6 +378,16 @@ window.localisation.it = {
|
||||
auth_keycloak_ci_hint:
|
||||
"Assicurati che l'URL di callback dell'autorizzazione sia impostato su https://{domain}/api/v1/auth/keycloak/token",
|
||||
auth_keycloak_cs_label: 'Keycloak Client Secret',
|
||||
auth_keycloak_custom_org_label: 'Organizzazione personalizzata di Keycloak',
|
||||
auth_keycloak_custom_icon_label: 'Icona personalizzata di Keycloak (URL)',
|
||||
auth_oidc_label: 'URL di individuazione di OIDC',
|
||||
auth_oidc_ci_label: 'ID client di OIDC',
|
||||
auth_oidc_ci_hint:
|
||||
"Assicurati che l'URL di callback dell'autorizzazione sia impostato su https://{domain}/api/v1/auth/oidc/token",
|
||||
auth_oidc_cs_label: 'OIDC Client Secret',
|
||||
auth_oidc_custom_org_label:
|
||||
'Nome organizzazione personalizzata OIDC (es. Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'Icona personalizzata di OIDC (URL)',
|
||||
currency_settings: 'Impostazioni valuta',
|
||||
allowed_currencies: 'Valute consentite',
|
||||
allowed_currencies_hint: 'Limita il numero di valute fiat disponibili',
|
||||
|
||||
@@ -369,6 +369,15 @@ window.localisation.jp = {
|
||||
auth_keycloak_ci_hint:
|
||||
'認証コールバックURLが https://{domain}/api/v1/auth/keycloak/token に設定されていることを確認してください。',
|
||||
auth_keycloak_cs_label: 'キークローククライアントシークレット',
|
||||
auth_keycloak_custom_org_label: 'Keycloak カスタム組織',
|
||||
auth_keycloak_custom_icon_label: 'Keycloak カスタムアイコン (URL)',
|
||||
auth_oidc_label: 'OIDC ディスカバリー URL',
|
||||
auth_oidc_ci_label: 'OIDC クライアント ID',
|
||||
auth_oidc_ci_hint:
|
||||
'認証コールバックURLが https://{domain}/api/v1/auth/oidc/token に設定されていることを確認してください。',
|
||||
auth_oidc_cs_label: 'OIDC クライアントシークレット',
|
||||
auth_oidc_custom_org_label: 'OIDC カスタム組織名(例:Zitadel、Authentik)',
|
||||
auth_oidc_custom_icon_label: 'OIDC カスタムアイコン (URL)',
|
||||
currency_settings: '通貨設定',
|
||||
allowed_currencies: '許可されている通貨',
|
||||
allowed_currencies_hint: '利用可能な法定通貨の数を制限する',
|
||||
|
||||
@@ -365,6 +365,16 @@ window.localisation.kr = {
|
||||
auth_keycloak_ci_hint:
|
||||
'승인 콜백 URL이 https://{domain}/api/v1/auth/keycloak/token으로 설정되어 있는지 확인하십시오.',
|
||||
auth_keycloak_cs_label: 'Keycloak 클라이언트 시크릿',
|
||||
auth_keycloak_custom_org_label: 'Keycloak 사용자 정의 조직',
|
||||
auth_keycloak_custom_icon_label: 'Keycloak 사용자 정의 아이콘 (URL)',
|
||||
auth_oidc_label: 'OIDC 디스커버리 URL',
|
||||
auth_oidc_ci_label: 'OIDC 클라이언트 ID',
|
||||
auth_oidc_ci_hint:
|
||||
'승인 콜백 URL이 https://{domain}/api/v1/auth/oidc/token으로 설정되어 있는지 확인하십시오.',
|
||||
auth_oidc_cs_label: 'OIDC 클라이언트 시크릿',
|
||||
auth_oidc_custom_org_label:
|
||||
'OIDC 사용자 정의 조직 이름 (예: Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'OIDC 사용자 정의 아이콘 (URL)',
|
||||
currency_settings: '통화 설정',
|
||||
allowed_currencies: '허용되는 통화',
|
||||
allowed_currencies_hint: '사용 가능한 법정 화폐의 수를 제한하십시오.',
|
||||
|
||||
@@ -377,6 +377,16 @@ window.localisation.nl = {
|
||||
auth_keycloak_ci_hint:
|
||||
'Zorg ervoor dat de autorisatie callback-URL is ingesteld op https://{domain}/api/v1/auth/keycloak/token',
|
||||
auth_keycloak_cs_label: 'Keycloak Clientgeheim',
|
||||
auth_keycloak_custom_org_label: 'Keycloak Aangepaste Organisatie',
|
||||
auth_keycloak_custom_icon_label: 'Keycloak Aangepast Pictogram (URL)',
|
||||
auth_oidc_label: 'OIDC Ontdekking URL',
|
||||
auth_oidc_ci_label: 'OIDC-client-ID',
|
||||
auth_oidc_ci_hint:
|
||||
'Zorg ervoor dat de autorisatie callback-URL is ingesteld op https://{domain}/api/v1/auth/oidc/token',
|
||||
auth_oidc_cs_label: 'OIDC Clientgeheim',
|
||||
auth_oidc_custom_org_label:
|
||||
'OIDC Aangepaste Organisatienaam (bijv. Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'OIDC Aangepast Pictogram (URL)',
|
||||
currency_settings: 'Valuta-instellingen',
|
||||
allowed_currencies: "Toegestane valuta's",
|
||||
allowed_currencies_hint: "Beperk het aantal beschikbare fiatvaluta's",
|
||||
|
||||
@@ -371,6 +371,16 @@ window.localisation.pi = {
|
||||
auth_keycloak_ci_hint:
|
||||
"Make sure thant th' authorization callback URL be set t' https://{domain}/api/v1/auth/keycloak/token",
|
||||
auth_keycloak_cs_label: 'Keycloak Client Secret',
|
||||
auth_keycloak_custom_org_label: 'Keycloak Custom Organization',
|
||||
auth_keycloak_custom_icon_label: 'Keycloak Custom Icon (URL)',
|
||||
auth_oidc_label: 'OIDC Discovery URL',
|
||||
auth_oidc_ci_label: 'OIDC Client ID',
|
||||
auth_oidc_ci_hint:
|
||||
"Make sure thant th' authorization callback URL be set t' https://{domain}/api/v1/auth/oidc/token",
|
||||
auth_oidc_cs_label: 'OIDC Client Secret',
|
||||
auth_oidc_custom_org_label:
|
||||
'OIDC Custom Organization Name (e.g., Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'OIDC Custom Icon (URL)',
|
||||
currency_settings: "Doubloon Settin's",
|
||||
allowed_currencies: "Allo'ed Doubloons",
|
||||
allowed_currencies_hint: 'Limit the number of available fiat doubloons',
|
||||
|
||||
@@ -372,6 +372,16 @@ window.localisation.pl = {
|
||||
auth_keycloak_ci_hint:
|
||||
'Upewnij się, że URL zwrotu autoryzacji jest ustawiony na https://{domain}/api/v1/auth/keycloak/token',
|
||||
auth_keycloak_cs_label: 'Hasło klienta Keycloak',
|
||||
auth_keycloak_custom_org_label: 'Własna organizacja Keycloak',
|
||||
auth_keycloak_custom_icon_label: 'Własna ikona Keycloak (URL)',
|
||||
auth_oidc_label: 'Adres URL Discovery OIDC',
|
||||
auth_oidc_ci_label: 'Identyfikator klienta OIDC',
|
||||
auth_oidc_ci_hint:
|
||||
'Upewnij się, że URL zwrotu autoryzacji jest ustawiony na https://{domain}/api/v1/auth/oidc/token',
|
||||
auth_oidc_cs_label: 'Hasło klienta OIDC',
|
||||
auth_oidc_custom_org_label:
|
||||
'Nazwa własnej organizacji OIDC (np. Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'Własna ikona OIDC (URL)',
|
||||
currency_settings: 'Ustawienia waluty',
|
||||
allowed_currencies: 'Dozwolone waluty',
|
||||
allowed_currencies_hint: 'Ogranicz liczbę dostępnych walut fiducjarnych',
|
||||
|
||||
@@ -375,6 +375,16 @@ window.localisation.pt = {
|
||||
auth_keycloak_ci_hint:
|
||||
'Certifique-se de que o URL de retorno de chamada de autorização esteja definido como https://{domain}/api/v1/auth/keycloak/token',
|
||||
auth_keycloak_cs_label: 'Segredo do Cliente do Keycloak',
|
||||
auth_keycloak_custom_org_label: 'Organização Personalizada do Keycloak',
|
||||
auth_keycloak_custom_icon_label: 'Ícone Personalizado do Keycloak (URL)',
|
||||
auth_oidc_label: 'URL de Descoberta do OIDC',
|
||||
auth_oidc_ci_label: 'ID do Cliente do OIDC',
|
||||
auth_oidc_ci_hint:
|
||||
'Certifique-se de que o URL de retorno de chamada de autorização esteja definido como https://{domain}/api/v1/auth/oidc/token',
|
||||
auth_oidc_cs_label: 'Segredo do Cliente do OIDC',
|
||||
auth_oidc_custom_org_label:
|
||||
'Nome da Organização Personalizada OIDC (ex. Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'Ícone Personalizado do OIDC (URL)',
|
||||
currency_settings: 'Configurações de Moeda',
|
||||
allowed_currencies: 'Moedas Permitidas',
|
||||
allowed_currencies_hint: 'Limite o número de moedas fiduciárias disponíveis',
|
||||
|
||||
@@ -371,6 +371,16 @@ window.localisation.sk = {
|
||||
auth_keycloak_ci_hint:
|
||||
'Uistite sa, že URL spätného volania autorizácie je nastavená na https://{domain}/api/v1/auth/keycloak/token',
|
||||
auth_keycloak_cs_label: 'Tajný kľúč klienta Keycloak',
|
||||
auth_keycloak_custom_org_label: 'Vlastná organizácia Keycloak',
|
||||
auth_keycloak_custom_icon_label: 'Vlastná ikona Keycloak (URL)',
|
||||
auth_oidc_label: 'URL zistenia OIDC',
|
||||
auth_oidc_ci_label: 'ID klienta OIDC',
|
||||
auth_oidc_ci_hint:
|
||||
'Uistite sa, že URL spätného volania autorizácie je nastavená na https://{domain}/api/v1/auth/oidc/token',
|
||||
auth_oidc_cs_label: 'Tajný kľúč klienta OIDC',
|
||||
auth_oidc_custom_org_label:
|
||||
'Názov vlastnej organizácie OIDC (napr. Zitadel, Authentik)',
|
||||
auth_oidc_custom_icon_label: 'Vlastná ikona OIDC (URL)',
|
||||
currency_settings: 'Nastavenia meny',
|
||||
allowed_currencies: 'Povolené meny',
|
||||
allowed_currencies_hint: 'Obmedzte počet dostupných fiat mien',
|
||||
|
||||
@@ -370,6 +370,16 @@ window.localisation.we = {
|
||||
auth_keycloak_ci_hint:
|
||||
"Gwnewch yn siŵr bod URL adalw awdurdodiad wedi'i osod i https://{domain}/api/v1/auth/keycloak/token",
|
||||
auth_keycloak_cs_label: 'Cyfrinach Cleient Keycloak',
|
||||
auth_keycloak_custom_org_label: "Sefydliad Wedi'i Addasu Keycloak",
|
||||
auth_keycloak_custom_icon_label: "Eicon Wedi'i Addasu Keycloak (URL)",
|
||||
auth_oidc_label: 'URL Darganfod OIDC',
|
||||
auth_oidc_ci_label: 'ID Cleient OIDC',
|
||||
auth_oidc_ci_hint:
|
||||
"Gwnewch yn siŵr bod URL adalw awdurdodiad wedi'i osod i https://{domain}/api/v1/auth/oidc/token",
|
||||
auth_oidc_cs_label: 'Cyfrinach Cleient OIDC',
|
||||
auth_oidc_custom_org_label:
|
||||
"Enw Sefydliad Wedi'i Addasu OIDC (e.e. Zitadel, Authentik)",
|
||||
auth_oidc_custom_icon_label: "Eicon Wedi'i Addasu OIDC (URL)",
|
||||
currency_settings: 'Gosodiadau Arian Cyfred',
|
||||
allowed_currencies: 'Ariannau a Ganiateir',
|
||||
allowed_currencies_hint: 'Cyfyngu nifer yr arian cyfred fiat sydd ar gael',
|
||||
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<!-- Background circle -->
|
||||
<circle cx="50" cy="50" r="48" fill="#4A90E2" stroke="#2E5F8E" stroke-width="2"/>
|
||||
|
||||
<!-- Lock body -->
|
||||
<rect x="35" y="45" width="30" height="25" rx="2" fill="#FFFFFF"/>
|
||||
|
||||
<!-- Lock shackle -->
|
||||
<path d="M 40 45 L 40 35 Q 40 25 50 25 Q 60 25 60 35 L 60 45"
|
||||
fill="none" stroke="#FFFFFF" stroke-width="4" stroke-linecap="round"/>
|
||||
|
||||
<!-- Keyhole -->
|
||||
<circle cx="50" cy="55" r="3" fill="#4A90E2"/>
|
||||
<rect x="48.5" y="55" width="3" height="8" fill="#4A90E2"/>
|
||||
|
||||
<!-- ID letters -->
|
||||
<text x="50" y="85" font-family="Arial, sans-serif" font-size="12" font-weight="bold"
|
||||
fill="#FFFFFF" text-anchor="middle">ID</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 773 B |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -442,7 +442,8 @@ window.app.component('username-password', {
|
||||
'nostr-auth-nip98',
|
||||
'google-auth',
|
||||
'github-auth',
|
||||
'keycloak-auth'
|
||||
'keycloak-auth',
|
||||
'oidc-auth'
|
||||
],
|
||||
username: this.userName,
|
||||
password: this.password_1,
|
||||
@@ -691,12 +692,10 @@ window.app.component('lnbits-node-qrcode', {
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
<div style="text-align: center">
|
||||
<vue-qrcode
|
||||
:value="info.addresses[0]"
|
||||
:options="{width: 250}"
|
||||
<lnbits-qrcode
|
||||
v-if='info.addresses[0]'
|
||||
class="rounded-borders"
|
||||
></vue-qrcode>
|
||||
:value="info.addresses[0]"
|
||||
></lnbits-qrcode>
|
||||
<div v-else class='text-subtitle1'>
|
||||
No addresses available
|
||||
</div>
|
||||
|
||||
@@ -234,7 +234,12 @@ window.app.component('lnbits-admin-funding-sources', {
|
||||
{
|
||||
spark_l2_external_endpoint: {
|
||||
label: 'External Sidecar Endpoint',
|
||||
hint: 'If specified then this endpoint will be used instead of the internal sidecar. Make sure to also specify the API key if your sidecar requires authentication.',
|
||||
hint: 'Make sure to also specify the API key if your sidecar requires authentication.',
|
||||
value: ''
|
||||
},
|
||||
spark_l2_mnemonic: {
|
||||
label: 'External Sidecar Mnemonic',
|
||||
hint: 'Mnemonic for the Spark wallet on the external sidecar. Required if the side car does not have its own mnemonic.',
|
||||
value: ''
|
||||
},
|
||||
spark_l2_external_api_key: {
|
||||
|
||||
@@ -17,6 +17,13 @@ window.app.component('lnbits-manage-wallet-list', {
|
||||
} else {
|
||||
this.activeWalletId = null
|
||||
}
|
||||
},
|
||||
'g.user.wallets': {
|
||||
handler() {
|
||||
this.paymentEvents()
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
@@ -131,6 +131,22 @@ window._lnbitsUtils = {
|
||||
currency: currency || 'sat'
|
||||
}).format(value)
|
||||
},
|
||||
getCurrencySymbol(currency) {
|
||||
const code = (currency || '').toUpperCase()
|
||||
if (code === 'BTC' || code === 'XBT' || code === 'SAT' || code === 'SATS') {
|
||||
return '₿'
|
||||
}
|
||||
try {
|
||||
const parts = new Intl.NumberFormat(window.i18n.global.locale, {
|
||||
style: 'currency',
|
||||
currency: code
|
||||
}).formatToParts(0)
|
||||
const symbolPart = parts.find(part => part.type === 'currency')
|
||||
return symbolPart?.value || code || '¤'
|
||||
} catch (e) {
|
||||
return code || '¤'
|
||||
}
|
||||
},
|
||||
formatSat(value) {
|
||||
return new Intl.NumberFormat(window.i18n.global.locale).format(value)
|
||||
},
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
exports.noConflict = function () { global._ = current; return exports; };
|
||||
}()));
|
||||
}(this, (function () {
|
||||
// Underscore.js 1.13.7
|
||||
// Underscore.js 1.13.8
|
||||
// https://underscorejs.org
|
||||
// (c) 2009-2024 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors
|
||||
// (c) 2009-2026 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors
|
||||
// Underscore may be freely distributed under the MIT license.
|
||||
|
||||
// Current version.
|
||||
var VERSION = '1.13.7';
|
||||
var VERSION = '1.13.8';
|
||||
|
||||
// Establish the root object, `window` (`self`) in the browser, `global`
|
||||
// on the server, or `this` in some virtual machines. We use `self`
|
||||
@@ -357,131 +357,146 @@
|
||||
// We use this string twice, so give it a name for minification.
|
||||
var tagDataView = '[object DataView]';
|
||||
|
||||
// Internal recursive comparison function for `_.isEqual`.
|
||||
function eq(a, b, aStack, bStack) {
|
||||
// Identical objects are equal. `0 === -0`, but they aren't identical.
|
||||
// See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal).
|
||||
if (a === b) return a !== 0 || 1 / a === 1 / b;
|
||||
// `null` or `undefined` only equal to itself (strict comparison).
|
||||
if (a == null || b == null) return false;
|
||||
// `NaN`s are equivalent, but non-reflexive.
|
||||
if (a !== a) return b !== b;
|
||||
// Exhaust primitive checks
|
||||
var type = typeof a;
|
||||
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
|
||||
return deepEq(a, b, aStack, bStack);
|
||||
}
|
||||
// Perform a deep comparison to check if two objects are equal.
|
||||
function isEqual(a, b) {
|
||||
var todo = [{a: a, b: b}];
|
||||
// Initializing stacks of traversed objects for cycle detection.
|
||||
var aStack = [], bStack = [];
|
||||
|
||||
while (todo.length) {
|
||||
var frame = todo.pop();
|
||||
if (frame === true) {
|
||||
// Remove the first object from the stack of traversed objects.
|
||||
aStack.pop();
|
||||
bStack.pop();
|
||||
continue;
|
||||
}
|
||||
a = frame.a;
|
||||
b = frame.b;
|
||||
|
||||
// Identical objects are equal. `0 === -0`, but they aren't identical.
|
||||
// See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal).
|
||||
if (a === b) {
|
||||
if (a !== 0 || 1 / a === 1 / b) continue;
|
||||
return false;
|
||||
}
|
||||
// `null` or `undefined` only equal to itself (strict comparison).
|
||||
if (a == null || b == null) return false;
|
||||
// `NaN`s are equivalent, but non-reflexive.
|
||||
if (a !== a) {
|
||||
if (b !== b) continue;
|
||||
return false;
|
||||
}
|
||||
// Exhaust primitive checks
|
||||
var type = typeof a;
|
||||
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
|
||||
|
||||
// Internal recursive comparison function for `_.isEqual`.
|
||||
function deepEq(a, b, aStack, bStack) {
|
||||
// Unwrap any wrapped objects.
|
||||
if (a instanceof _$1) a = a._wrapped;
|
||||
if (b instanceof _$1) b = b._wrapped;
|
||||
// Compare `[[Class]]` names.
|
||||
var className = toString.call(a);
|
||||
if (className !== toString.call(b)) return false;
|
||||
// Work around a bug in IE 10 - Edge 13.
|
||||
if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) {
|
||||
if (!isDataView$1(b)) return false;
|
||||
className = tagDataView;
|
||||
}
|
||||
switch (className) {
|
||||
// These types are compared by value.
|
||||
// Unwrap any wrapped objects.
|
||||
if (a instanceof _$1) a = a._wrapped;
|
||||
if (b instanceof _$1) b = b._wrapped;
|
||||
// Compare `[[Class]]` names.
|
||||
var className = toString.call(a);
|
||||
if (className !== toString.call(b)) return false;
|
||||
// Work around a bug in IE 10 - Edge 13.
|
||||
if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) {
|
||||
if (!isDataView$1(b)) return false;
|
||||
className = tagDataView;
|
||||
}
|
||||
switch (className) {
|
||||
// These types are compared by value.
|
||||
case '[object RegExp]':
|
||||
// RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
|
||||
case '[object String]':
|
||||
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
|
||||
// equivalent to `new String("5")`.
|
||||
return '' + a === '' + b;
|
||||
if ('' + a === '' + b) continue;
|
||||
return false;
|
||||
case '[object Number]':
|
||||
// `NaN`s are equivalent, but non-reflexive.
|
||||
// Object(NaN) is equivalent to NaN.
|
||||
if (+a !== +a) return +b !== +b;
|
||||
// An `egal` comparison is performed for other numeric values.
|
||||
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
|
||||
todo.push({a: +a, b: +b});
|
||||
continue;
|
||||
case '[object Date]':
|
||||
case '[object Boolean]':
|
||||
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
|
||||
// millisecond representations. Note that invalid dates with millisecond representations
|
||||
// of `NaN` are not equivalent.
|
||||
return +a === +b;
|
||||
if (+a === +b) continue;
|
||||
return false;
|
||||
case '[object Symbol]':
|
||||
return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
|
||||
if (SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b)) continue;
|
||||
return false;
|
||||
case '[object ArrayBuffer]':
|
||||
case tagDataView:
|
||||
// Coerce to typed array so we can fall through.
|
||||
return deepEq(toBufferView(a), toBufferView(b), aStack, bStack);
|
||||
}
|
||||
todo.push({a: toBufferView(a), b: toBufferView(b)});
|
||||
continue;
|
||||
}
|
||||
|
||||
var areArrays = className === '[object Array]';
|
||||
if (!areArrays && isTypedArray$1(a)) {
|
||||
var areArrays = className === '[object Array]';
|
||||
if (!areArrays && isTypedArray$1(a)) {
|
||||
var byteLength = getByteLength(a);
|
||||
if (byteLength !== getByteLength(b)) return false;
|
||||
if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true;
|
||||
if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) continue;
|
||||
areArrays = true;
|
||||
}
|
||||
if (!areArrays) {
|
||||
if (typeof a != 'object' || typeof b != 'object') return false;
|
||||
|
||||
// Objects with different constructors are not equivalent, but `Object`s or `Array`s
|
||||
// from different frames are.
|
||||
var aCtor = a.constructor, bCtor = b.constructor;
|
||||
if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor &&
|
||||
isFunction$1(bCtor) && bCtor instanceof bCtor)
|
||||
&& ('constructor' in a && 'constructor' in b)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Assume equality for cyclic structures. The algorithm for detecting cyclic
|
||||
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
|
||||
if (!areArrays) {
|
||||
if (typeof a != 'object' || typeof b != 'object') return false;
|
||||
|
||||
// Initializing stack of traversed objects.
|
||||
// It's done here since we only need them for objects and arrays comparison.
|
||||
aStack = aStack || [];
|
||||
bStack = bStack || [];
|
||||
var length = aStack.length;
|
||||
while (length--) {
|
||||
// Linear search. Performance is inversely proportional to the number of
|
||||
// unique nested structures.
|
||||
if (aStack[length] === a) return bStack[length] === b;
|
||||
}
|
||||
// Objects with different constructors are not equivalent, but `Object`s or `Array`s
|
||||
// from different frames are.
|
||||
var aCtor = a.constructor, bCtor = b.constructor;
|
||||
if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor &&
|
||||
isFunction$1(bCtor) && bCtor instanceof bCtor)
|
||||
&& ('constructor' in a && 'constructor' in b)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the first object to the stack of traversed objects.
|
||||
aStack.push(a);
|
||||
bStack.push(b);
|
||||
// Assume equality for cyclic structures. The algorithm for detecting cyclic
|
||||
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
|
||||
|
||||
// Recursively compare objects and arrays.
|
||||
if (areArrays) {
|
||||
// Compare array lengths to determine if a deep comparison is necessary.
|
||||
length = a.length;
|
||||
if (length !== b.length) return false;
|
||||
// Deep compare the contents, ignoring non-numeric properties.
|
||||
var length = aStack.length;
|
||||
while (length--) {
|
||||
if (!eq(a[length], b[length], aStack, bStack)) return false;
|
||||
// Linear search. Performance is inversely proportional to the number of
|
||||
// unique nested structures.
|
||||
if (aStack[length] === a) {
|
||||
if (bStack[length] === b) break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Deep compare objects.
|
||||
var _keys = keys(a), key;
|
||||
length = _keys.length;
|
||||
// Ensure that both objects contain the same number of properties before comparing deep equality.
|
||||
if (keys(b).length !== length) return false;
|
||||
while (length--) {
|
||||
// Deep compare each member
|
||||
key = _keys[length];
|
||||
if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
|
||||
if (length >= 0) continue;
|
||||
|
||||
// Add the first object to the stack of traversed objects.
|
||||
aStack.push(a);
|
||||
bStack.push(b);
|
||||
todo.push(true);
|
||||
|
||||
// Recursively compare objects and arrays.
|
||||
if (areArrays) {
|
||||
// Compare array lengths to determine if a deep comparison is necessary.
|
||||
length = a.length;
|
||||
if (length !== b.length) return false;
|
||||
// Deep compare the contents, ignoring non-numeric properties.
|
||||
while (length--) {
|
||||
todo.push({a: a[length], b: b[length]});
|
||||
}
|
||||
} else {
|
||||
// Deep compare objects.
|
||||
var _keys = keys(a), key;
|
||||
length = _keys.length;
|
||||
// Ensure that both objects contain the same number of properties before comparing deep equality.
|
||||
if (keys(b).length !== length) return false;
|
||||
while (length--) {
|
||||
// Deep compare each member
|
||||
key = _keys[length];
|
||||
if (!has$1(b, key)) return false;
|
||||
todo.push({a: a[key], b: b[key]});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove the first object from the stack of traversed objects.
|
||||
aStack.pop();
|
||||
bStack.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Perform a deep comparison to check if two objects are equal.
|
||||
function isEqual(a, b) {
|
||||
return eq(a, b);
|
||||
}
|
||||
|
||||
// Retrieve all the enumerable property names of an object.
|
||||
function allKeys(obj) {
|
||||
if (!isObject(obj)) return [];
|
||||
@@ -1032,25 +1047,27 @@
|
||||
var isArrayLike = createSizePropertyCheck(getLength);
|
||||
|
||||
// Internal implementation of a recursive `flatten` function.
|
||||
function flatten$1(input, depth, strict, output) {
|
||||
output = output || [];
|
||||
if (!depth && depth !== 0) {
|
||||
depth = Infinity;
|
||||
} else if (depth <= 0) {
|
||||
return output.concat(input);
|
||||
}
|
||||
var idx = output.length;
|
||||
for (var i = 0, length = getLength(input); i < length; i++) {
|
||||
var value = input[i];
|
||||
if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) {
|
||||
function flatten$1(input, depth, strict) {
|
||||
if (!depth && depth !== 0) depth = Infinity;
|
||||
var output = [], idx = 0, i = 0, length = getLength(input) || 0, stack = [];
|
||||
while (true) {
|
||||
if (i >= length) {
|
||||
if (!stack.length) break;
|
||||
var frame = stack.pop();
|
||||
i = frame.i;
|
||||
input = frame.v;
|
||||
length = getLength(input);
|
||||
continue;
|
||||
}
|
||||
var value = input[i++];
|
||||
if (stack.length >= depth) {
|
||||
output[idx++] = value;
|
||||
} else if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) {
|
||||
// Flatten current level of array or arguments object.
|
||||
if (depth > 1) {
|
||||
flatten$1(value, depth - 1, strict, output);
|
||||
idx = output.length;
|
||||
} else {
|
||||
var j = 0, len = value.length;
|
||||
while (j < len) output[idx++] = value[j++];
|
||||
}
|
||||
stack.push({i: i, v: input});
|
||||
i = 0;
|
||||
input = value;
|
||||
length = getLength(input);
|
||||
} else if (!strict) {
|
||||
output[idx++] = value;
|
||||
}
|
||||
|
||||
@@ -1050,6 +1050,34 @@ include('components/lnbits-error.vue') %}
|
||||
></span>
|
||||
</div>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="authMethods.includes('oidc-auth')"
|
||||
href="/api/v1/auth/oidc"
|
||||
type="a"
|
||||
outline
|
||||
no-caps
|
||||
color="grey"
|
||||
class="btn-fixed-width"
|
||||
>
|
||||
<q-avatar size="32px" class="q-mr-md">
|
||||
<q-img
|
||||
:src="
|
||||
g.settings.oidcIcon
|
||||
? g.settings.oidcIcon
|
||||
: utils.url_for('lnbits/static/images/generic-oidc-logo.svg')
|
||||
"
|
||||
></q-img>
|
||||
</q-avatar>
|
||||
<div>
|
||||
<span
|
||||
v-text="
|
||||
$t('signin_with_custom_org', {
|
||||
custom_org: g.settings.oidcOrg || 'OIDC'
|
||||
})
|
||||
"
|
||||
></span>
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</template>
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
<h6 class="q-my-none q-mb-sm">Assets</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('max_asset_size_mb')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -18,9 +15,6 @@
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-9">
|
||||
<p>
|
||||
<span v-text="$t('assets_allowed_mime_types')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model.number="newAllowedAssetMimeType"
|
||||
@@ -56,11 +50,8 @@
|
||||
</div>
|
||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||
<h6 class="q-my-none q-mb-sm">Thumbnails</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('thumbnail_width')"></span>
|
||||
</p>
|
||||
<div class="row q-col-gutter-md q-my-lg">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -71,10 +62,7 @@
|
||||
:hint="$t('thumbnail_width_desc')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('thumbnail_height')"></span>
|
||||
</p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -85,10 +73,7 @@
|
||||
:hint="$t('thumbnail_height_desc')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('thumbnail_format')"></span>
|
||||
</p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
v-model.number="formData.lnbits_asset_thumbnail_format"
|
||||
@@ -101,9 +86,6 @@
|
||||
<h6 class="q-my-none q-mb-sm">Users</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('max_assets_per_user')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -115,9 +97,6 @@
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-9">
|
||||
<p>
|
||||
<span v-text="$t('assets_no_limit_users')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="newNoLimitUser"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-sm-12 q-pr-sm">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
@@ -62,7 +62,7 @@
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12 q-pr-sm">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
@@ -83,7 +83,7 @@
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12 q-pr-sm">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
@@ -104,7 +104,7 @@
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12 q-pr-sm">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
@@ -129,7 +129,6 @@
|
||||
<q-separator class="q-mb-xl q-mt-sm"></q-separator>
|
||||
<div class="row q-mb-lg">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<p><span v-text="$t('audit_http_methods')"></span></p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_audit_http_methods"
|
||||
@@ -148,7 +147,6 @@
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<p><span v-text="$t('audit_resp_codes')"></span></p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
@@ -177,7 +175,6 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<p><span v-text="$t('audit_paths')"></span></p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
@@ -204,9 +201,6 @@
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<p>
|
||||
<span v-text="$t('audit_paths_exclude')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
|
||||
type="number"
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
:hint="$t('extension_builder_manifest_url_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 q-pb-md">
|
||||
<p>
|
||||
<span v-text="$t('reviews_url')"></span>
|
||||
</p>
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
</ul>
|
||||
</q-card-section>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item :label="$t('service_fee')">
|
||||
<q-expansion-item :label="$t('service_fees')">
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-12">
|
||||
@@ -140,7 +140,6 @@
|
||||
v-model="formData.stripe_limits.service_fee_percent"
|
||||
@update:model-value="formData.touch = null"
|
||||
:label="$t('service_fee_label')"
|
||||
:hint="$t('service_fee_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
@@ -151,8 +150,7 @@
|
||||
min="0"
|
||||
v-model="formData.stripe_limits.service_max_fee_sats"
|
||||
@update:model-value="formData.touch = null"
|
||||
:label="$t('service_fee_max')"
|
||||
:hint="$t('service_fee_max_hint')"
|
||||
:label="$t('service_fee_max_label')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
|
||||
@@ -76,47 +76,75 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<p><span v-text="$t('fee_reserve')"></span></p>
|
||||
|
||||
<div class="col-12">
|
||||
<h6 class="q-my-none">
|
||||
<span v-text="$t('routing_fee_reserve_calculations')"></span>
|
||||
</h6>
|
||||
<p class="q-mt-sm q-mb-none">
|
||||
<span v-html="$t('routing_fee_reserve_calculations_desc')"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p>
|
||||
<span v-text="$t('fee_reserve')"></span>
|
||||
<sup>
|
||||
<q-icon name="info" size="16px" class="q-ml-xs"></q-icon>
|
||||
<q-tooltip max-width="300px">
|
||||
<span v-html="$t('fee_reserve_min_hint')"></span>
|
||||
</q-tooltip>
|
||||
</sup>
|
||||
</p>
|
||||
<q-input
|
||||
type="number"
|
||||
filled
|
||||
v-model="formData.lnbits_reserve_fee_min"
|
||||
:label="$t('fee_reserve_msats')"
|
||||
:suffix="$t('millisats')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<p>
|
||||
<span v-text="$t('fee_reserve_percent')"></span>
|
||||
<sup>
|
||||
<q-icon name="info" size="16px" class="q-ml-xs"></q-icon>
|
||||
<q-tooltip max-width="300px">
|
||||
<span v-html="$t('fee_reserve_percent_hint')"></span>
|
||||
</q-tooltip>
|
||||
</sup>
|
||||
</p>
|
||||
<q-input
|
||||
type="number"
|
||||
filled
|
||||
name="lnbits_reserve_fee_percent"
|
||||
v-model="formData.lnbits_reserve_fee_percent"
|
||||
:label="$t('reserve_fee_in_percent')"
|
||||
step="0.1"
|
||||
suffix="%"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
</div>
|
||||
<div class="row q-col-gutter-md q-mt-sm">
|
||||
<div class="col-12">
|
||||
<h6 class="q-my-none">
|
||||
<span v-text="$t('payment_timeouts')"></span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('invoice_expiry')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model.number="formData.lightning_invoice_expiry"
|
||||
type="number"
|
||||
:label="$t('invoice_expiry_label')"
|
||||
:suffix="$t('seconds')"
|
||||
mask="#######"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<p>
|
||||
<span v-text="$t('payment_wait_time')"></span>
|
||||
<sup>
|
||||
<q-icon name="info" size="16px" class="q-ml-xs"></q-icon>
|
||||
<q-tooltip max-width="150px">
|
||||
<q-tooltip max-width="300px">
|
||||
<span v-text="$t('payment_wait_time_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
</sup>
|
||||
@@ -126,8 +154,29 @@
|
||||
filled
|
||||
name="lnbits_funding_source_pay_invoice_wait_seconds"
|
||||
v-model="formData.lnbits_funding_source_pay_invoice_wait_seconds"
|
||||
:label="$t('payment_wait_time')"
|
||||
:hint="$t('payment_wait_time_desc')"
|
||||
:suffix="$t('seconds')"
|
||||
step="1"
|
||||
min="0"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p>
|
||||
<span v-text="$t('payment_pending_interval')"></span>
|
||||
<sup>
|
||||
<q-icon name="info" size="16px" class="q-ml-xs"></q-icon>
|
||||
<q-tooltip max-width="150px">
|
||||
<span v-text="$t('payment_pending_interval_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
</sup>
|
||||
</p>
|
||||
<q-input
|
||||
type="number"
|
||||
filled
|
||||
name="lnbits_funding_source_pending_interval_seconds"
|
||||
v-model="formData.lnbits_funding_source_pending_interval_seconds"
|
||||
:label="$t('payment_pending_interval')"
|
||||
:hint="$t('payment_pending_interval_desc')"
|
||||
step="1"
|
||||
min="0"
|
||||
></q-input>
|
||||
@@ -165,6 +214,11 @@
|
||||
<h6 class="q-mt-lg q-mb-sm">
|
||||
<p v-text="$t('watchdog')"></p>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<p v-text="$t('watchdog_introduction')"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
@@ -239,6 +293,7 @@
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_watchdog_delta"
|
||||
:suffix="$t('sats')"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
@@ -12,11 +12,14 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p>Active Funding<small> (Requires server restart)</small></p>
|
||||
<p>
|
||||
<span v-text="$t('funding_source')"></span>
|
||||
<small><span v-text="$t('requires_server_restart')"></span></small>
|
||||
</p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_backend_wallet_class"
|
||||
hint="Select the active funding wallet"
|
||||
:hint="$t('funding_source_info')"
|
||||
:options="sortedAllowedFundingSources"
|
||||
:option-label="item => getFundingSourceLabel(item)"
|
||||
></q-select>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<template id="lnbits-admin-security">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none">
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('server_management')"></span>
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('base_url')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model.number="formData.lnbits_baseurl"
|
||||
@@ -17,7 +16,7 @@
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('authentication')"></span>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="row q-col-gutter-sm q-mb-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
@@ -193,6 +192,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section
|
||||
v-if="formData.auth_allowed_methods?.includes('oidc-auth')"
|
||||
class="q-pl-xl"
|
||||
>
|
||||
<strong class="q-my-none q-mb-sm">OIDC Auth</strong>
|
||||
|
||||
<div class="row q-col-gutter-sm q-col-gutter-y-md">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.oidc_discovery_url"
|
||||
:label="$t('auth_oidc_label')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.oidc_client_id"
|
||||
:label="$t('auth_oidc_ci_label')"
|
||||
:hint="$t('auth_oidc_ci_hint')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.oidc_client_secret"
|
||||
type="password"
|
||||
:label="$t('auth_oidc_cs_label')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.oidc_client_custom_org"
|
||||
:label="$t('auth_oidc_custom_org_label')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.oidc_client_custom_icon"
|
||||
:label="$t('auth_oidc_custom_icon_label')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section class="q-pa-none">
|
||||
<br />
|
||||
@@ -305,6 +355,7 @@
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_rate_limit_no"
|
||||
:label="$t('number_of_requests')"
|
||||
:hint="$t('number_of_requests_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
|
||||
@@ -38,14 +38,11 @@
|
||||
</div>
|
||||
|
||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||
<h6 class="q-my-none">
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('payments')"></span>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('max_outgoing_payment_amount')"></span>
|
||||
</p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -57,10 +54,7 @@
|
||||
></q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('max_incoming_payment_amount')"></span>
|
||||
</p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -75,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||
<h6 class="q-my-none">
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('wallet_limiter')"></span>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
@@ -113,35 +107,32 @@
|
||||
</div>
|
||||
|
||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||
<h6 class="q-my-none">
|
||||
<span v-text="$t('service_fee')"></span>
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('service_fees')"></span>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('service_fee')"></span></p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_service_fee"
|
||||
:label="$t('service_fee_label')"
|
||||
step="0.1"
|
||||
:hint="$t('service_fee_hint')"
|
||||
:suffix="'%'"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('service_fee_max')"></span></p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_service_fee_max"
|
||||
:label="$t('service_fee_max_label')"
|
||||
:hint="$t('service_fee_max_hint')"
|
||||
:suffix="$t('sats')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('fee_wallet')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_service_fee_wallet"
|
||||
@@ -151,16 +142,9 @@
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>
|
||||
<span v-text="$t('disable_fee_internal')"></span>
|
||||
</p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('disable_fee')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('disable_fee_internal_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
<br />
|
||||
<div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-5">
|
||||
<p><span v-text="$t('ui_site_title')"></span></p>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
@@ -18,8 +17,7 @@
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<p><span v-text="$t('ui_site_tagline')"></span></p>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
@@ -28,7 +26,9 @@
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-2 q-mt-xl">
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-12">
|
||||
<q-toggle
|
||||
:tip="$t('ui_toggle_elements_tip')"
|
||||
v-model="formData.lnbits_show_home_page_elements"
|
||||
@@ -40,78 +40,66 @@
|
||||
></q-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
<span v-text="$t('ui_site_description')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
v-model="formData.lnbits_site_description"
|
||||
filled
|
||||
type="textarea"
|
||||
:hint="$t('ui_site_description_hint')"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
v-model="formData.lnbits_site_description"
|
||||
filled
|
||||
type="textarea"
|
||||
:label="$t('ui_site_description')"
|
||||
:hint="$t('ui_site_description_hint')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p>
|
||||
<span v-text="$t('ui_default_wallet_name')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_default_wallet_name"
|
||||
:label="$t('lnbits_wallet')"
|
||||
:label="$t('ui_default_wallet_name')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_qr_logo"
|
||||
label="https://example.com/image.svg"
|
||||
:label="$t('ui_qr_code_logo')"
|
||||
:hint="$t('ui_qr_code_logo_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('ui_apple_touch_icon')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_apple_touch_icon"
|
||||
label="https://example.com/image.png"
|
||||
:label="$t('ui_apple_touch_icon')"
|
||||
:hint="$t('ui_apple_touch_icon_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-12">
|
||||
<p><span v-text="$t('wallet_featured_button_title')"></span></p>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p>
|
||||
<span v-text="$t('wallet_featured_button_label')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_wallet_featured_button_label"
|
||||
label="Loop to Onchain"
|
||||
:label="$t('wallet_featured_button_label')"
|
||||
:hint="$t('wallet_featured_button_label_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('wallet_featured_button_url')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_wallet_featured_button_url"
|
||||
label="/boltz"
|
||||
:label="$t('wallet_featured_button_url')"
|
||||
:hint="$t('wallet_featured_button_url_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('wallet_featured_button_icon')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
@@ -121,21 +109,27 @@
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('ui_custom_badge')"></span></p>
|
||||
<div class="col-12 col-md-12">
|
||||
<h6 class="q-my-sm">
|
||||
<span v-text="$t('ui_custom_badge_title')"></span>
|
||||
</h6>
|
||||
<p class="q-mb-none">
|
||||
<span v-text="$t('ui_custom_badge_desc')"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
tip="Custom Badge"
|
||||
v-model.trim="formData.lnbits_custom_badge"
|
||||
:label="$t('ui_custom_badge_label')"
|
||||
:label="$t('ui_custom_badge')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_custom_badge_color"
|
||||
@@ -145,8 +139,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('ui_custom_image')"></span></p>
|
||||
<div class="col-12">
|
||||
<h6 class="q-my-sm"><span v-text="$t('ui_custom_image')"></span></h6>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
@@ -157,10 +151,8 @@
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('themes')"></span></p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_theme_options"
|
||||
@@ -172,41 +164,26 @@
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('custom_logo')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_custom_logo"
|
||||
label="https://example.com/image.png"
|
||||
:label="$t('custom_logo')"
|
||||
:hint="$t('custom_logo_hint')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('ad_space_title')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_ad_space_title"
|
||||
:label="$t('ad_space_title_label')"
|
||||
></q-input>
|
||||
<br />
|
||||
<div class="col-12 col-md-12">
|
||||
<h6 class="q-my-sm">
|
||||
<span v-text="$t('ad_space_section_title')"></span>
|
||||
</h6>
|
||||
<p class="q-my-none">
|
||||
<span v-text="$t('ad_space_section_desc')"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('ad_slots')"></span></p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formData.lnbits_ad_space"
|
||||
type="text"
|
||||
:label="$t('ad_slots_label')"
|
||||
:hint="$t('ad_slots_hint')"
|
||||
><q-tooltip>
|
||||
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
|
||||
</q-tooltip>
|
||||
</q-input>
|
||||
<div class="col-12 col-md-12">
|
||||
<q-toggle
|
||||
v-model="formData.lnbits_ad_space_enabled"
|
||||
:label="
|
||||
@@ -215,8 +192,30 @@
|
||||
: $t('ads_disabled')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_ad_space_title"
|
||||
:label="$t('ad_space_title')"
|
||||
:hint="$t('ad_space_title_hint')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formData.lnbits_ad_space"
|
||||
type="text"
|
||||
:label="$t('ad_slots')"
|
||||
:hint="$t('ad_slots_hint')"
|
||||
><q-tooltip>
|
||||
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
|
||||
</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md q-my-md">
|
||||
<div class="col-12" v-text="$t('ui_default_theme')"></div>
|
||||
|
||||
@@ -27,12 +27,9 @@
|
||||
></q-btn>
|
||||
<q-toolbar-title>
|
||||
<q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
|
||||
<q-img
|
||||
v-if="customLogo"
|
||||
height="30px"
|
||||
alt="Logo"
|
||||
:src="customLogo"
|
||||
></q-img>
|
||||
<q-avatar v-if="g.settings.customLogo" height="30px">
|
||||
<img alt="Logo" :src="g.settings.customLogo" />
|
||||
</q-avatar>
|
||||
<span v-else-if="g.settings.siteTitle == 'LNbits'"
|
||||
><strong>LN</strong>bits</span
|
||||
>
|
||||
|
||||
@@ -262,7 +262,9 @@
|
||||
<div
|
||||
v-if="
|
||||
'google-auth' in g.settings.authMethods ||
|
||||
'github-auth' in g.settings.authMethods
|
||||
'github-auth' in g.settings.authMethods ||
|
||||
'keycloak-auth' in g.settings.authMethods ||
|
||||
'oidc-auth' in g.settings.authMethods
|
||||
"
|
||||
class="col q-pa-sm text-h6"
|
||||
>
|
||||
@@ -310,6 +312,58 @@
|
||||
<div>GitHub</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div
|
||||
v-if="'keycloak-auth' in g.settings.authMethods"
|
||||
class="col q-pa-sm"
|
||||
>
|
||||
<q-btn
|
||||
:href="`/api/v1/auth/keycloak?user_id=${g.user.id}`"
|
||||
type="a"
|
||||
outline
|
||||
no-caps
|
||||
color="grey"
|
||||
rounded
|
||||
class="full-width"
|
||||
>
|
||||
<q-avatar size="32px" class="q-mr-md">
|
||||
<q-img
|
||||
:src="
|
||||
g.settings.keycloakIcon
|
||||
? g.settings.keycloakIcon
|
||||
: '{{ static_url_for('static', 'images/keycloak-logo.png') }}'
|
||||
"
|
||||
></q-img>
|
||||
</q-avatar>
|
||||
<div
|
||||
v-text="g.settings.keycloakOrg || 'Keycloak'"
|
||||
></div>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div
|
||||
v-if="'oidc-auth' in g.settings.authMethods"
|
||||
class="col q-pa-sm"
|
||||
>
|
||||
<q-btn
|
||||
:href="`/api/v1/auth/oidc?user_id=${g.user.id}`"
|
||||
type="a"
|
||||
outline
|
||||
no-caps
|
||||
color="grey"
|
||||
rounded
|
||||
class="full-width"
|
||||
>
|
||||
<q-avatar size="32px" class="q-mr-md">
|
||||
<q-img
|
||||
:src="
|
||||
g.settings.oidcIcon
|
||||
? g.settings.oidcIcon
|
||||
: '{{ static_url_for('static', 'images/generic-oidc-logo.svg') }}'
|
||||
"
|
||||
></q-img>
|
||||
</q-avatar>
|
||||
<div v-text="g.settings.oidcOrg || 'OIDC'"></div>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
|
||||
@@ -438,7 +438,7 @@
|
||||
<q-card-section>
|
||||
<div v-if="selectedRelease.paymentRequest">
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()"
|
||||
:value="'LIGHTNING:' + selectedRelease.paymentRequest.toUpperCase()"
|
||||
:href="'lightning:' + selectedRelease.paymentRequest"
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
@@ -836,7 +836,7 @@
|
||||
<div v-if="selectedExtension.payToEnable.paymentRequest" class="col">
|
||||
<lnbits-qrcode
|
||||
:value="
|
||||
'lightning:' +
|
||||
'LIGHTNING:' +
|
||||
selectedExtension.payToEnable.paymentRequest.toUpperCase()
|
||||
"
|
||||
:href="
|
||||
@@ -1247,13 +1247,10 @@
|
||||
<q-dialog v-model="paymentDialog.show" position="top">
|
||||
<q-card class="q-pa-md lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-xl">
|
||||
<lnbits-qrcode
|
||||
:value="paymentDialog.invoice"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
<lnbits-qrcode
|
||||
:value="'LIGHTNING:' + paymentDialog.invoice.toUpperCase()"
|
||||
:href="'lightning:' + paymentDialog.invoice"
|
||||
></lnbits-qrcode>
|
||||
</q-card-section>
|
||||
<q-card-actions align="between">
|
||||
<q-btn v-close-popup flat color="grey" :label="$t('close')"></q-btn>
|
||||
|
||||
@@ -589,23 +589,16 @@
|
||||
v-if="transactionDetailsDialog.data.bolt11"
|
||||
class="text-center q-mb-lg"
|
||||
>
|
||||
<a
|
||||
<lnbits-qrcode
|
||||
:href="
|
||||
'lightning:' +
|
||||
transactionDetailsDialog.data.bolt11
|
||||
"
|
||||
>
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode-vue
|
||||
:value="
|
||||
'lightning:' +
|
||||
transactionDetailsDialog.data.bolt11.toUpperCase()
|
||||
"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode-vue>
|
||||
</q-responsive>
|
||||
</a>
|
||||
:value="
|
||||
'LIGHTNING:' +
|
||||
transactionDetailsDialog.data.bolt11.toUpperCase()
|
||||
"
|
||||
></lnbits-qrcode>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@@ -698,25 +691,15 @@
|
||||
v-if="props.row.bolt11"
|
||||
class="text-center q-mb-lg"
|
||||
>
|
||||
<a
|
||||
<lnbits-qrcode
|
||||
:value="
|
||||
'LIGHTNING:' +
|
||||
props.row.bolt11.toUpperCase()
|
||||
"
|
||||
:href="
|
||||
'lightning:' + props.row.bolt11
|
||||
"
|
||||
>
|
||||
<q-responsive
|
||||
:ratio="1"
|
||||
class="q-mx-xl"
|
||||
>
|
||||
<qrcode-vue
|
||||
:value="
|
||||
'lightning:' +
|
||||
props.row.bolt11.toUpperCase()
|
||||
"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode-vue>
|
||||
</q-responsive>
|
||||
</a>
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
</q-card>
|
||||
</div>
|
||||
<div
|
||||
v-if="!g.mobileSimple || !$q.screen.lt.md"
|
||||
v-show="!g.mobileSimple || !$q.screen.lt.md"
|
||||
class="col-12 col-md-5 q-gutter-y-md"
|
||||
>
|
||||
<lnbits-wallet-extra
|
||||
@@ -438,7 +438,7 @@
|
||||
<lnbits-qrcode
|
||||
v-else
|
||||
:href="'lightning:' + receive.paymentReq"
|
||||
:value="'lightning:' + receive.paymentReq"
|
||||
:value="'LIGHTNING:' + receive.paymentReq.toUpperCase()"
|
||||
>
|
||||
</lnbits-qrcode>
|
||||
<div class="text-center">
|
||||
|
||||
@@ -69,7 +69,7 @@ def decrypt_content(
|
||||
1:
|
||||
]
|
||||
# extract iv and content
|
||||
(encrypted_content_b64, iv_b64) = content.split("?iv=")
|
||||
encrypted_content_b64, iv_b64 = content.split("?iv=")
|
||||
encrypted_content = base64.b64decode(encrypted_content_b64.encode("ascii"))
|
||||
iv = base64.b64decode(iv_b64.encode("ascii"))
|
||||
# Decrypt
|
||||
|
||||
@@ -8,6 +8,8 @@ from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from coincurve.keys import PrivateKey
|
||||
from embit.bip39 import mnemonic_from_bytes, mnemonic_is_valid
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.helpers import normalize_endpoint
|
||||
@@ -79,6 +81,11 @@ class SparkL2Wallet(Wallet):
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
res = await self._request("POST", "/v1/balance")
|
||||
status = res.get("status")
|
||||
if status == "missing_mnemonic":
|
||||
await self._check_sidecar_mnemonic()
|
||||
return StatusResponse("Spark sidecar mnemonic not set", 0)
|
||||
|
||||
balance_msat = res.get("balance_msat")
|
||||
if balance_msat is not None:
|
||||
return StatusResponse(None, int(balance_msat))
|
||||
@@ -136,6 +143,9 @@ class SparkL2Wallet(Wallet):
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
try:
|
||||
max_fee_sats = (int(fee_limit_msat) + 999) // 1000
|
||||
logger.info(
|
||||
f"Paying invoice via Spark sidecar with max fee {max_fee_sats} sats."
|
||||
)
|
||||
|
||||
payment_hash = None
|
||||
try:
|
||||
@@ -237,7 +247,7 @@ class SparkL2Wallet(Wallet):
|
||||
) -> dict[str, Any]:
|
||||
error_message = None
|
||||
try:
|
||||
r = await self.client.request(method, path, json=json_data)
|
||||
r = await self.client.request(method, path, json=json_data, timeout=30)
|
||||
r.raise_for_status()
|
||||
j = r.json()
|
||||
except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as exc:
|
||||
@@ -319,3 +329,30 @@ class SparkL2Wallet(Wallet):
|
||||
if mapped.failed:
|
||||
return False
|
||||
return None
|
||||
|
||||
async def _check_sidecar_mnemonic(self):
|
||||
if settings.spark_l2_mnemonic:
|
||||
valid = mnemonic_is_valid(settings.spark_l2_mnemonic)
|
||||
if not valid:
|
||||
logger.warning("SPARK_L2_MNEMONIC is set but invalid. Please recheck!")
|
||||
return
|
||||
await self._set_sidecar_mnemonic(settings.spark_l2_mnemonic)
|
||||
return
|
||||
|
||||
logger.info("SPARK_L2_MNEMONIC is not set, one will be generated for you.")
|
||||
mnemonic = mnemonic_from_bytes(PrivateKey().secret)
|
||||
await self._set_sidecar_mnemonic(mnemonic)
|
||||
|
||||
async def _set_sidecar_mnemonic(self, mnemonic: str):
|
||||
logger.info("Checking 'SPARK_L2_MNEMONIC' on the Spark sidecar.")
|
||||
payload = {"mnemonic": mnemonic}
|
||||
resp = await self._request("POST", "/v1/mnemonic", payload)
|
||||
status = resp.get("status")
|
||||
logger.info(f"Spark sidecar mnemonic status: {status}")
|
||||
if status == "set":
|
||||
logger.info("Updating 'SPARK_L2_MNEMONIC' mnemonic settings.")
|
||||
from lnbits.core.crud.settings import set_settings_field
|
||||
|
||||
await set_settings_field("spark_l2_mnemonic", mnemonic)
|
||||
else:
|
||||
logger.info("Nothing to do for 'SPARK_L2_MNEMONIC' on the Spark sidecar.")
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"quasar": "2.18.6",
|
||||
"showdown": "^2.1.0",
|
||||
"underscore": "^1.13.7",
|
||||
"underscore": "^1.13.8",
|
||||
"vue": "3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-qrcode-reader": "^5.7.3",
|
||||
@@ -1260,9 +1260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1386,9 +1386,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -1722,9 +1722,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.7",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
|
||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="
|
||||
"version": "1.13.8",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
|
||||
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.25",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"quasar": "2.18.6",
|
||||
"showdown": "^2.1.0",
|
||||
"underscore": "^1.13.7",
|
||||
"underscore": "^1.13.8",
|
||||
"vue": "3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-qrcode-reader": "^5.7.3",
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
[project]
|
||||
name = "lnbits"
|
||||
version = "1.5.0"
|
||||
version = "1.5.2-rc3"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||
urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/lnbits" }
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"bech32==1.2.0",
|
||||
"click==8.3.1",
|
||||
"fastapi==0.116.1",
|
||||
"starlette==0.47.1",
|
||||
"httpx==0.27.2",
|
||||
"jinja2==3.1.6",
|
||||
"lnurl==0.8.3",
|
||||
"pydantic==1.10.26",
|
||||
"pyqrcode==1.2.1",
|
||||
"shortuuid==1.0.13",
|
||||
"sse-starlette==2.3.6",
|
||||
"typing-extensions==4.15.0",
|
||||
"uvicorn==0.40.0",
|
||||
"sqlalchemy==1.4.54",
|
||||
"aiosqlite==0.22.1",
|
||||
"asyncpg==0.31.0",
|
||||
"uvloop==0.22.1",
|
||||
"websockets==15.0.1",
|
||||
"loguru==0.7.3",
|
||||
"grpcio==1.76.0",
|
||||
"protobuf==6.33.2",
|
||||
"pyln-client==25.12",
|
||||
"pywebpush==2.2.0",
|
||||
"slowapi==0.1.9",
|
||||
"websocket-client==1.9.0",
|
||||
"pycryptodomex==3.23.0",
|
||||
"packaging==25.0",
|
||||
"bolt11==2.1.1",
|
||||
"pyjwt==2.10.1",
|
||||
"itsdangerous==2.2.0",
|
||||
"fastapi-sso==0.19.0",
|
||||
"bech32~=1.2.0",
|
||||
"click~=8.3.1",
|
||||
"fastapi~=0.116.1",
|
||||
"starlette~=0.47.1",
|
||||
"httpx~=0.27.2",
|
||||
"jinja2~=3.1.6",
|
||||
"lnurl~=0.10.0",
|
||||
"pydantic~=1.10.26",
|
||||
"pyqrcode~=1.2.1",
|
||||
"shortuuid~=1.0.13",
|
||||
"sse-starlette~=2.3.6",
|
||||
"typing-extensions~=4.15.0",
|
||||
"uvicorn~=0.40.0",
|
||||
"sqlalchemy~=1.4.54",
|
||||
"aiosqlite~=0.22.1",
|
||||
"asyncpg~=0.31.0",
|
||||
"uvloop~=0.22.1",
|
||||
"websockets~=15.0.1",
|
||||
"loguru~=0.7.3",
|
||||
"grpcio~=1.76.0",
|
||||
"protobuf~=6.33.5",
|
||||
"pyln-client~=25.12.0",
|
||||
"pywebpush~=2.2.0",
|
||||
"slowapi~=0.1.9",
|
||||
"websocket-client~=1.9.0",
|
||||
"pycryptodomex~=3.23.0",
|
||||
"packaging~=25.0.0",
|
||||
"bolt11~=2.1.1",
|
||||
"pyjwt~=2.12.0",
|
||||
"itsdangerous~=2.2.0",
|
||||
"fastapi-sso~=0.19.0",
|
||||
# needed for boltz, lnurldevice, watchonly extensions
|
||||
"embit==0.8.0",
|
||||
"embit~=0.8.0",
|
||||
# needed for scheduler extension
|
||||
"python-crontab==3.3.0",
|
||||
"pynostr==0.7.0",
|
||||
"python-multipart==0.0.21",
|
||||
"filetype==1.2.0",
|
||||
"nostr-sdk==0.44.0",
|
||||
"bcrypt==5.0.0",
|
||||
"jsonpath-ng==1.7.0",
|
||||
"pillow>=12.1.0",
|
||||
"python-dotenv>=1.2.1",
|
||||
"greenlet (>=3.3.0,<4.0.0)",
|
||||
"python-crontab~=3.3.0",
|
||||
"pynostr~=0.7.0",
|
||||
"python-multipart~=0.0.22",
|
||||
"filetype~=1.2.0",
|
||||
"nostr-sdk~=0.44.0",
|
||||
"bcrypt~=5.0.0",
|
||||
"jsonpath-ng~=1.7.0",
|
||||
"pillow~=12.1.0",
|
||||
"python-dotenv~=1.2.1",
|
||||
"greenlet~=3.3.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -58,31 +58,31 @@ lnbits = "lnbits.server:main"
|
||||
lnbits-cli = "lnbits.commands:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
breez = ["breez-sdk==0.8.0", "breez-sdk-liquid==0.11.11"]
|
||||
liquid = ["wallycore==1.5.1"]
|
||||
migration = ["psycopg2-binary==2.9.11"]
|
||||
breez = ["breez-sdk~=0.8.0", "breez-sdk-liquid~=0.11.11"]
|
||||
liquid = ["wallycore~=1.5.1"]
|
||||
migration = ["psycopg2-binary~=2.9.11"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"black>=25.12.0,<26.0.0",
|
||||
"mypy==1.17.1",
|
||||
"types-protobuf>=6.32.1.20251210,<7.0.0",
|
||||
"pre-commit>=4.5.1,<5.0.0",
|
||||
"openapi-spec-validator>=0.7.2,<1.0.0",
|
||||
"ruff>=0.14.10,<1.0.0",
|
||||
"types-passlib>=1.7.7.20250602,<2.0.0",
|
||||
"openai>=2.14.0",
|
||||
"json5>=0.13.0,<1.0.0",
|
||||
"asgi-lifespan>=2.1.0,<3.0.0",
|
||||
"anyio>=4.12.1",
|
||||
"pytest>=9.0.2",
|
||||
"pytest-cov>=7.0.0",
|
||||
"pytest-md>=0.2.0,<0.3.0",
|
||||
"pytest-httpserver>=1.1.3,<2.0.0",
|
||||
"pytest-mock>=3.15.1,<4.0.0",
|
||||
"types-mock>=5.2.0.20250924,<6.0.0",
|
||||
"mock>=5.2.0,<6.0.0",
|
||||
"grpcio-tools>=1.76.0,<2.0.0"
|
||||
"black~=26.3.1",
|
||||
"mypy~=1.17.1",
|
||||
"types-protobuf~=6.32.1.20251210",
|
||||
"pre-commit~=4.5.1",
|
||||
"openapi-spec-validator~=0.7.2",
|
||||
"ruff~=0.14.10",
|
||||
"types-passlib~=1.7.7.20250602",
|
||||
"openai~=2.14.0",
|
||||
"json5~=0.13.0",
|
||||
"asgi-lifespan~=2.1.0",
|
||||
"anyio~=4.12.1",
|
||||
"pytest~=9.0.2",
|
||||
"pytest-cov~=7.0.0",
|
||||
"pytest-md~=0.2.0",
|
||||
"pytest-httpserver~=1.1.3",
|
||||
"pytest-mock~=3.15.1",
|
||||
"types-mock~=5.2.0.20250924",
|
||||
"mock~=5.2.0",
|
||||
"grpcio-tools~=1.76.0"
|
||||
]
|
||||
|
||||
[tool.poetry]
|
||||
|
||||
@@ -43,6 +43,7 @@ def anyio_backend():
|
||||
@pytest.fixture(scope="session")
|
||||
def settings():
|
||||
# override settings for tests
|
||||
lnbits_settings.auth_https_only = False
|
||||
lnbits_settings.lnbits_admin_extensions = []
|
||||
lnbits_settings.lnbits_data_folder = "./tests/data"
|
||||
lnbits_settings.lnbits_admin_ui = True
|
||||
@@ -61,6 +62,7 @@ def run_before_and_after_tests(settings: Settings):
|
||||
|
||||
|
||||
# use session scope to run once before and once after all tests
|
||||
# random change
|
||||
@pytest.fixture(scope="session")
|
||||
async def app(settings: Settings):
|
||||
app = create_app()
|
||||
|
||||
@@ -6,10 +6,6 @@ from pydantic import BaseModel
|
||||
from lnbits.wallets import get_funding_source, set_funding_source
|
||||
|
||||
|
||||
class FakeError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DbTestModel(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import hashlib
|
||||
|
||||
import pytest
|
||||
from pytest_mock.plugin import MockerFixture
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.crud import get_standalone_payment, update_payment
|
||||
@@ -18,7 +19,7 @@ from lnbits.exceptions import PaymentError
|
||||
from lnbits.tasks import create_task, wait_for_paid_invoices
|
||||
from lnbits.wallets import get_funding_source
|
||||
|
||||
from ..helpers import FakeError, is_fake, is_regtest
|
||||
from ..helpers import is_fake, is_regtest
|
||||
from .helpers import (
|
||||
cancel_invoice,
|
||||
get_real_invoice,
|
||||
@@ -138,7 +139,9 @@ async def test_pay_real_invoice_mainnet(
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_from):
|
||||
async def test_create_real_invoice(
|
||||
client, adminkey_headers_from, inkey_headers_from, mocker: MockerFixture
|
||||
):
|
||||
prev_balance = await get_node_balance_sats()
|
||||
create_invoice = CreateInvoice(out=False, amount=1000, memo="test")
|
||||
response = await client.post(
|
||||
@@ -156,34 +159,32 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
|
||||
payment_status = response.json()
|
||||
assert not payment_status["paid"]
|
||||
|
||||
async def on_paid(payment: Payment):
|
||||
on_paid_mock = mocker.AsyncMock()
|
||||
create_task(wait_for_paid_invoices("test_create_invoice", on_paid_mock)())
|
||||
|
||||
assert payment.payment_hash == invoice["payment_hash"]
|
||||
assert payment.checking_id == invoice["checking_id"]
|
||||
|
||||
response = await client.get(
|
||||
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
|
||||
)
|
||||
assert response.status_code < 300
|
||||
payment_status = response.json()
|
||||
assert payment_status["paid"]
|
||||
|
||||
await asyncio.sleep(1)
|
||||
balance = await get_node_balance_sats()
|
||||
fee = abs(payment_status.get("details", {}).get("fee", 0) // 1000)
|
||||
assert balance - prev_balance == create_invoice.amount - fee
|
||||
|
||||
assert payment_status.get("preimage") is not None
|
||||
|
||||
# exit out of infinite loop
|
||||
raise FakeError()
|
||||
|
||||
task = create_task(wait_for_paid_invoices("test_create_invoice", on_paid)())
|
||||
pay_real_invoice(invoice["bolt11"])
|
||||
|
||||
# wait for the task to exit
|
||||
with pytest.raises(FakeError):
|
||||
await task
|
||||
await asyncio.sleep(1)
|
||||
|
||||
assert on_paid_mock.call_count == 1
|
||||
payment = on_paid_mock.call_args_list[0][0][0]
|
||||
|
||||
assert payment.payment_hash == invoice["payment_hash"]
|
||||
assert payment.checking_id == invoice["checking_id"]
|
||||
|
||||
response = await client.get(
|
||||
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
|
||||
)
|
||||
assert response.status_code < 300
|
||||
payment_status = response.json()
|
||||
assert payment_status["paid"]
|
||||
|
||||
await asyncio.sleep(1)
|
||||
balance = await get_node_balance_sats()
|
||||
fee = abs(payment_status.get("details", {}).get("fee", 0) // 1000)
|
||||
assert balance - prev_balance == create_invoice.amount - fee
|
||||
|
||||
assert payment_status.get("preimage") is not None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -367,7 +368,7 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
async def test_receive_real_invoice_set_pending_and_check_state(
|
||||
client, adminkey_headers_from, inkey_headers_from
|
||||
client, adminkey_headers_from, inkey_headers_from, mocker: MockerFixture
|
||||
):
|
||||
"""
|
||||
1. We create a real invoice
|
||||
@@ -391,39 +392,36 @@ async def test_receive_real_invoice_set_pending_and_check_state(
|
||||
payment_status = response.json()
|
||||
assert not payment_status["paid"]
|
||||
|
||||
async def on_paid(payment: Payment):
|
||||
on_paid_mock = mocker.AsyncMock()
|
||||
create_task(wait_for_paid_invoices("test_create_invoice", on_paid_mock)())
|
||||
|
||||
assert payment.payment_hash == invoice["payment_hash"]
|
||||
assert payment.checking_id == invoice["checking_id"]
|
||||
|
||||
response = await client.get(
|
||||
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
|
||||
)
|
||||
assert response.status_code < 300
|
||||
payment_status = response.json()
|
||||
assert payment_status["paid"]
|
||||
|
||||
assert payment
|
||||
|
||||
# set the incoming invoice to pending
|
||||
payment.status = PaymentState.PENDING
|
||||
await update_payment(payment)
|
||||
|
||||
payment_pending = await get_standalone_payment(
|
||||
invoice["payment_hash"], incoming=True
|
||||
)
|
||||
assert payment_pending
|
||||
assert payment_pending.success is False
|
||||
assert payment_pending.failed is False
|
||||
|
||||
# exit out of infinite loop
|
||||
raise FakeError()
|
||||
|
||||
task = create_task(wait_for_paid_invoices("test_create_invoice", on_paid)())
|
||||
pay_real_invoice(invoice["bolt11"])
|
||||
|
||||
with pytest.raises(FakeError):
|
||||
await task
|
||||
await asyncio.sleep(1)
|
||||
|
||||
assert on_paid_mock.call_count == 1
|
||||
payment = on_paid_mock.call_args_list[0][0][0]
|
||||
|
||||
assert payment.payment_hash == invoice["payment_hash"]
|
||||
assert payment.checking_id == invoice["checking_id"]
|
||||
|
||||
response = await client.get(
|
||||
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
|
||||
)
|
||||
assert response.status_code < 300
|
||||
payment_status = response.json()
|
||||
assert payment_status["paid"]
|
||||
|
||||
# set the incoming invoice to pending
|
||||
payment.status = PaymentState.PENDING
|
||||
await update_payment(payment)
|
||||
|
||||
payment_pending = await get_standalone_payment(
|
||||
invoice["payment_hash"], incoming=True
|
||||
)
|
||||
assert payment_pending
|
||||
assert payment_pending.success is False
|
||||
assert payment_pending.failed is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -2,10 +2,16 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from lnbits.core.crud.users import get_user_from_account
|
||||
from lnbits.core.crud.wallets import delete_wallet, get_wallets
|
||||
from lnbits.core.models.users import Account
|
||||
from lnbits.core.services.users import create_user_account
|
||||
from lnbits.core.crud.users import (
|
||||
create_account,
|
||||
delete_account,
|
||||
get_accounts,
|
||||
get_user_from_account,
|
||||
)
|
||||
from lnbits.core.crud.wallets import delete_wallet, force_delete_wallet, get_wallets
|
||||
from lnbits.core.models.users import Account, AccountFilters
|
||||
from lnbits.core.services.users import create_user_account, create_user_account_no_ckeck
|
||||
from lnbits.db import Filter, Filters, Operator
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -36,3 +42,151 @@ async def test_get_user_from_account_is_wallet_created():
|
||||
assert (
|
||||
len(user.wallets) == 1
|
||||
), "A new wallet should be created for the user if none exist after deletion"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_accounts_success_flow():
|
||||
# Create a new account
|
||||
username = f"user_{uuid4().hex[:8]}"
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
username=username,
|
||||
email=f"{username}@lnbits.com",
|
||||
)
|
||||
await create_account(account)
|
||||
# Should return the created account
|
||||
filters = Filters[AccountFilters](filters=[], model=AccountFilters)
|
||||
filters.sortby = "created_at"
|
||||
filters.direction = "desc"
|
||||
page = await get_accounts(filters=filters)
|
||||
assert page.total >= 1
|
||||
found = any(a.username == username for a in page.data)
|
||||
assert found
|
||||
await delete_account(account.id)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_accounts_with_wallet_id_filter():
|
||||
# Create account and wallet
|
||||
username = f"user_{uuid4().hex[:8]}"
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
username=username,
|
||||
email=f"{username}@lnbits.com",
|
||||
)
|
||||
await create_user_account_no_ckeck(account)
|
||||
|
||||
wallets = await get_wallets(account.id, deleted=False)
|
||||
assert wallets
|
||||
wallet = wallets[0]
|
||||
# Filter by wallet_id
|
||||
filters = Filters[AccountFilters](
|
||||
filters=[
|
||||
Filter(
|
||||
field="wallet_id",
|
||||
op=Operator.EQ,
|
||||
model=AccountFilters,
|
||||
values={"wallet_id__0": wallet.id},
|
||||
)
|
||||
],
|
||||
model=AccountFilters,
|
||||
)
|
||||
page = await get_accounts(filters=filters)
|
||||
assert page.total == 1
|
||||
assert page.data[0].id == account.id
|
||||
await delete_account(account.id)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_accounts_wallet_id_not_found():
|
||||
|
||||
filters = Filters[AccountFilters](
|
||||
filters=[
|
||||
Filter(
|
||||
field="wallet_id",
|
||||
op=Operator.EQ,
|
||||
model=AccountFilters,
|
||||
values={"wallet_id__0": uuid4().hex},
|
||||
)
|
||||
],
|
||||
model=AccountFilters,
|
||||
)
|
||||
page = await get_accounts(filters=filters)
|
||||
assert page.total == 0
|
||||
assert page.data == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_accounts_empty_filters():
|
||||
# Should not raise, should return a Page
|
||||
page = await get_accounts()
|
||||
assert hasattr(page, "data")
|
||||
assert hasattr(page, "total")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_accounts_with_deleted_wallet():
|
||||
# Create account and wallet, then delete wallet
|
||||
username = f"user_{uuid4().hex[:8]}"
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
username=username,
|
||||
email=f"{username}@lnbits.com",
|
||||
)
|
||||
await create_user_account_no_ckeck(account)
|
||||
|
||||
wallets = await get_wallets(account.id, deleted=False)
|
||||
assert wallets
|
||||
wallet = wallets[0]
|
||||
await delete_wallet(user_id=account.id, wallet_id=wallet.id)
|
||||
|
||||
filters = Filters[AccountFilters](
|
||||
filters=[
|
||||
Filter(
|
||||
field="wallet_id",
|
||||
op=Operator.EQ,
|
||||
model=AccountFilters,
|
||||
values={"wallet_id__0": wallet.id},
|
||||
)
|
||||
],
|
||||
model=AccountFilters,
|
||||
)
|
||||
page = await get_accounts(filters=filters)
|
||||
assert page.total == 1
|
||||
assert page.data[0].id == account.id
|
||||
|
||||
await force_delete_wallet(wallet_id=wallet.id)
|
||||
|
||||
filters = Filters[AccountFilters](
|
||||
filters=[
|
||||
Filter(
|
||||
field="wallet_id",
|
||||
op=Operator.EQ,
|
||||
model=AccountFilters,
|
||||
values={"wallet_id__0": wallet.id},
|
||||
)
|
||||
],
|
||||
model=AccountFilters,
|
||||
)
|
||||
page = await get_accounts(filters=filters)
|
||||
assert page.total == 0
|
||||
assert page.data == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_accounts_group_by_and_pagination():
|
||||
# Create multiple accounts
|
||||
accounts = []
|
||||
for _ in range(3):
|
||||
username = f"user_{uuid4().hex[:8]}"
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
username=username,
|
||||
email=f"{username}@lnbits.com",
|
||||
)
|
||||
await create_user_account_no_ckeck(account)
|
||||
accounts.append(account)
|
||||
filters = Filters[AccountFilters](model=AccountFilters, limit=2, offset=0)
|
||||
page = await get_accounts(filters=filters)
|
||||
assert page.total >= 3
|
||||
assert len(page.data) <= 2
|
||||
|
||||
@@ -6,25 +6,21 @@ from tests.helpers import DbTestModel
|
||||
@pytest.fixture(scope="session")
|
||||
async def fetch_page(db):
|
||||
await db.execute("DROP TABLE IF EXISTS test_db_fetch_page")
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE test_db_fetch_page (
|
||||
id TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
""")
|
||||
await db.execute("""
|
||||
INSERT INTO test_db_fetch_page (id, name, value) VALUES
|
||||
('1', 'Alice', 'foo'),
|
||||
('2', 'Bob', 'bar'),
|
||||
('3', 'Carol', 'bar'),
|
||||
('4', 'Dave', 'bar'),
|
||||
('5', 'Dave', 'foo')
|
||||
"""
|
||||
)
|
||||
""")
|
||||
yield
|
||||
await db.execute("DROP TABLE test_db_fetch_page")
|
||||
|
||||
|
||||
@@ -10,16 +10,12 @@ from pytest_mock.plugin import MockerFixture
|
||||
|
||||
from lnbits.core.crud import create_wallet, get_standalone_payment, get_wallet
|
||||
from lnbits.core.crud.payments import get_payment, get_payments_paginated
|
||||
from lnbits.core.models import Payment, PaymentState, Wallet
|
||||
from lnbits.core.models import PaymentState, Wallet
|
||||
from lnbits.core.services import create_invoice, create_user_account, pay_invoice
|
||||
from lnbits.core.services.payments import update_wallet_balance
|
||||
from lnbits.exceptions import InvoiceError, PaymentError
|
||||
from lnbits.settings import Settings
|
||||
from lnbits.tasks import (
|
||||
create_permanent_task,
|
||||
internal_invoice_listener,
|
||||
register_invoice_listener,
|
||||
)
|
||||
from lnbits.tasks import create_task, wait_for_paid_invoices
|
||||
from lnbits.wallets.base import PaymentResponse
|
||||
from lnbits.wallets.fake import FakeWallet
|
||||
|
||||
@@ -230,13 +226,13 @@ async def test_pay_for_extension(to_wallet: Wallet, settings: Settings):
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_notification_for_internal_payment(to_wallet: Wallet):
|
||||
async def test_notification_for_internal_payment(
|
||||
to_wallet: Wallet, mocker: MockerFixture
|
||||
):
|
||||
test_name = "test_notification_for_internal_payment"
|
||||
|
||||
create_permanent_task(internal_invoice_listener)
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, test_name)
|
||||
|
||||
on_paid_mock = mocker.AsyncMock()
|
||||
create_task(wait_for_paid_invoices(test_name, on_paid_mock)())
|
||||
payment = await create_invoice(
|
||||
wallet_id=to_wallet.id,
|
||||
amount=123,
|
||||
@@ -248,17 +244,15 @@ async def test_notification_for_internal_payment(to_wallet: Wallet):
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
while True:
|
||||
_payment: Payment = invoice_queue.get_nowait() # raises if queue empty
|
||||
assert _payment
|
||||
if _payment.memo == test_name:
|
||||
assert _payment.status == PaymentState.SUCCESS.value
|
||||
assert _payment.bolt11 == payment.bolt11
|
||||
assert _payment.amount == 123_000
|
||||
updated_payment = await get_payment(_payment.checking_id)
|
||||
assert updated_payment.webhook_status == "404"
|
||||
assert on_paid_mock.call_count == 1
|
||||
_payment = on_paid_mock.call_args_list[0][0][0]
|
||||
|
||||
break # we found our payment, success
|
||||
assert _payment.memo == test_name
|
||||
assert _payment.status == PaymentState.SUCCESS.value
|
||||
assert _payment.bolt11 == payment.bolt11
|
||||
assert _payment.amount == 123_000
|
||||
updated_payment = await get_payment(_payment.checking_id)
|
||||
assert updated_payment.webhook_status == "404"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -40,7 +40,7 @@ def encrypt_content(priv_key, dest_pub_key, content):
|
||||
def decrypt_content(priv_key, source_pub_key, content):
|
||||
p = PublicKey(bytes.fromhex("02" + source_pub_key))
|
||||
shared = p.multiply(bytes.fromhex(priv_key)).format()[1:]
|
||||
(encrypted_content_b64, iv_b64) = content.split("?iv=")
|
||||
encrypted_content_b64, iv_b64 = content.split("?iv=")
|
||||
encrypted_content = base64.b64decode(encrypted_content_b64.encode("ascii"))
|
||||
iv = base64.b64decode(iv_b64.encode("ascii"))
|
||||
aes = AES.new(shared, AES.MODE_CBC, iv)
|
||||
|
||||
@@ -136,12 +136,10 @@ def migrate_db(file: str, schema: str, exclude_tables: list[str] | None = None):
|
||||
assert os.path.isfile(file), f"{file} does not exist!"
|
||||
|
||||
sqlite_cursor = get_sqlite_cursor(file)
|
||||
tables = sqlite_cursor.execute(
|
||||
"""
|
||||
tables = sqlite_cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name not like 'sqlite?_%' escape '?'
|
||||
"""
|
||||
).fetchall()
|
||||
""").fetchall()
|
||||
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
@@ -184,11 +182,9 @@ def build_table_columns(file: str, schema: str, table_name: str):
|
||||
sqlite_columns = sqlite_cursor.execute(
|
||||
f"PRAGMA table_info({table_name})"
|
||||
).fetchall()
|
||||
pg_cursor.execute(
|
||||
f"""
|
||||
pg_cursor.execute(f"""
|
||||
SELECT table_name, column_name, udt_name FROM information_schema.columns
|
||||
WHERE table_schema = '{schema}'AND table_name = '{table_name}';"""
|
||||
)
|
||||
WHERE table_schema = '{schema}'AND table_name = '{table_name}';""")
|
||||
pg_columns = pg_cursor.fetchall()
|
||||
|
||||
columns = []
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# 1. Always check the results of the procedure
|
||||
# 2. Always run "npx prettier -w lnbits/static/i18n/XX.js" to reformat the result
|
||||
"""_summary_
|
||||
The script syncs missing translation keys from en.js into another language file
|
||||
using GPT-4o.
|
||||
|
||||
What it does:
|
||||
1. Loads en.js and the target language file (e.g. de.js)
|
||||
2. Finds keys present in en.js but missing from the target
|
||||
3. For each missing key, calls GPT-4o to translate the English string
|
||||
4. Preserves %{variable} placeholders verbatim — skips the translation if they get
|
||||
mangled
|
||||
5. Writes the updated language file back, maintaining key order from en.js
|
||||
6. If no keys are missing, it instead checks that %{variable} placeholders match
|
||||
between English and the target
|
||||
|
||||
1. Always check the results of the procedure
|
||||
2. Always run "npx prettier -w lnbits/static/i18n/XX.js" to format the result
|
||||
|
||||
Usage:
|
||||
export OPENAI_API_KEY=sk-...
|
||||
python3 tools/i18n-ai-tool.py de
|
||||
npx prettier -w lnbits/static/i18n/de.js
|
||||
Returns:
|
||||
_type_: _description_
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||