Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa1fec87c7 | ||
|
|
74a1dd5ea6 | ||
|
|
ef47f1660d | ||
|
|
4341c329fd | ||
|
|
fdf3ab2688 | ||
|
|
99e4f33142 | ||
|
|
f4f43ad361 | ||
|
|
f14ea6c577 | ||
|
|
07428ecf94 | ||
|
|
867e3d06f6 | ||
|
|
4b4b6d0bcd | ||
|
|
07b1521dad | ||
|
|
7a2ddd9826 | ||
|
|
0eb4b477b7 | ||
|
|
06a0ba58ce | ||
|
|
5399b36027 | ||
|
|
385fb4f9bc | ||
|
|
8db76b8864 | ||
|
|
9e3ab0ef26 | ||
|
|
04c9b67997 | ||
|
|
116f982aab | ||
|
|
63cc89e2b6 | ||
|
|
183e6e5661 | ||
|
|
6b3fd80e46 | ||
|
|
5999a773be | ||
|
|
7e0fadad3b | ||
|
|
b2d6243697 | ||
|
|
6b5a77fb3b | ||
|
|
cc8fb68b02 | ||
|
|
658da6b28e | ||
|
|
7d734ecb74 | ||
|
|
9177dd195b | ||
|
|
15faab4f38 | ||
|
|
2cce687865 | ||
|
|
313bd3f647 | ||
|
|
13a93836d9 | ||
|
|
4f76d0483e | ||
|
|
719d86aa9c | ||
|
|
bbad4a91ae | ||
|
|
fcebb7e28c | ||
|
|
ce5aa4c8a7 | ||
|
|
75bae67446 | ||
|
|
8c184356ef | ||
|
|
efc0547271 | ||
|
|
7a393b11fd | ||
|
|
fae3eca3c7 | ||
|
|
3398070dd5 |
@@ -7,9 +7,13 @@
|
|||||||
# They are NOT managed by the Admin UI and are not stored in the database.
|
# They are NOT managed by the Admin UI and are not stored in the database.
|
||||||
|
|
||||||
# === First Install Token ===
|
# === First Install Token ===
|
||||||
# if set the user is required to enter this token on the /first_install page
|
# If set the user is required to enter this token on the /first_install page
|
||||||
# FIRST_INSTALL_TOKEN="myaccesstoken"
|
# FIRST_INSTALL_TOKEN="myaccesstoken"
|
||||||
|
|
||||||
|
# === Security ===
|
||||||
|
# When enabled (recommended), auth cookies require HTTPS and SSO will reject insecure HTTP.
|
||||||
|
AUTH_HTTPS_ONLY=true
|
||||||
|
|
||||||
# === Logging and Development ===
|
# === Logging and Development ===
|
||||||
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
@@ -24,6 +28,10 @@ LOG_ROTATION="100 MB"
|
|||||||
LOG_RETENTION="3 months"
|
LOG_RETENTION="3 months"
|
||||||
# for database cleanup commands
|
# for database cleanup commands
|
||||||
# CLEANUP_WALLETS_DAYS=90
|
# CLEANUP_WALLETS_DAYS=90
|
||||||
|
# Hard limit for total created users. Set to 0 to disable the limit.
|
||||||
|
# LNBITS_MAX_USERS=0
|
||||||
|
# Hard limit for total installed extensions. Set to 0 to disable the limit.
|
||||||
|
# LNBITS_MAX_EXTENSIONS=0
|
||||||
|
|
||||||
# === Admin Settings ===
|
# === Admin Settings ===
|
||||||
|
|
||||||
@@ -94,7 +102,7 @@ AUTH_SECRET_KEY=""
|
|||||||
######################################
|
######################################
|
||||||
|
|
||||||
AUTH_TOKEN_EXPIRE_MINUTES=525600
|
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"
|
AUTH_ALLOWED_METHODS="user-id-only, username-password"
|
||||||
# Set this flag if HTTP is used for OAuth
|
# Set this flag if HTTP is used for OAuth
|
||||||
# OAUTHLIB_INSECURE_TRANSPORT="1"
|
# OAUTHLIB_INSECURE_TRANSPORT="1"
|
||||||
@@ -271,6 +279,50 @@ KEYCLOAK_DISCOVERY_URL=""
|
|||||||
KEYCLOAK_CLIENT_CUSTOM_ORG=""
|
KEYCLOAK_CLIENT_CUSTOM_ORG=""
|
||||||
KEYCLOAK_CLIENT_CUSTOM_ICON=""
|
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
|
||||||
|
|
||||||
|
|
||||||
######################################
|
######################################
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ jobs:
|
|||||||
--onefile \
|
--onefile \
|
||||||
--name lnbits \
|
--name lnbits \
|
||||||
--hidden-import=embit \
|
--hidden-import=embit \
|
||||||
|
--hidden-import=bitstring.bitstore_bitarray \
|
||||||
--collect-all embit \
|
--collect-all embit \
|
||||||
|
--collect-all bitstring \
|
||||||
|
--collect-all bitarray \
|
||||||
--collect-all lnbits \
|
--collect-all lnbits \
|
||||||
--collect-all sqlalchemy \
|
--collect-all sqlalchemy \
|
||||||
--collect-all breez_sdk \
|
--collect-all breez_sdk \
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: bundle
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bundle:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
- uses: lnbits/lnbits/.github/actions/prepare@dev
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
node-version: "24.x"
|
||||||
|
npm: true
|
||||||
|
- run: make bundle
|
||||||
|
- name: Commit and push bundle changes
|
||||||
|
run: |
|
||||||
|
git config user.name "alan"
|
||||||
|
git config user.email "alan@lnbits.com"
|
||||||
|
git add lnbits/static
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "chore: make bundle [skip ci]"
|
||||||
|
git push
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
name: LNbits CI
|
name: LNbits CI
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
|
|
||||||
@@ -16,7 +11,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
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"]
|
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
with:
|
with:
|
||||||
@@ -30,7 +25,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
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"]
|
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
with:
|
with:
|
||||||
@@ -44,7 +39,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
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"]
|
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
with:
|
with:
|
||||||
@@ -58,7 +53,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.12"]
|
||||||
uses: ./.github/workflows/migration.yml
|
uses: ./.github/workflows/migration.yml
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@@ -74,7 +69,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/regtest.yml
|
uses: ./.github/workflows/regtest.yml
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10"]
|
python-version: ["3.12"]
|
||||||
backend-wallet-class:
|
backend-wallet-class:
|
||||||
- BoltzWallet
|
- BoltzWallet
|
||||||
- LndRestWallet
|
- LndRestWallet
|
||||||
@@ -94,7 +89,11 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10"]
|
python-version: ["3.12"]
|
||||||
uses: ./.github/workflows/jmeter.yml
|
uses: ./.github/workflows/jmeter.yml
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
bundle:
|
||||||
|
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi, regtest, jmeter ]
|
||||||
|
uses: ./.github/workflows/bundle.yml
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ jobs:
|
|||||||
- name: run LNbits
|
- name: run LNbits
|
||||||
env:
|
env:
|
||||||
LNBITS_ADMIN_UI: true
|
LNBITS_ADMIN_UI: true
|
||||||
|
AUTH_HTTPS_ONLY: false
|
||||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
||||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -6,53 +6,30 @@ jobs:
|
|||||||
|
|
||||||
black:
|
black:
|
||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
|
||||||
with:
|
with:
|
||||||
make: checkblack
|
make: checkblack
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
ruff:
|
ruff:
|
||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
|
||||||
with:
|
with:
|
||||||
make: checkruff
|
make: checkruff
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
mypy:
|
mypy:
|
||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
|
||||||
with:
|
with:
|
||||||
make: mypy
|
make: mypy
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
pyright:
|
pyright:
|
||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
|
||||||
with:
|
with:
|
||||||
make: pyright
|
make: pyright
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
npm: true
|
npm: true
|
||||||
|
|
||||||
|
|
||||||
prettier:
|
prettier:
|
||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
with:
|
with:
|
||||||
make: checkprettier
|
make: checkprettier
|
||||||
npm: true
|
npm: true
|
||||||
|
|
||||||
bundle:
|
|
||||||
uses: ./.github/workflows/make.yml
|
|
||||||
with:
|
|
||||||
make: checkbundle
|
|
||||||
npm: true
|
|
||||||
|
|
||||||
poetry:
|
poetry:
|
||||||
uses: ./.github/workflows/poetry.yml
|
uses: ./.github/workflows/poetry.yml
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ on:
|
|||||||
python-version:
|
python-version:
|
||||||
description: "python version"
|
description: "python version"
|
||||||
type: string
|
type: string
|
||||||
default: "3.10"
|
default: "3.12"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
make:
|
make:
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os-version: ["ubuntu-24.04"]
|
os-version: ["ubuntu-24.04"]
|
||||||
node-version: ["18.x"]
|
node-version: ["24.x"]
|
||||||
runs-on: ${{ matrix.os-version }}
|
runs-on: ${{ matrix.os-version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
python-version:
|
python-version:
|
||||||
default: "3.10"
|
default: "3.12"
|
||||||
type: string
|
type: string
|
||||||
os-version:
|
os-version:
|
||||||
default: "ubuntu-24.04"
|
default: "ubuntu-24.04"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
python-version:
|
python-version:
|
||||||
default: "3.10"
|
default: "3.12"
|
||||||
type: string
|
type: string
|
||||||
os-version:
|
os-version:
|
||||||
default: "ubuntu-24.04"
|
default: "ubuntu-24.04"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ __pycache__
|
|||||||
*$py.class
|
*$py.class
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
.vscode
|
.vscode
|
||||||
|
.codex
|
||||||
*-lock.json
|
*-lock.json
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ repos:
|
|||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 25.1.0
|
rev: 26.3.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.10
|
rev: v0.14.10
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [ --fix, --exit-non-zero-on-fix ]
|
args: [ --fix, --exit-non-zero-on-fix ]
|
||||||
|
|||||||
@@ -43,4 +43,4 @@ ENV LNBITS_HOST="0.0.0.0"
|
|||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
CMD ["sh", "-c", "uv run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
|
CMD ["sh", "-c", "uv --offline run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ test-regtest:
|
|||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
DEBUG=true \
|
DEBUG=true \
|
||||||
|
rm -rf ./tests/data \
|
||||||
uv run pytest tests/regtest
|
uv run pytest tests/regtest
|
||||||
|
|
||||||
test-migration:
|
test-migration:
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
<source media="(prefers-color-scheme: dark)" srcset="docs/logos/lnbits-full-inverse.svg">
|
||||||
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
|
<img src="docs/logos/lnbits-full.svg" alt="LNbits" style="width:300px">
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||||
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
<img alt="lnbits_head" src="docs/assets/header.jpg" />
|
||||||
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||||
|
|
||||||
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
||||||
@@ -46,7 +46,7 @@ Get yourself familiar and test on our demo server [demo.lnbits.com](https://demo
|
|||||||
|
|
||||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv. Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
|
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv. Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/w8jdGpF.png" style="width:800px">
|
<img alt="lnbits_wallet" src="docs/assets/wallet.jpg" />
|
||||||
|
|
||||||
## LNbits extension universe
|
## LNbits extension universe
|
||||||
|
|
||||||
@@ -54,25 +54,25 @@ Extend YOUR LNbits to meet YOUR needs.
|
|||||||
|
|
||||||
All non-core features are installed as extensions, reducing your code base and making your LNbits unique to you. Extend your LNbits install in any direction, and even create and share your own extensions.
|
All non-core features are installed as extensions, reducing your code base and making your LNbits unique to you. Extend your LNbits install in any direction, and even create and share your own extensions.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/aEBpwJF.png" style="width:800px">
|
<img alt="lnbits_extensions" src="docs/assets/extensions.jpg" />
|
||||||
|
|
||||||
## LNbits API
|
## LNbits API
|
||||||
|
|
||||||
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
|
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/V742sb9.png" style="width:800px">
|
<img alt="lnbits_api" src="docs/assets/api.jpg" />
|
||||||
|
|
||||||
## LNbits node manager
|
## LNbits node manager
|
||||||
|
|
||||||
LNbits comes packaged with a light node management UI, to make running your node that much easier.
|
LNbits comes packaged with a light node management UI, to make running your node that much easier.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/TYqIK60.png" style="width:800px">
|
<img alt="lnbits_api" src="docs/assets/lightning_node.jpg" />
|
||||||
|
|
||||||
## LNbits across all your devices
|
## LNbits merchant tools
|
||||||
|
|
||||||
As well as working great in a browser, LNbits has native IoS and Android apps as well as a chrome extension. So you can enjoy the same UI across ALL your devices.
|
The LNbits stack can process both bitcoin and fiat payments, making it a turnkey, all-in-one solution for merchants. With orders and inventory shared across extensions, and built-in notifications for Nostr, Telegram, and email, LNbits keeps everything in sync, freeing merchants to focus on their business.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/J96EbRf.png" style="width:800px">
|
<img alt="lnbits_merchants" src="docs/assets/merchants_small.webp" />
|
||||||
|
|
||||||
## Powered by LNbits
|
## Powered by LNbits
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 305 KiB |
@@ -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
|
||||||
@@ -468,7 +468,12 @@ def register_async_tasks() -> None:
|
|||||||
create_permanent_task(wait_for_audit_data)
|
create_permanent_task(wait_for_audit_data)
|
||||||
create_permanent_task(wait_notification_messages)
|
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(invoice_listener)
|
||||||
create_permanent_task(internal_invoice_listener)
|
create_permanent_task(internal_invoice_listener)
|
||||||
create_permanent_task(cache.invalidate_forever)
|
create_permanent_task(cache.invalidate_forever)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .extensions import (
|
|||||||
drop_extension_db,
|
drop_extension_db,
|
||||||
get_installed_extension,
|
get_installed_extension,
|
||||||
get_installed_extensions,
|
get_installed_extensions,
|
||||||
|
get_installed_extensions_count,
|
||||||
get_user_active_extensions_ids,
|
get_user_active_extensions_ids,
|
||||||
get_user_extension,
|
get_user_extension,
|
||||||
get_user_extensions,
|
get_user_extensions,
|
||||||
@@ -58,6 +59,7 @@ from .users import (
|
|||||||
get_account_by_username,
|
get_account_by_username,
|
||||||
get_account_by_username_or_email,
|
get_account_by_username_or_email,
|
||||||
get_accounts,
|
get_accounts,
|
||||||
|
get_accounts_count,
|
||||||
get_user,
|
get_user,
|
||||||
get_user_access_control_lists,
|
get_user_access_control_lists,
|
||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
@@ -117,11 +119,13 @@ __all__ = [
|
|||||||
"get_account_by_username",
|
"get_account_by_username",
|
||||||
"get_account_by_username_or_email",
|
"get_account_by_username_or_email",
|
||||||
"get_accounts",
|
"get_accounts",
|
||||||
|
"get_accounts_count",
|
||||||
"get_admin_settings",
|
"get_admin_settings",
|
||||||
"get_db_version",
|
"get_db_version",
|
||||||
"get_db_versions",
|
"get_db_versions",
|
||||||
"get_installed_extension",
|
"get_installed_extension",
|
||||||
"get_installed_extensions",
|
"get_installed_extensions",
|
||||||
|
"get_installed_extensions_count",
|
||||||
"get_latest_payments_by_extension",
|
"get_latest_payments_by_extension",
|
||||||
"get_payment",
|
"get_payment",
|
||||||
"get_payments",
|
"get_payments",
|
||||||
|
|||||||
@@ -90,6 +90,13 @@ async def get_installed_extensions(
|
|||||||
return all_extensions
|
return all_extensions
|
||||||
|
|
||||||
|
|
||||||
|
async def get_installed_extensions_count(conn: Connection | None = None) -> int:
|
||||||
|
row: dict | None = await (conn or db).fetchone(
|
||||||
|
"SELECT COUNT(*) as count FROM installed_extensions"
|
||||||
|
)
|
||||||
|
return int(row["count"]) if row else 0
|
||||||
|
|
||||||
|
|
||||||
async def get_user_extension(
|
async def get_user_extension(
|
||||||
user_id: str, extension: str, conn: Connection | None = None
|
user_id: str, extension: str, conn: Connection | None = None
|
||||||
) -> UserExtension | None:
|
) -> UserExtension | None:
|
||||||
|
|||||||
@@ -149,14 +149,12 @@ async def get_payments_paginated( # noqa: C901
|
|||||||
f"(status = '{PaymentState.SUCCESS}' OR status = '{PaymentState.PENDING}')"
|
f"(status = '{PaymentState.SUCCESS}' OR status = '{PaymentState.PENDING}')"
|
||||||
)
|
)
|
||||||
elif complete:
|
elif complete:
|
||||||
clause.append(
|
clause.append(f"""
|
||||||
f"""
|
|
||||||
(
|
(
|
||||||
status = '{PaymentState.SUCCESS}'
|
status = '{PaymentState.SUCCESS}'
|
||||||
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||||
)
|
)
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
elif pending:
|
elif pending:
|
||||||
clause.append(f"status = '{PaymentState.PENDING}'")
|
clause.append(f"status = '{PaymentState.PENDING}'")
|
||||||
elif failed:
|
elif failed:
|
||||||
@@ -346,14 +344,12 @@ async def get_payments_history(
|
|||||||
"wallet_id": wallet_id,
|
"wallet_id": wallet_id,
|
||||||
}
|
}
|
||||||
# count outgoing payments if they are still pending
|
# count outgoing payments if they are still pending
|
||||||
where = [
|
where = [f"""
|
||||||
f"""
|
|
||||||
wallet_id = :wallet_id AND (
|
wallet_id = :wallet_id AND (
|
||||||
status = '{PaymentState.SUCCESS}'
|
status = '{PaymentState.SUCCESS}'
|
||||||
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||||
)
|
)
|
||||||
"""
|
"""]
|
||||||
]
|
|
||||||
clause = filters.where(where)
|
clause = filters.where(where)
|
||||||
transactions: list[dict] = await db.fetchall(
|
transactions: list[dict] = await db.fetchall(
|
||||||
# This query is safe from SQL injection:
|
# This query is safe from SQL injection:
|
||||||
|
|||||||
@@ -105,7 +105,8 @@ async def get_settings_field(
|
|||||||
)
|
)
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
|
value = json.loads(row["value"]) if row["value"] else None
|
||||||
|
return SettingsField(id=row["id"], value=value, tag=row["tag"])
|
||||||
|
|
||||||
|
|
||||||
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
|
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ from typing import Any
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||||
from lnbits.core.crud.wallets import 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.db import db
|
||||||
from lnbits.core.models import UserAcls
|
from lnbits.core.models import UserAcls
|
||||||
from lnbits.db import Connection, Filters, Page
|
from lnbits.db import Connection, Filters, Page
|
||||||
@@ -32,6 +37,13 @@ async def create_account(
|
|||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
async def get_accounts_count(conn: Connection | None = None) -> int:
|
||||||
|
row: dict | None = await (conn or db).fetchone(
|
||||||
|
"SELECT COUNT(*) as count FROM accounts"
|
||||||
|
)
|
||||||
|
return int(row["count"]) if row else 0
|
||||||
|
|
||||||
|
|
||||||
async def update_account(account: Account, conn: Connection | None = None) -> Account:
|
async def update_account(account: Account, conn: Connection | None = None) -> Account:
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
await (conn or db).update("accounts", account)
|
await (conn or db).update("accounts", account)
|
||||||
@@ -52,17 +64,22 @@ async def get_accounts(
|
|||||||
) -> Page[AccountOverview]:
|
) -> Page[AccountOverview]:
|
||||||
where_clauses = []
|
where_clauses = []
|
||||||
values: dict[str, Any] = {}
|
values: dict[str, Any] = {}
|
||||||
|
filters = filters or Filters()
|
||||||
|
|
||||||
# Make wallet filter explicit
|
wallet_filter = filters.get_filter_by_field("wallet_id")
|
||||||
wallet_filter = (
|
|
||||||
next((f for f in filters.filters if f.field == "wallet_id"), None)
|
if wallet_filter and wallet_filter.values:
|
||||||
if filters
|
wallet_id_value = next(iter(wallet_filter.values.values()), None)
|
||||||
else None
|
wallet = (
|
||||||
)
|
await get_standalone_wallet(wallet_id_value, deleted=None, conn=conn)
|
||||||
if filters and wallet_filter and wallet_filter.values:
|
if wallet_id_value
|
||||||
where_clauses.append("wallets.id = :wallet_id")
|
else None
|
||||||
values = {**values, "wallet_id": next(iter(wallet_filter.values.values()))}
|
)
|
||||||
filters.filters = [f for f in filters.filters if f.field != "wallet_id"]
|
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(
|
return await (conn or db).fetch_page(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -10,31 +10,26 @@ from lnbits.db import Connection
|
|||||||
|
|
||||||
|
|
||||||
async def m000_create_migrations_table(db: Connection):
|
async def m000_create_migrations_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS dbversions (
|
CREATE TABLE IF NOT EXISTS dbversions (
|
||||||
db TEXT PRIMARY KEY,
|
db TEXT PRIMARY KEY,
|
||||||
version INT NOT NULL
|
version INT NOT NULL
|
||||||
)
|
)
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m001_initial(db: Connection):
|
async def m001_initial(db: Connection):
|
||||||
"""
|
"""
|
||||||
Initial LNbits tables.
|
Initial LNbits tables.
|
||||||
"""
|
"""
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS accounts (
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
email TEXT,
|
email TEXT,
|
||||||
pass TEXT
|
pass TEXT
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
await db.execute("""
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS extensions (
|
CREATE TABLE IF NOT EXISTS extensions (
|
||||||
"user" TEXT NOT NULL,
|
"user" TEXT NOT NULL,
|
||||||
extension TEXT NOT NULL,
|
extension TEXT NOT NULL,
|
||||||
@@ -42,10 +37,8 @@ async def m001_initial(db: Connection):
|
|||||||
|
|
||||||
UNIQUE ("user", extension)
|
UNIQUE ("user", extension)
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
await db.execute("""
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS wallets (
|
CREATE TABLE IF NOT EXISTS wallets (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
@@ -53,10 +46,8 @@ async def m001_initial(db: Connection):
|
|||||||
adminkey TEXT NOT NULL,
|
adminkey TEXT NOT NULL,
|
||||||
inkey TEXT
|
inkey TEXT
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
await db.execute(f"""
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE IF NOT EXISTS apipayments (
|
CREATE TABLE IF NOT EXISTS apipayments (
|
||||||
payhash TEXT NOT NULL,
|
payhash TEXT NOT NULL,
|
||||||
amount {db.big_int} 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},
|
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||||
UNIQUE (wallet, payhash)
|
UNIQUE (wallet, payhash)
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
||||||
SELECT wallet, SUM(amount) AS s -- incoming
|
SELECT wallet, SUM(amount) AS s -- incoming
|
||||||
@@ -85,8 +74,7 @@ async def m001_initial(db: Connection):
|
|||||||
GROUP BY wallet
|
GROUP BY wallet
|
||||||
)x
|
)x
|
||||||
GROUP BY wallet;
|
GROUP BY wallet;
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m002_add_fields_to_apipayments(db: Connection):
|
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("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
||||||
SELECT wallet, SUM(amount) AS s -- incoming
|
SELECT wallet, SUM(amount) AS s -- incoming
|
||||||
@@ -164,8 +151,7 @@ async def m004_ensure_fees_are_always_negative(db: Connection):
|
|||||||
GROUP BY wallet
|
GROUP BY wallet
|
||||||
)x
|
)x
|
||||||
GROUP BY wallet;
|
GROUP BY wallet;
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m005_balance_check_balance_notify(db: Connection):
|
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.
|
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 (
|
CREATE TABLE IF NOT EXISTS balance_check (
|
||||||
wallet TEXT NOT NULL REFERENCES wallets (id),
|
wallet TEXT NOT NULL REFERENCES wallets (id),
|
||||||
service TEXT NOT NULL,
|
service TEXT NOT NULL,
|
||||||
@@ -183,19 +168,16 @@ async def m005_balance_check_balance_notify(db: Connection):
|
|||||||
|
|
||||||
UNIQUE(wallet, service)
|
UNIQUE(wallet, service)
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS balance_notify (
|
CREATE TABLE IF NOT EXISTS balance_notify (
|
||||||
wallet TEXT NOT NULL REFERENCES wallets (id),
|
wallet TEXT NOT NULL REFERENCES wallets (id),
|
||||||
url TEXT NOT NULL,
|
url TEXT NOT NULL,
|
||||||
|
|
||||||
UNIQUE(wallet, url)
|
UNIQUE(wallet, url)
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m006_add_invoice_expiry_to_apipayments(db: Connection):
|
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):
|
async def m008_create_admin_settings_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
super_user TEXT,
|
super_user TEXT,
|
||||||
editable_settings TEXT NOT NULL DEFAULT '{}'
|
editable_settings TEXT NOT NULL DEFAULT '{}'
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m009_create_tinyurl_table(db: Connection):
|
async def m009_create_tinyurl_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE IF NOT EXISTS tiny_url (
|
CREATE TABLE IF NOT EXISTS tiny_url (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
url TEXT,
|
url TEXT,
|
||||||
@@ -282,13 +261,11 @@ async def m009_create_tinyurl_table(db: Connection):
|
|||||||
wallet TEXT,
|
wallet TEXT,
|
||||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m010_create_installed_extensions_table(db: Connection):
|
async def m010_create_installed_extensions_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS installed_extensions (
|
CREATE TABLE IF NOT EXISTS installed_extensions (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
version TEXT NOT NULL,
|
version TEXT NOT NULL,
|
||||||
@@ -299,8 +276,7 @@ async def m010_create_installed_extensions_table(db: Connection):
|
|||||||
active BOOLEAN DEFAULT false,
|
active BOOLEAN DEFAULT false,
|
||||||
meta TEXT NOT NULL DEFAULT '{}'
|
meta TEXT NOT NULL DEFAULT '{}'
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m011_optimize_balances_view(db: Connection):
|
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.
|
over the payments table instead of 2.
|
||||||
"""
|
"""
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT wallet, SUM(amount - abs(fee)) AS balance
|
SELECT wallet, SUM(amount - abs(fee)) AS balance
|
||||||
FROM apipayments
|
FROM apipayments
|
||||||
WHERE (pending = false AND amount > 0) OR amount < 0
|
WHERE (pending = false AND amount > 0) OR amount < 0
|
||||||
GROUP BY wallet
|
GROUP BY wallet
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m012_add_currency_to_wallet(db: Connection):
|
async def m012_add_currency_to_wallet(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
ALTER TABLE wallets ADD COLUMN currency TEXT
|
ALTER TABLE wallets ADD COLUMN currency TEXT
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m013_add_deleted_to_wallets(db: Connection):
|
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.
|
Sets deleted column to wallets.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = await db.execute(
|
result = await db.execute("""
|
||||||
"""
|
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM wallets
|
FROM wallets
|
||||||
WHERE user LIKE 'del:%'
|
WHERE user LIKE 'del:%'
|
||||||
AND adminkey LIKE 'del:%'
|
AND adminkey LIKE 'del:%'
|
||||||
AND inkey LIKE 'del:%'
|
AND inkey LIKE 'del:%'
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
rows = result.mappings().all()
|
rows = result.mappings().all()
|
||||||
|
|
||||||
for row in rows:
|
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):
|
async def m015_create_push_notification_subscriptions_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
|
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
|
||||||
endpoint TEXT NOT NULL,
|
endpoint TEXT NOT NULL,
|
||||||
"user" 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},
|
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||||
PRIMARY KEY (endpoint, "user")
|
PRIMARY KEY (endpoint, "user")
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m016_add_username_column_to_accounts(db: Connection):
|
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.
|
Make deleted wallets not show up in the balances view.
|
||||||
"""
|
"""
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT apipayments.wallet,
|
SELECT apipayments.wallet,
|
||||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
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)
|
AND ((apipayments.pending = false AND apipayments.amount > 0)
|
||||||
OR apipayments.amount < 0)
|
OR apipayments.amount < 0)
|
||||||
GROUP BY wallet
|
GROUP BY wallet
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m019_balances_view_based_on_wallets(db: Connection):
|
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.
|
Important for querying whole lnbits balances.
|
||||||
"""
|
"""
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT apipayments.wallet,
|
SELECT apipayments.wallet,
|
||||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
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)
|
AND ((apipayments.pending = false AND apipayments.amount > 0)
|
||||||
OR apipayments.amount < 0)
|
OR apipayments.amount < 0)
|
||||||
GROUP BY apipayments.wallet
|
GROUP BY apipayments.wallet
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m020_add_column_column_to_user_extensions(db: Connection):
|
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("UPDATE apipayments SET status = 'success' WHERE NOT pending")
|
||||||
|
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT apipayments.wallet,
|
SELECT apipayments.wallet,
|
||||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
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)
|
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
|
||||||
)
|
)
|
||||||
GROUP BY apipayments.wallet
|
GROUP BY apipayments.wallet
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m022_add_pubkey_to_accounts(db: Connection):
|
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):
|
async def m025_refresh_view(db: Connection):
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT apipayments.wallet_id,
|
SELECT apipayments.wallet_id,
|
||||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
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)
|
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
|
||||||
)
|
)
|
||||||
GROUP BY apipayments.wallet_id
|
GROUP BY apipayments.wallet_id
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m026_update_payment_table(db: Connection):
|
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):
|
async def m028_update_settings(db: Connection):
|
||||||
|
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS system_settings (
|
CREATE TABLE IF NOT EXISTS system_settings (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
value TEXT,
|
value TEXT,
|
||||||
@@ -667,8 +626,7 @@ async def m028_update_settings(db: Connection):
|
|||||||
|
|
||||||
UNIQUE (id, tag)
|
UNIQUE (id, tag)
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
async def _insert_key_value(id_: str, value: Any):
|
async def _insert_key_value(id_: str, value: Any):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@@ -691,8 +649,7 @@ async def m028_update_settings(db: Connection):
|
|||||||
|
|
||||||
|
|
||||||
async def m029_create_audit_table(db: Connection):
|
async def m029_create_audit_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE IF NOT EXISTS audit (
|
CREATE TABLE IF NOT EXISTS audit (
|
||||||
component TEXT,
|
component TEXT,
|
||||||
ip_address TEXT,
|
ip_address TEXT,
|
||||||
@@ -706,16 +663,13 @@ async def m029_create_audit_table(db: Connection):
|
|||||||
delete_at TIMESTAMP,
|
delete_at TIMESTAMP,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m030_add_user_api_tokens_column(db: Connection):
|
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
|
ALTER TABLE accounts ADD COLUMN access_control_list TEXT
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m031_add_color_and_icon_to_wallets(db: Connection):
|
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):
|
async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m035_add_wallet_type_column(db: Connection):
|
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'
|
ALTER TABLE wallets ADD COLUMN wallet_type TEXT DEFAULT 'lightning'
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m036_add_shared_wallet_column(db: Connection):
|
async def m036_add_shared_wallet_column(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m037_create_assets_table(db: Connection):
|
async def m037_create_assets_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE IF NOT EXISTS assets (
|
CREATE TABLE IF NOT EXISTS assets (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
@@ -776,16 +723,13 @@ async def m037_create_assets_table(db: Connection):
|
|||||||
data {db.blob} NOT NULL,
|
data {db.blob} NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m038_add_labels_for_payments(db: Connection):
|
async def m038_add_labels_for_payments(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
ALTER TABLE apipayments ADD COLUMN labels TEXT
|
ALTER TABLE apipayments ADD COLUMN labels TEXT
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m039_index_payments(db: Connection):
|
async def m039_index_payments(db: Connection):
|
||||||
@@ -804,11 +748,9 @@ async def m039_index_payments(db: Connection):
|
|||||||
]
|
]
|
||||||
for index in indexes:
|
for index in indexes:
|
||||||
logger.debug(f"Creating index idx_payments_{index}...")
|
logger.debug(f"Creating index idx_payments_{index}...")
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_payments_{index} ON apipayments ({index});
|
CREATE INDEX IF NOT EXISTS idx_payments_{index} ON apipayments ({index});
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m040_index_wallets(db: Connection):
|
async def m040_index_wallets(db: Connection):
|
||||||
@@ -825,11 +767,9 @@ async def m040_index_wallets(db: Connection):
|
|||||||
|
|
||||||
for index in indexes:
|
for index in indexes:
|
||||||
logger.debug(f"Creating index idx_wallets_{index}...")
|
logger.debug(f"Creating index idx_wallets_{index}...")
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_wallets_{index} ON wallets ("{index}");
|
CREATE INDEX IF NOT EXISTS idx_wallets_{index} ON wallets ("{index}");
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m042_index_accounts(db: Connection):
|
async def m042_index_accounts(db: Connection):
|
||||||
@@ -843,11 +783,9 @@ async def m042_index_accounts(db: Connection):
|
|||||||
|
|
||||||
for index in indexes:
|
for index in indexes:
|
||||||
logger.debug(f"Creating index idx_wallets_{index}...")
|
logger.debug(f"Creating index idx_wallets_{index}...")
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m043_add_ui_customization_to_accounts(db: Connection):
|
async def m043_add_ui_customization_to_accounts(db: Connection):
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ class ExplicitRelease(BaseModel):
|
|||||||
details_link: str | None
|
details_link: str | None
|
||||||
paid_features: str | None
|
paid_features: str | None
|
||||||
pay_link: str | None
|
pay_link: str | None
|
||||||
|
admin_only: bool = False
|
||||||
|
super_user_only: bool = False
|
||||||
|
|
||||||
def is_version_compatible(self):
|
def is_version_compatible(self):
|
||||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
@@ -52,6 +54,8 @@ class GitHubRelease(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
organisation: str
|
organisation: str
|
||||||
repository: str
|
repository: str
|
||||||
|
admin_only: bool = False
|
||||||
|
super_user_only: bool = False
|
||||||
|
|
||||||
|
|
||||||
class Manifest(BaseModel):
|
class Manifest(BaseModel):
|
||||||
@@ -83,6 +87,8 @@ class ExtensionConfig(BaseModel):
|
|||||||
warning: str | None = ""
|
warning: str | None = ""
|
||||||
min_lnbits_version: str | None
|
min_lnbits_version: str | None
|
||||||
max_lnbits_version: str | None
|
max_lnbits_version: str | None
|
||||||
|
admin_only: bool = False
|
||||||
|
super_user_only: bool = False
|
||||||
|
|
||||||
def is_version_compatible(self) -> bool:
|
def is_version_compatible(self) -> bool:
|
||||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
@@ -195,6 +201,8 @@ class ExtensionRelease(BaseModel):
|
|||||||
cost_sats: int | None = None
|
cost_sats: int | None = None
|
||||||
paid_sats: int | None = 0
|
paid_sats: int | None = 0
|
||||||
payment_hash: str | None = None
|
payment_hash: str | None = None
|
||||||
|
admin_only: bool = False
|
||||||
|
super_user_only: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def archive_url(self) -> str:
|
def archive_url(self) -> str:
|
||||||
@@ -263,6 +271,8 @@ class ExtensionRelease(BaseModel):
|
|||||||
paid_features=e.paid_features,
|
paid_features=e.paid_features,
|
||||||
repo=e.repo,
|
repo=e.repo,
|
||||||
icon=e.icon,
|
icon=e.icon,
|
||||||
|
admin_only=e.admin_only,
|
||||||
|
super_user_only=e.super_user_only,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -289,6 +299,8 @@ class ExtensionRelease(BaseModel):
|
|||||||
release.min_lnbits_version = config.min_lnbits_version
|
release.min_lnbits_version = config.min_lnbits_version
|
||||||
release.max_lnbits_version = config.max_lnbits_version
|
release.max_lnbits_version = config.max_lnbits_version
|
||||||
release.is_version_compatible = config.is_version_compatible()
|
release.is_version_compatible = config.is_version_compatible()
|
||||||
|
release.admin_only = config.admin_only
|
||||||
|
release.super_user_only = config.super_user_only
|
||||||
|
|
||||||
release.icon = icon_to_github_url(f"{org}/{repo}", config.tile)
|
release.icon = icon_to_github_url(f"{org}/{repo}", config.tile)
|
||||||
|
|
||||||
@@ -336,6 +348,8 @@ class ExtensionMeta(BaseModel):
|
|||||||
paid_features: str | None = None
|
paid_features: str | None = None
|
||||||
has_paid_release: bool = False
|
has_paid_release: bool = False
|
||||||
has_free_release: bool = False
|
has_free_release: bool = False
|
||||||
|
admin_only: bool = False
|
||||||
|
super_user_only: bool = False
|
||||||
|
|
||||||
|
|
||||||
class InstallableExtension(BaseModel):
|
class InstallableExtension(BaseModel):
|
||||||
@@ -399,6 +413,16 @@ class InstallableExtension(BaseModel):
|
|||||||
return False
|
return False
|
||||||
return self.meta.pay_to_enable.required is True
|
return self.meta.pay_to_enable.required is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_admin_only(self) -> bool:
|
||||||
|
return settings.is_admin_extension(self.id) or bool(
|
||||||
|
self.meta and self.meta.admin_only
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_super_user_only(self) -> bool:
|
||||||
|
return bool(self.meta and self.meta.super_user_only)
|
||||||
|
|
||||||
async def download_archive(self):
|
async def download_archive(self):
|
||||||
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
|
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
|
||||||
ext_zip_file = self.zip_path
|
ext_zip_file = self.zip_path
|
||||||
@@ -454,6 +478,16 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
self.name = config_json.get("name")
|
self.name = config_json.get("name")
|
||||||
self.short_description = config_json.get("short_description")
|
self.short_description = config_json.get("short_description")
|
||||||
|
if self.meta:
|
||||||
|
self.meta.admin_only = config_json.get("admin_only", False)
|
||||||
|
self.meta.super_user_only = config_json.get("super_user_only", False)
|
||||||
|
if self.meta.installed_release:
|
||||||
|
self.meta.installed_release.admin_only = config_json.get(
|
||||||
|
"admin_only", False
|
||||||
|
)
|
||||||
|
self.meta.installed_release.super_user_only = config_json.get(
|
||||||
|
"super_user_only", False
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.meta
|
self.meta
|
||||||
@@ -496,6 +530,10 @@ class InstallableExtension(BaseModel):
|
|||||||
return
|
return
|
||||||
if not release.is_version_compatible:
|
if not release.is_version_compatible:
|
||||||
return
|
return
|
||||||
|
if not self.meta:
|
||||||
|
self.meta = ExtensionMeta()
|
||||||
|
self.meta.admin_only = release.admin_only
|
||||||
|
self.meta.super_user_only = release.super_user_only
|
||||||
if not self.meta or not self.meta.latest_release:
|
if not self.meta or not self.meta.latest_release:
|
||||||
meta = self.meta or ExtensionMeta()
|
meta = self.meta or ExtensionMeta()
|
||||||
meta.latest_release = release
|
meta.latest_release = release
|
||||||
@@ -558,6 +596,13 @@ class InstallableExtension(BaseModel):
|
|||||||
github_release.organisation, github_release.repository
|
github_release.organisation, github_release.repository
|
||||||
)
|
)
|
||||||
source_repo = f"{github_release.organisation}/{github_release.repository}"
|
source_repo = f"{github_release.organisation}/{github_release.repository}"
|
||||||
|
admin_only = github_release.admin_only or config.admin_only
|
||||||
|
super_user_only = github_release.super_user_only or config.super_user_only
|
||||||
|
latest_extension_release = ExtensionRelease.from_github_release(
|
||||||
|
source_repo, latest_release
|
||||||
|
)
|
||||||
|
latest_extension_release.admin_only = admin_only
|
||||||
|
latest_extension_release.super_user_only = super_user_only
|
||||||
return InstallableExtension(
|
return InstallableExtension(
|
||||||
id=github_release.id,
|
id=github_release.id,
|
||||||
name=config.name,
|
name=config.name,
|
||||||
@@ -569,9 +614,9 @@ class InstallableExtension(BaseModel):
|
|||||||
config.tile,
|
config.tile,
|
||||||
),
|
),
|
||||||
meta=ExtensionMeta(
|
meta=ExtensionMeta(
|
||||||
latest_release=ExtensionRelease.from_github_release(
|
admin_only=admin_only,
|
||||||
source_repo, latest_release
|
super_user_only=super_user_only,
|
||||||
),
|
latest_release=latest_extension_release,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -580,7 +625,12 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension:
|
def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension:
|
||||||
meta = ExtensionMeta(archive=e.archive, dependencies=e.dependencies)
|
meta = ExtensionMeta(
|
||||||
|
archive=e.archive,
|
||||||
|
dependencies=e.dependencies,
|
||||||
|
admin_only=e.admin_only,
|
||||||
|
super_user_only=e.super_user_only,
|
||||||
|
)
|
||||||
return InstallableExtension(
|
return InstallableExtension(
|
||||||
id=e.id,
|
id=e.id,
|
||||||
name=e.name,
|
name=e.name,
|
||||||
@@ -610,6 +660,8 @@ class InstallableExtension(BaseModel):
|
|||||||
short_description=config_json.get("short_description"),
|
short_description=config_json.get("short_description"),
|
||||||
icon=config_json.get("tile"),
|
icon=config_json.get("tile"),
|
||||||
meta=ExtensionMeta(
|
meta=ExtensionMeta(
|
||||||
|
admin_only=config_json.get("admin_only", False),
|
||||||
|
super_user_only=config_json.get("super_user_only", False),
|
||||||
installed_release=ExtensionRelease(
|
installed_release=ExtensionRelease(
|
||||||
name=ext_id,
|
name=ext_id,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -617,7 +669,9 @@ class InstallableExtension(BaseModel):
|
|||||||
source_repo=f"{conf_path}",
|
source_repo=f"{conf_path}",
|
||||||
min_lnbits_version=config_json.get("min_lnbits_version"),
|
min_lnbits_version=config_json.get("min_lnbits_version"),
|
||||||
max_lnbits_version=config_json.get("max_lnbits_version"),
|
max_lnbits_version=config_json.get("max_lnbits_version"),
|
||||||
)
|
admin_only=config_json.get("admin_only", False),
|
||||||
|
super_user_only=config_json.get("super_user_only", False),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -719,6 +773,13 @@ class InstallableExtension(BaseModel):
|
|||||||
repo_releases = await ExtensionRelease.get_github_releases(
|
repo_releases = await ExtensionRelease.get_github_releases(
|
||||||
r.organisation, r.repository
|
r.organisation, r.repository
|
||||||
)
|
)
|
||||||
|
for repo_release in repo_releases:
|
||||||
|
repo_release.admin_only = (
|
||||||
|
repo_release.admin_only or r.admin_only
|
||||||
|
)
|
||||||
|
repo_release.super_user_only = (
|
||||||
|
repo_release.super_user_only or r.super_user_only
|
||||||
|
)
|
||||||
extension_releases += repo_releases
|
extension_releases += repo_releases
|
||||||
|
|
||||||
for e in manifest.extensions:
|
for e in manifest.extensions:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
from pydantic import BaseModel
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class TinyURL(BaseModel):
|
class TinyURL(BaseModel):
|
||||||
@@ -6,4 +8,4 @@ class TinyURL(BaseModel):
|
|||||||
url: str
|
url: str
|
||||||
endless: bool
|
endless: bool
|
||||||
wallet: str
|
wallet: str
|
||||||
time: float
|
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from lnbits.core.crud import (
|
|||||||
delete_installed_extension,
|
delete_installed_extension,
|
||||||
get_db_version,
|
get_db_version,
|
||||||
get_installed_extension,
|
get_installed_extension,
|
||||||
|
get_installed_extensions_count,
|
||||||
update_installed_extension_state,
|
update_installed_extension_state,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.extensions import (
|
from lnbits.core.crud.extensions import (
|
||||||
@@ -38,6 +39,8 @@ async def install_extension(
|
|||||||
if installed_ext and installed_ext.meta:
|
if installed_ext and installed_ext.meta:
|
||||||
ext_info.meta.payments = installed_ext.meta.payments
|
ext_info.meta.payments = installed_ext.meta.payments
|
||||||
|
|
||||||
|
await check_extensions_limit(installed_ext)
|
||||||
|
|
||||||
if not skip_download:
|
if not skip_download:
|
||||||
await ext_info.download_archive()
|
await ext_info.download_archive()
|
||||||
|
|
||||||
@@ -63,6 +66,15 @@ async def install_extension(
|
|||||||
return extension
|
return extension
|
||||||
|
|
||||||
|
|
||||||
|
async def check_extensions_limit(installed_ext: InstallableExtension | None = None):
|
||||||
|
if settings.lnbits_max_extensions == 0 or installed_ext:
|
||||||
|
return
|
||||||
|
|
||||||
|
extensions_count = await get_installed_extensions_count()
|
||||||
|
if extensions_count >= settings.lnbits_max_extensions:
|
||||||
|
raise ValueError("Max amount of extensions have been installed")
|
||||||
|
|
||||||
|
|
||||||
async def uninstall_extension(ext_id: str):
|
async def uninstall_extension(ext_id: str):
|
||||||
await stop_extension_background_work(ext_id)
|
await stop_extension_background_work(ext_id)
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ from lnbits.core.crud.payments import get_daily_stats
|
|||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
||||||
from lnbits.core.models.payments import CreateInvoice
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
|
from lnbits.core.services.fiat_providers import handle_fiat_payment_confirmation
|
||||||
from lnbits.db import Connection, Filters
|
from lnbits.db import Connection, Filters
|
||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
||||||
from lnbits.fiat import get_fiat_provider
|
from lnbits.fiat import get_fiat_provider
|
||||||
from lnbits.helpers import check_callback_url
|
from lnbits.helpers import check_callback_url
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.tasks import create_task, internal_invoice_queue_put
|
|
||||||
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash, verify_preimage
|
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash, verify_preimage
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
||||||
from lnbits.wallets import fake_wallet, get_funding_source
|
from lnbits.wallets import fake_wallet, get_funding_source
|
||||||
@@ -509,6 +509,8 @@ async def update_wallet_balance(
|
|||||||
)
|
)
|
||||||
payment.status = PaymentState.SUCCESS
|
payment.status = PaymentState.SUCCESS
|
||||||
await update_payment(payment, conn=conn)
|
await update_payment(payment, conn=conn)
|
||||||
|
from lnbits.tasks import internal_invoice_queue_put
|
||||||
|
|
||||||
await internal_invoice_queue_put(payment.checking_id)
|
await internal_invoice_queue_put(payment.checking_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -819,6 +821,8 @@ async def _pay_external_invoice(
|
|||||||
|
|
||||||
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
|
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
|
||||||
|
|
||||||
|
from lnbits.tasks import create_task
|
||||||
|
|
||||||
task = create_task(
|
task = create_task(
|
||||||
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
|
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
|
||||||
)
|
)
|
||||||
@@ -1068,3 +1072,30 @@ async def _send_payment_notification_in_background(
|
|||||||
if not wallet:
|
if not wallet:
|
||||||
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
send_payment_notification_in_background(wallet, payment)
|
send_payment_notification_in_background(wallet, payment)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_invoice_callback(checking_id: str) -> Payment | None:
|
||||||
|
"""
|
||||||
|
Takes a checking_id of an incoming payment, from either paid_invoices_stream()
|
||||||
|
or internal_invoice_queue. Checks its status, updates and returns it.
|
||||||
|
returns None if no payment was found or it not and incoming payment.
|
||||||
|
"""
|
||||||
|
payment = await get_standalone_payment(checking_id, incoming=True)
|
||||||
|
if not payment:
|
||||||
|
logger.warning(f"No payment found for '{checking_id}'.")
|
||||||
|
return None
|
||||||
|
if not payment.is_in:
|
||||||
|
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
status = await check_payment_status(
|
||||||
|
payment, skip_internal_payment_notifications=True
|
||||||
|
)
|
||||||
|
payment.fee = status.fee_msat or payment.fee
|
||||||
|
# only overwrite preimage if status.preimage provides it
|
||||||
|
payment.preimage = status.preimage or payment.preimage
|
||||||
|
payment.status = PaymentState.SUCCESS
|
||||||
|
await update_payment(payment)
|
||||||
|
if payment.fiat_provider:
|
||||||
|
await handle_fiat_payment_confirmation(payment)
|
||||||
|
return payment
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from ..crud import (
|
|||||||
get_account_by_email,
|
get_account_by_email,
|
||||||
get_account_by_pubkey,
|
get_account_by_pubkey,
|
||||||
get_account_by_username,
|
get_account_by_username,
|
||||||
|
get_accounts_count,
|
||||||
get_super_settings,
|
get_super_settings,
|
||||||
get_user_extensions,
|
get_user_extensions,
|
||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
@@ -55,6 +56,8 @@ async def create_user_account_no_ckeck(
|
|||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
|
await check_users_limit(conn)
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
account.validate_fields()
|
account.validate_fields()
|
||||||
if account.username and await get_account_by_username(
|
if account.username and await get_account_by_username(
|
||||||
@@ -95,6 +98,15 @@ async def create_user_account_no_ckeck(
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def check_users_limit(conn: Connection | None = None):
|
||||||
|
if settings.lnbits_max_users == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
users_count = await get_accounts_count(conn=conn)
|
||||||
|
if users_count >= settings.lnbits_max_users:
|
||||||
|
raise ValueError("Max amount of users have been created")
|
||||||
|
|
||||||
|
|
||||||
async def update_user_account(account: Account) -> Account:
|
async def update_user_account(account: Account) -> Account:
|
||||||
account.validate_fields()
|
account.validate_fields()
|
||||||
|
|
||||||
@@ -173,6 +185,12 @@ async def check_admin_settings():
|
|||||||
if account and account.extra and account.extra.provider == "env":
|
if account and account.extra and account.extra.provider == "env":
|
||||||
settings.first_install = True
|
settings.first_install = True
|
||||||
|
|
||||||
|
if settings.has_first_install_token_changed():
|
||||||
|
logger.warning("First install token is changed. Resetting admin settings.")
|
||||||
|
new_settings = await init_admin_settings()
|
||||||
|
settings.super_user = new_settings.super_user
|
||||||
|
settings.first_install = True
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
|
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
|
||||||
"to get the superuser."
|
"to get the superuser."
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
|
||||||
%} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}{%
|
|
||||||
endblock %}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
|
|
||||||
context %} {% block scripts %} {{ window_vars() }} {% endblock %} {% block page
|
|
||||||
%} {% endblock %}
|
|
||||||
@@ -12,6 +12,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
|
|||||||
from fastapi_sso.sso.base import OpenID, SSOBase
|
from fastapi_sso.sso.base import OpenID, SSOBase
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.crud.settings import set_settings_field
|
||||||
from lnbits.core.crud.users import (
|
from lnbits.core.crud.users import (
|
||||||
get_user_access_control_lists,
|
get_user_access_control_lists,
|
||||||
update_user_access_control_list,
|
update_user_access_control_list,
|
||||||
@@ -154,9 +155,20 @@ async def impersonate_user(
|
|||||||
|
|
||||||
max_age = settings.auth_token_expire_minutes * 60
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
response.set_cookie(
|
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
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -177,7 +189,12 @@ async def stop_impersonate_user(
|
|||||||
)
|
)
|
||||||
max_age = settings.auth_token_expire_minutes * 60
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
response.set_cookie(
|
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("admin_access_token")
|
||||||
response.delete_cookie("is_access_token_expired")
|
response.delete_cookie("is_access_token_expired")
|
||||||
@@ -532,6 +549,13 @@ async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
|||||||
account.hash_password(data.password)
|
account.hash_password(data.password)
|
||||||
await update_account(account)
|
await update_account(account)
|
||||||
settings.first_install = False
|
settings.first_install = False
|
||||||
|
|
||||||
|
# only confrm it after the super user has been successfully updated
|
||||||
|
if settings.first_install_token:
|
||||||
|
settings.first_install_token_confirmed = data.first_install_token
|
||||||
|
await set_settings_field(
|
||||||
|
"first_install_token_confirmed", data.first_install_token
|
||||||
|
)
|
||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@@ -560,7 +584,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
|||||||
id=uuid4().hex, email=email, extra=UserExtra(email_verified=True)
|
id=uuid4().hex, email=email, extra=UserExtra(email_verified=True)
|
||||||
)
|
)
|
||||||
await create_user_account(account)
|
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(
|
def _auth_success_response(
|
||||||
@@ -575,9 +599,20 @@ def _auth_success_response(
|
|||||||
max_age = settings.auth_token_expire_minutes * 60
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
"cookie_access_token", access_token, httponly=True, 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")
|
response.delete_cookie("is_access_token_expired")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -594,15 +629,28 @@ def _auth_api_token_response(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
def _auth_redirect_response(path: str, user_id: str, email: str) -> RedirectResponse:
|
||||||
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
|
payload = AccessTokenPayload(
|
||||||
|
usr=user_id, sub="", email=email, auth_time=int(time())
|
||||||
|
)
|
||||||
access_token = create_access_token(data=payload.dict())
|
access_token = create_access_token(data=payload.dict())
|
||||||
max_age = settings.auth_token_expire_minutes * 60
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
response = RedirectResponse(path)
|
response = RedirectResponse(path)
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
"cookie_access_token", access_token, httponly=True, 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")
|
response.delete_cookie("is_access_token_expired")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -622,7 +670,10 @@ def _new_sso(provider: str) -> SSOBase | None:
|
|||||||
|
|
||||||
sso_provider_class = _find_auth_provider_class(provider)
|
sso_provider_class = _find_auth_provider_class(provider)
|
||||||
sso_provider = sso_provider_class(
|
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 (
|
if (
|
||||||
discovery_url
|
discovery_url
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ async def api_install_extension(data: CreateExtension):
|
|||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Incompatible extension version.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Incompatible extension version.")
|
||||||
|
|
||||||
release.payment_hash = data.payment_hash
|
release.payment_hash = data.payment_hash
|
||||||
ext_meta = ExtensionMeta(installed_release=release)
|
ext_meta = ExtensionMeta(
|
||||||
|
installed_release=release,
|
||||||
|
admin_only=release.admin_only,
|
||||||
|
super_user_only=release.super_user_only,
|
||||||
|
)
|
||||||
ext_info = InstallableExtension(
|
ext_info = InstallableExtension(
|
||||||
id=data.ext_id,
|
id=data.ext_id,
|
||||||
name=data.ext_id,
|
name=data.ext_id,
|
||||||
@@ -98,12 +102,16 @@ async def api_install_extension(data: CreateExtension):
|
|||||||
ext_info.clean_extension_files()
|
ext_info.clean_extension_files()
|
||||||
detail = (
|
detail = (
|
||||||
str(exc)
|
str(exc)
|
||||||
if isinstance(exc, AssertionError)
|
if isinstance(exc, (AssertionError, ValueError))
|
||||||
else f"Failed to install extension '{ext_info.id}'."
|
else f"Failed to install extension '{ext_info.id}'."
|
||||||
f"({ext_info.installed_version})."
|
f"({ext_info.installed_version})."
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=(
|
||||||
|
HTTPStatus.BAD_REQUEST
|
||||||
|
if isinstance(exc, (AssertionError, ValueError))
|
||||||
|
else HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
),
|
||||||
detail=detail,
|
detail=detail,
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
@@ -172,6 +180,19 @@ async def api_update_pay_to_enable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_enable_extension_access(
|
||||||
|
ext_id: str, ext: InstallableExtension, account_id: AccountId
|
||||||
|
) -> None:
|
||||||
|
if ext.is_super_user_only and not settings.is_super_user(account_id.id):
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.FORBIDDEN, f"User not authorized for extension '{ext_id}'."
|
||||||
|
)
|
||||||
|
if ext.is_admin_only and not settings.is_admin_user(account_id.id):
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.FORBIDDEN, f"User not authorized for extension '{ext_id}'."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@extension_router.put("/{ext_id}/enable")
|
@extension_router.put("/{ext_id}/enable")
|
||||||
async def api_enable_extension(
|
async def api_enable_extension(
|
||||||
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||||
@@ -187,6 +208,7 @@ async def api_enable_extension(
|
|||||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||||
if not ext.active:
|
if not ext.active:
|
||||||
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||||
|
_check_enable_extension_access(ext_id, ext, account_id)
|
||||||
|
|
||||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||||
if not user_ext:
|
if not user_ext:
|
||||||
@@ -460,6 +482,8 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
|
|||||||
"min_lnbits_version": config.min_lnbits_version,
|
"min_lnbits_version": config.min_lnbits_version,
|
||||||
"is_version_compatible": config.is_version_compatible(),
|
"is_version_compatible": config.is_version_compatible(),
|
||||||
"warning": config.warning,
|
"warning": config.warning,
|
||||||
|
"admin_only": config.admin_only,
|
||||||
|
"super_user_only": config.super_user_only,
|
||||||
}
|
}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -569,7 +593,8 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
(True for version in db_versions if version.db == ext.id), False
|
(True for version in db_versions if version.db == ext.id), False
|
||||||
),
|
),
|
||||||
"isAvailable": ext.id in all_ext_ids,
|
"isAvailable": ext.id in all_ext_ids,
|
||||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
"isAdminOnly": ext.is_admin_only,
|
||||||
|
"isSuperUserOnly": ext.is_super_user_only,
|
||||||
"isActive": ext.id not in inactive_extensions,
|
"isActive": ext.id not in inactive_extensions,
|
||||||
"latestRelease": (
|
"latestRelease": (
|
||||||
dict(ext.meta.latest_release)
|
dict(ext.meta.latest_release)
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ async def index(
|
|||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"index.html",
|
"base.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
},
|
},
|
||||||
@@ -211,7 +211,7 @@ async def index(
|
|||||||
@generic_router.get("/node/public")
|
@generic_router.get("/node/public")
|
||||||
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
||||||
async def index_public(request: Request) -> HTMLResponse:
|
async def index_public(request: Request) -> HTMLResponse:
|
||||||
return template_renderer().TemplateResponse(request, "index.html", {"public": True})
|
return template_renderer().TemplateResponse(request, "base.html", {"public": True})
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/uuidv4/{hex_value}")
|
@generic_router.get("/uuidv4/{hex_value}")
|
||||||
|
|||||||
@@ -613,6 +613,12 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
for page_filter in self.filters:
|
for page_filter in self.filters:
|
||||||
page_filter.table_name = table_name
|
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):
|
class DbJsonEncoder(json.JSONEncoder):
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from lnbits.core.crud import (
|
|||||||
get_account,
|
get_account,
|
||||||
get_account_by_email,
|
get_account_by_email,
|
||||||
get_account_by_username,
|
get_account_by_username,
|
||||||
|
get_installed_extension,
|
||||||
get_user_active_extensions_ids,
|
get_user_active_extensions_ids,
|
||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
@@ -424,7 +425,19 @@ async def check_user_extension_access(
|
|||||||
Check if the user has access to a particular extension.
|
Check if the user has access to a particular extension.
|
||||||
Raises HTTP Forbidden if the user is not allowed.
|
Raises HTTP Forbidden if the user is not allowed.
|
||||||
"""
|
"""
|
||||||
if settings.is_admin_extension(ext_id) and not settings.is_admin_user(user_id):
|
ext = await get_installed_extension(ext_id, conn=conn)
|
||||||
|
is_admin_only = settings.is_admin_extension(ext_id) or bool(
|
||||||
|
ext and ext.meta and ext.meta.admin_only
|
||||||
|
)
|
||||||
|
is_super_user_only = bool(ext and ext.meta and ext.meta.super_user_only)
|
||||||
|
|
||||||
|
if is_super_user_only and not settings.is_super_user(user_id):
|
||||||
|
return SimpleStatus(
|
||||||
|
success=False,
|
||||||
|
message=f"User not authorized for extension '{ext_id}'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_admin_only and not settings.is_admin_user(user_id):
|
||||||
return SimpleStatus(
|
return SimpleStatus(
|
||||||
success=False, message=f"User not authorized for extension '{ext_id}'."
|
success=False, message=f"User not authorized for extension '{ext_id}'."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ def static_url_for(static: str, path: str) -> str:
|
|||||||
def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
|
def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
|
||||||
folders = [
|
folders = [
|
||||||
"lnbits/templates",
|
"lnbits/templates",
|
||||||
"lnbits/core/templates",
|
|
||||||
settings.extension_builder_working_dir_path.as_posix(),
|
settings.extension_builder_working_dir_path.as_posix(),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -324,7 +324,8 @@ class AssetSettings(LNbitsSettings):
|
|||||||
"heif",
|
"heif",
|
||||||
"heics",
|
"heics",
|
||||||
"text/plain",
|
"text/plain",
|
||||||
"text/json" "text/xml",
|
"text/json",
|
||||||
|
"text/xml",
|
||||||
"application/json",
|
"application/json",
|
||||||
"application/pdf",
|
"application/pdf",
|
||||||
]
|
]
|
||||||
@@ -447,6 +448,7 @@ class SecuritySettings(LNbitsSettings):
|
|||||||
|
|
||||||
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
|
first_install_token_confirmed: str | None = Field(default=None)
|
||||||
|
|
||||||
def is_wallet_max_balance_exceeded(self, amount):
|
def is_wallet_max_balance_exceeded(self, amount):
|
||||||
return (
|
return (
|
||||||
@@ -587,6 +589,8 @@ class ZBDFundingSource(LNbitsSettings):
|
|||||||
class PhoenixdFundingSource(LNbitsSettings):
|
class PhoenixdFundingSource(LNbitsSettings):
|
||||||
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
|
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
|
||||||
phoenixd_api_password: str | None = Field(default=None)
|
phoenixd_api_password: str | None = Field(default=None)
|
||||||
|
phoenixd_data_dir: str | None = Field(default=None)
|
||||||
|
phoenixd_mnemonic: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class AlbyFundingSource(LNbitsSettings):
|
class AlbyFundingSource(LNbitsSettings):
|
||||||
@@ -732,6 +736,7 @@ class FundingSourcesSettings(
|
|||||||
# How long to wait for the payment to be confirmed before returning a pending status
|
# How long to wait for the payment to be confirmed before returning a pending status
|
||||||
# It will not fail the payment, it will make it return pending after the timeout
|
# It will not fail the payment, it will make it return pending after the timeout
|
||||||
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
|
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)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
|
|
||||||
|
|
||||||
@@ -796,6 +801,7 @@ class AuthMethods(Enum):
|
|||||||
google_auth = "google-auth"
|
google_auth = "google-auth"
|
||||||
github_auth = "github-auth"
|
github_auth = "github-auth"
|
||||||
keycloak_auth = "keycloak-auth"
|
keycloak_auth = "keycloak-auth"
|
||||||
|
oidc_auth = "oidc-auth"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls):
|
def all(cls):
|
||||||
@@ -806,6 +812,7 @@ class AuthMethods(Enum):
|
|||||||
AuthMethods.google_auth.value,
|
AuthMethods.google_auth.value,
|
||||||
AuthMethods.github_auth.value,
|
AuthMethods.github_auth.value,
|
||||||
AuthMethods.keycloak_auth.value,
|
AuthMethods.keycloak_auth.value,
|
||||||
|
AuthMethods.oidc_auth.value,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -851,6 +858,14 @@ class KeycloakAuthSettings(LNbitsSettings):
|
|||||||
keycloak_client_custom_icon: str | None = Field(default=None)
|
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):
|
class AuditSettings(LNbitsSettings):
|
||||||
lnbits_audit_enabled: bool = Field(default=True)
|
lnbits_audit_enabled: bool = Field(default=True)
|
||||||
|
|
||||||
@@ -958,6 +973,7 @@ class EditableSettings(
|
|||||||
GoogleAuthSettings,
|
GoogleAuthSettings,
|
||||||
GitHubAuthSettings,
|
GitHubAuthSettings,
|
||||||
KeycloakAuthSettings,
|
KeycloakAuthSettings,
|
||||||
|
OidcAuthSettings,
|
||||||
):
|
):
|
||||||
@validator(
|
@validator(
|
||||||
"lnbits_admin_users",
|
"lnbits_admin_users",
|
||||||
@@ -993,6 +1009,8 @@ class EnvSettings(LNbitsSettings):
|
|||||||
debug: bool = Field(default=False)
|
debug: bool = Field(default=False)
|
||||||
debug_database: bool = Field(default=False)
|
debug_database: bool = Field(default=False)
|
||||||
bundle_assets: bool = Field(default=True)
|
bundle_assets: bool = Field(default=True)
|
||||||
|
# When enabled, auth cookies require HTTPS and SSO will reject insecure HTTP.
|
||||||
|
auth_https_only: bool = Field(default=True)
|
||||||
host: str = Field(default="127.0.0.1")
|
host: str = Field(default="127.0.0.1")
|
||||||
port: int = Field(default=5000, gt=0)
|
port: int = Field(default=5000, gt=0)
|
||||||
forwarded_allow_ips: str = Field(default="*")
|
forwarded_allow_ips: str = Field(default="*")
|
||||||
@@ -1010,11 +1028,20 @@ class EnvSettings(LNbitsSettings):
|
|||||||
|
|
||||||
cleanup_wallets_days: int = Field(default=90, ge=0)
|
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
|
lnbits_max_users: int = Field(default=0, ge=0)
|
||||||
|
lnbits_max_extensions: int = Field(default=0, ge=0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_default_extension_path(self) -> bool:
|
def has_default_extension_path(self) -> bool:
|
||||||
return self.lnbits_extensions_path == "lnbits"
|
return self.lnbits_extensions_path == "lnbits"
|
||||||
|
|
||||||
|
def has_first_install_token_changed(self) -> bool:
|
||||||
|
if not self.first_install_token:
|
||||||
|
return False
|
||||||
|
if not settings.first_install_token_confirmed:
|
||||||
|
return False
|
||||||
|
return self.first_install_token != settings.first_install_token_confirmed
|
||||||
|
|
||||||
def check_auth_secret_key(self):
|
def check_auth_secret_key(self):
|
||||||
if self.auth_secret_key:
|
if self.auth_secret_key:
|
||||||
return
|
return
|
||||||
@@ -1174,6 +1201,8 @@ class PublicSettings(BaseModel):
|
|||||||
auth_methods: list[str] = Field(alias="authMethods")
|
auth_methods: list[str] = Field(alias="authMethods")
|
||||||
keycloak_org: str | None = Field(alias="keycloakOrg")
|
keycloak_org: str | None = Field(alias="keycloakOrg")
|
||||||
keycloak_icon: str | None = Field(alias="keycloakIcon")
|
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_holdinvoice: bool = Field(alias="hasHoldinvoice")
|
||||||
has_nodemanager: bool = Field(alias="hasNodemanager")
|
has_nodemanager: bool = Field(alias="hasNodemanager")
|
||||||
show_nodemanager: bool = Field(alias="showNodemanager")
|
show_nodemanager: bool = Field(alias="showNodemanager")
|
||||||
@@ -1238,6 +1267,8 @@ class PublicSettings(BaseModel):
|
|||||||
authMethods=settings.auth_allowed_methods,
|
authMethods=settings.auth_allowed_methods,
|
||||||
keycloakOrg=settings.keycloak_client_custom_org,
|
keycloakOrg=settings.keycloak_client_custom_org,
|
||||||
keycloakIcon=settings.keycloak_client_custom_icon,
|
keycloakIcon=settings.keycloak_client_custom_icon,
|
||||||
|
oidcOrg=settings.oidc_client_custom_org,
|
||||||
|
oidcIcon=settings.oidc_client_custom_icon,
|
||||||
hasHoldinvoice=settings.has_holdinvoice,
|
hasHoldinvoice=settings.has_holdinvoice,
|
||||||
hasNodemanager=settings.has_nodemanager,
|
hasNodemanager=settings.has_nodemanager,
|
||||||
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
|
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ window.localisation.br = {
|
|||||||
only_admins_can_create_extensions:
|
only_admins_can_create_extensions:
|
||||||
'Apenas contas de administrador podem criar extensões',
|
'Apenas contas de administrador podem criar extensões',
|
||||||
admin_only: 'Apenas para Administração',
|
admin_only: 'Apenas para Administração',
|
||||||
|
super_user_only: 'Apenas para superusuário',
|
||||||
make_user_admin: 'Tornar usuário administrador',
|
make_user_admin: 'Tornar usuário administrador',
|
||||||
revoke_admin: 'Revogar Admin',
|
revoke_admin: 'Revogar Admin',
|
||||||
new_version: 'Nova Versão',
|
new_version: 'Nova Versão',
|
||||||
@@ -640,6 +641,16 @@ window.localisation.br = {
|
|||||||
auth_keycloak_ci_hint:
|
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',
|
'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_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_org_label: 'Keycloak Custom Organization',
|
||||||
auth_keycloak_custom_icon_label: 'Ícone Personalizado do Keycloak (URL)',
|
auth_keycloak_custom_icon_label: 'Ícone Personalizado do Keycloak (URL)',
|
||||||
currency_settings: 'Configurações de Moeda',
|
currency_settings: 'Configurações de Moeda',
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ window.localisation.cn = {
|
|||||||
all: '全部',
|
all: '全部',
|
||||||
only_admins_can_install: '(只有管理员账户可以安装扩展)',
|
only_admins_can_install: '(只有管理员账户可以安装扩展)',
|
||||||
admin_only: '仅限管理员',
|
admin_only: '仅限管理员',
|
||||||
|
super_user_only: '仅限超级用户',
|
||||||
new_version: '新版本',
|
new_version: '新版本',
|
||||||
extension_depends_on: '依赖于:',
|
extension_depends_on: '依赖于:',
|
||||||
extension_rating_soon: '即将推出评分',
|
extension_rating_soon: '即将推出评分',
|
||||||
@@ -353,6 +354,15 @@ window.localisation.cn = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'确保授权回调URL设置为https://{domain}/api/v1/auth/keycloak/token',
|
'确保授权回调URL设置为https://{domain}/api/v1/auth/keycloak/token',
|
||||||
auth_keycloak_cs_label: 'Keycloak客户端密钥',
|
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: '货币设置',
|
currency_settings: '货币设置',
|
||||||
allowed_currencies: '允许的货币',
|
allowed_currencies: '允许的货币',
|
||||||
allowed_currencies_hint: '限制可用法定货币的数量',
|
allowed_currencies_hint: '限制可用法定货币的数量',
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ window.localisation.cs = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'(Pouze administrátorské účty mohou instalovat rozšíření)',
|
'(Pouze administrátorské účty mohou instalovat rozšíření)',
|
||||||
admin_only: 'Pouze pro adminy',
|
admin_only: 'Pouze pro adminy',
|
||||||
|
super_user_only: 'Pouze pro superuživatele',
|
||||||
new_version: 'Nová verze',
|
new_version: 'Nová verze',
|
||||||
extension_depends_on: 'Závisí na:',
|
extension_depends_on: 'Závisí na:',
|
||||||
extension_rating_soon: 'Hodnocení brzy dostupné',
|
extension_rating_soon: 'Hodnocení brzy dostupné',
|
||||||
@@ -367,6 +368,16 @@ window.localisation.cs = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'Ujistěte se, že je autorizace callback URL nastavena na https://{domain}/api/v1/auth/keycloak/token',
|
'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_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',
|
currency_settings: 'Nastavení měny',
|
||||||
allowed_currencies: 'Povolené měny',
|
allowed_currencies: 'Povolené měny',
|
||||||
allowed_currencies_hint: 'Omezte počet dostupných fiat měn',
|
allowed_currencies_hint: 'Omezte počet dostupných fiat měn',
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ window.localisation.de = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'(Nur Administratorkonten können Erweiterungen installieren)',
|
'(Nur Administratorkonten können Erweiterungen installieren)',
|
||||||
admin_only: 'Nur für Admins',
|
admin_only: 'Nur für Admins',
|
||||||
|
super_user_only: 'Nur für Superuser',
|
||||||
new_version: 'Neue Version',
|
new_version: 'Neue Version',
|
||||||
extension_depends_on: 'Hängt ab von:',
|
extension_depends_on: 'Hängt ab von:',
|
||||||
extension_rating_soon: 'Bewertungen sind bald verfügbar',
|
extension_rating_soon: 'Bewertungen sind bald verfügbar',
|
||||||
@@ -377,6 +378,16 @@ window.localisation.de = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'Stellen Sie sicher, dass die Autorisierungs-Callback-URL auf https://{domain}/api/v1/auth/keycloak/token eingestellt ist.',
|
'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_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',
|
currency_settings: 'Währungseinstellungen',
|
||||||
allowed_currencies: 'Erlaubte Währungen',
|
allowed_currencies: 'Erlaubte Währungen',
|
||||||
allowed_currencies_hint:
|
allowed_currencies_hint:
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ window.localisation.en = {
|
|||||||
only_admins_can_create_extensions:
|
only_admins_can_create_extensions:
|
||||||
'Only admin accounts can create extensions',
|
'Only admin accounts can create extensions',
|
||||||
admin_only: 'Admin Only',
|
admin_only: 'Admin Only',
|
||||||
|
super_user_only: 'Super User Only',
|
||||||
make_user_admin: 'Make User Admin',
|
make_user_admin: 'Make User Admin',
|
||||||
revoke_admin: 'Revoke Admin',
|
revoke_admin: 'Revoke Admin',
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
@@ -275,6 +276,8 @@ window.localisation.en = {
|
|||||||
requires_server_restart:
|
requires_server_restart:
|
||||||
'Changing these settings requires a server restart to take effect.',
|
'Changing these settings requires a server restart to take effect.',
|
||||||
funding_source_info: 'Select the active funding wallet',
|
funding_source_info: 'Select the active funding wallet',
|
||||||
|
phoenixd_warning:
|
||||||
|
"Phoenixd mnemonic is only available if phoenixd data-dir is specified and is readable by LNbits. It's not indicative of phoenixd not running. It just means LNbits cannot access the mnemonic to display it here.",
|
||||||
latest_update: 'You are on the latest version {version}.',
|
latest_update: 'You are on the latest version {version}.',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
notifications_configure: 'Configure Notifications',
|
notifications_configure: 'Configure Notifications',
|
||||||
@@ -611,6 +614,10 @@ window.localisation.en = {
|
|||||||
payment_timeouts: 'Payment Timeouts',
|
payment_timeouts: 'Payment Timeouts',
|
||||||
payment_wait_time: 'Payment Wait Time',
|
payment_wait_time: 'Payment Wait Time',
|
||||||
seconds: 'seconds',
|
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:
|
payment_wait_time_desc:
|
||||||
'Wait time before marking an outgoing payment as pending. Default: 5s; raise for slow-settling invoices.',
|
'Wait time before marking an outgoing payment as pending. Default: 5s; raise for slow-settling invoices.',
|
||||||
payment_wait_time_tooltip:
|
payment_wait_time_tooltip:
|
||||||
@@ -642,6 +649,14 @@ window.localisation.en = {
|
|||||||
auth_keycloak_cs_label: 'Keycloak Client Secret',
|
auth_keycloak_cs_label: 'Keycloak Client Secret',
|
||||||
auth_keycloak_custom_org_label: 'Keycloak Custom Organization',
|
auth_keycloak_custom_org_label: 'Keycloak Custom Organization',
|
||||||
auth_keycloak_custom_icon_label: 'Keycloak Custom Icon (URL)',
|
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',
|
currency_settings: 'Currency Settings',
|
||||||
allowed_currencies: 'Allowed Currencies',
|
allowed_currencies: 'Allowed Currencies',
|
||||||
allowed_currencies_hint:
|
allowed_currencies_hint:
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ window.localisation.es = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'(Solo las cuentas de administrador pueden instalar extensiones)',
|
'(Solo las cuentas de administrador pueden instalar extensiones)',
|
||||||
admin_only: 'Solo administradores',
|
admin_only: 'Solo administradores',
|
||||||
|
super_user_only: 'Solo superusuario',
|
||||||
new_version: 'Nueva Versión',
|
new_version: 'Nueva Versión',
|
||||||
extension_depends_on: 'Depende de:',
|
extension_depends_on: 'Depende de:',
|
||||||
extension_rating_soon: 'Calificaciones próximamente',
|
extension_rating_soon: 'Calificaciones próximamente',
|
||||||
@@ -379,6 +380,16 @@ window.localisation.es = {
|
|||||||
auth_keycloak_ci_hint:
|
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',
|
'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_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',
|
currency_settings: 'Configuración de moneda',
|
||||||
allowed_currencies: 'Monedas permitidas',
|
allowed_currencies: 'Monedas permitidas',
|
||||||
allowed_currencies_hint:
|
allowed_currencies_hint:
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ window.localisation.fi = {
|
|||||||
all: 'Kaikki',
|
all: 'Kaikki',
|
||||||
only_admins_can_install: '(Vain pääkäyttäjät voivat asentaa laajennuksia)',
|
only_admins_can_install: '(Vain pääkäyttäjät voivat asentaa laajennuksia)',
|
||||||
admin_only: 'Pääkäyttäjille',
|
admin_only: 'Pääkäyttäjille',
|
||||||
|
super_user_only: 'Vain superkäyttäjälle',
|
||||||
new_version: 'Uusi versio',
|
new_version: 'Uusi versio',
|
||||||
extension_depends_on: 'Edellyttää:',
|
extension_depends_on: 'Edellyttää:',
|
||||||
extension_rating_soon: 'Arvostelut on tulossa pian',
|
extension_rating_soon: 'Arvostelut on tulossa pian',
|
||||||
@@ -520,6 +521,14 @@ window.localisation.fi = {
|
|||||||
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
||||||
auth_keycloak_custom_org_label: 'Valinnainen Keycloak-organisaatio',
|
auth_keycloak_custom_org_label: 'Valinnainen Keycloak-organisaatio',
|
||||||
auth_keycloak_custom_icon_label: 'Valinnainen Keycloak-kuvake (URL)',
|
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',
|
currency_settings: 'Valuutta-asetukset',
|
||||||
allowed_currencies: 'Käytettävät valuutat',
|
allowed_currencies: 'Käytettävät valuutat',
|
||||||
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ window.localisation.fr = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Seuls les comptes administrateurs peuvent installer des extensions',
|
'Seuls les comptes administrateurs peuvent installer des extensions',
|
||||||
admin_only: 'Réservé aux administrateurs',
|
admin_only: 'Réservé aux administrateurs',
|
||||||
|
super_user_only: 'Réservé au super utilisateur',
|
||||||
new_version: 'Nouvelle version',
|
new_version: 'Nouvelle version',
|
||||||
extension_depends_on: 'Dépend de :',
|
extension_depends_on: 'Dépend de :',
|
||||||
extension_rating_soon: 'Notes des utilisateurs à venir bientôt',
|
extension_rating_soon: 'Notes des utilisateurs à venir bientôt',
|
||||||
@@ -381,6 +382,16 @@ window.localisation.fr = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
"Assurez-vous que l'URL de rappel d'autorisation est définie sur https://{domain}/api/v1/auth/keycloak/token",
|
"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_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',
|
currency_settings: 'Paramètres de devise',
|
||||||
allowed_currencies: 'Devises autorisées',
|
allowed_currencies: 'Devises autorisées',
|
||||||
allowed_currencies_hint:
|
allowed_currencies_hint:
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ window.localisation.it = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Solo gli account amministratore possono installare estensioni.',
|
'Solo gli account amministratore possono installare estensioni.',
|
||||||
admin_only: 'Solo amministratore',
|
admin_only: 'Solo amministratore',
|
||||||
|
super_user_only: 'Solo superutente',
|
||||||
new_version: 'Nuova Versione',
|
new_version: 'Nuova Versione',
|
||||||
extension_depends_on: 'Dipende da:',
|
extension_depends_on: 'Dipende da:',
|
||||||
extension_rating_soon: 'Valutazioni in arrivo',
|
extension_rating_soon: 'Valutazioni in arrivo',
|
||||||
@@ -378,6 +379,16 @@ window.localisation.it = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
"Assicurati che l'URL di callback dell'autorizzazione sia impostato su https://{domain}/api/v1/auth/keycloak/token",
|
"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_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',
|
currency_settings: 'Impostazioni valuta',
|
||||||
allowed_currencies: 'Valute consentite',
|
allowed_currencies: 'Valute consentite',
|
||||||
allowed_currencies_hint: 'Limita il numero di valute fiat disponibili',
|
allowed_currencies_hint: 'Limita il numero di valute fiat disponibili',
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ window.localisation.jp = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'(管理者アカウントのみが拡張機能をインストールできます)',
|
'(管理者アカウントのみが拡張機能をインストールできます)',
|
||||||
admin_only: '管理者のみ',
|
admin_only: '管理者のみ',
|
||||||
|
super_user_only: 'スーパー ユーザーのみ',
|
||||||
new_version: '新しいバージョン',
|
new_version: '新しいバージョン',
|
||||||
extension_depends_on: '依存先:',
|
extension_depends_on: '依存先:',
|
||||||
extension_rating_soon: '評価は近日公開',
|
extension_rating_soon: '評価は近日公開',
|
||||||
@@ -369,6 +370,15 @@ window.localisation.jp = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'認証コールバックURLが https://{domain}/api/v1/auth/keycloak/token に設定されていることを確認してください。',
|
'認証コールバックURLが https://{domain}/api/v1/auth/keycloak/token に設定されていることを確認してください。',
|
||||||
auth_keycloak_cs_label: 'キークローククライアントシークレット',
|
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: '通貨設定',
|
currency_settings: '通貨設定',
|
||||||
allowed_currencies: '許可されている通貨',
|
allowed_currencies: '許可されている通貨',
|
||||||
allowed_currencies_hint: '利用可能な法定通貨の数を制限する',
|
allowed_currencies_hint: '利用可能な法定通貨の数を制限する',
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ window.localisation.kr = {
|
|||||||
all: '전체',
|
all: '전체',
|
||||||
only_admins_can_install: '(관리자 계정만이 확장 기능을 설치할 수 있습니다)',
|
only_admins_can_install: '(관리자 계정만이 확장 기능을 설치할 수 있습니다)',
|
||||||
admin_only: '관리자 전용',
|
admin_only: '관리자 전용',
|
||||||
|
super_user_only: '슈퍼유저 전용',
|
||||||
new_version: '새로운 버전',
|
new_version: '새로운 버전',
|
||||||
extension_depends_on: '의존성 존재:',
|
extension_depends_on: '의존성 존재:',
|
||||||
extension_rating_soon: '평점 기능도 곧 구현됩니다',
|
extension_rating_soon: '평점 기능도 곧 구현됩니다',
|
||||||
@@ -365,6 +366,16 @@ window.localisation.kr = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'승인 콜백 URL이 https://{domain}/api/v1/auth/keycloak/token으로 설정되어 있는지 확인하십시오.',
|
'승인 콜백 URL이 https://{domain}/api/v1/auth/keycloak/token으로 설정되어 있는지 확인하십시오.',
|
||||||
auth_keycloak_cs_label: 'Keycloak 클라이언트 시크릿',
|
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: '통화 설정',
|
currency_settings: '통화 설정',
|
||||||
allowed_currencies: '허용되는 통화',
|
allowed_currencies: '허용되는 통화',
|
||||||
allowed_currencies_hint: '사용 가능한 법정 화폐의 수를 제한하십시오.',
|
allowed_currencies_hint: '사용 가능한 법정 화폐의 수를 제한하십시오.',
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ window.localisation.nl = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Alleen beheerdersaccounts kunnen extensies installeren',
|
'Alleen beheerdersaccounts kunnen extensies installeren',
|
||||||
admin_only: 'Alleen beheerder',
|
admin_only: 'Alleen beheerder',
|
||||||
|
super_user_only: 'Alleen supergebruiker',
|
||||||
new_version: 'Nieuwe Versie',
|
new_version: 'Nieuwe Versie',
|
||||||
extension_depends_on: 'Afhankelijk van:',
|
extension_depends_on: 'Afhankelijk van:',
|
||||||
extension_rating_soon: 'Beoordelingen binnenkort beschikbaar',
|
extension_rating_soon: 'Beoordelingen binnenkort beschikbaar',
|
||||||
@@ -377,6 +378,16 @@ window.localisation.nl = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'Zorg ervoor dat de autorisatie callback-URL is ingesteld op https://{domain}/api/v1/auth/keycloak/token',
|
'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_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',
|
currency_settings: 'Valuta-instellingen',
|
||||||
allowed_currencies: "Toegestane valuta's",
|
allowed_currencies: "Toegestane valuta's",
|
||||||
allowed_currencies_hint: "Beperk het aantal beschikbare fiatvaluta's",
|
allowed_currencies_hint: "Beperk het aantal beschikbare fiatvaluta's",
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ window.localisation.pi = {
|
|||||||
all: 'Arr',
|
all: 'Arr',
|
||||||
only_admins_can_install: '(Only admin accounts can install extensions)',
|
only_admins_can_install: '(Only admin accounts can install extensions)',
|
||||||
admin_only: "Cap'n Only",
|
admin_only: "Cap'n Only",
|
||||||
|
super_user_only: "Big Cap'n Only",
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
extension_depends_on: 'Depends on:',
|
extension_depends_on: 'Depends on:',
|
||||||
extension_rating_soon: "Ratings a'comin' soon",
|
extension_rating_soon: "Ratings a'comin' soon",
|
||||||
@@ -371,6 +372,16 @@ window.localisation.pi = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
"Make sure thant th' authorization callback URL be set t' https://{domain}/api/v1/auth/keycloak/token",
|
"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_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",
|
currency_settings: "Doubloon Settin's",
|
||||||
allowed_currencies: "Allo'ed Doubloons",
|
allowed_currencies: "Allo'ed Doubloons",
|
||||||
allowed_currencies_hint: 'Limit the number of available fiat doubloons',
|
allowed_currencies_hint: 'Limit the number of available fiat doubloons',
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ window.localisation.pl = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Tylko konta administratorów mogą instalować rozszerzenia',
|
'Tylko konta administratorów mogą instalować rozszerzenia',
|
||||||
admin_only: 'Tylko dla administratora',
|
admin_only: 'Tylko dla administratora',
|
||||||
|
super_user_only: 'Tylko dla superużytkownika',
|
||||||
new_version: 'Nowa wersja',
|
new_version: 'Nowa wersja',
|
||||||
extension_depends_on: 'Zależy od:',
|
extension_depends_on: 'Zależy od:',
|
||||||
extension_rating_soon: 'Oceny będą dostępne wkrótce',
|
extension_rating_soon: 'Oceny będą dostępne wkrótce',
|
||||||
@@ -372,6 +373,16 @@ window.localisation.pl = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'Upewnij się, że URL zwrotu autoryzacji jest ustawiony na https://{domain}/api/v1/auth/keycloak/token',
|
'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_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',
|
currency_settings: 'Ustawienia waluty',
|
||||||
allowed_currencies: 'Dozwolone waluty',
|
allowed_currencies: 'Dozwolone waluty',
|
||||||
allowed_currencies_hint: 'Ogranicz liczbę dostępnych walut fiducjarnych',
|
allowed_currencies_hint: 'Ogranicz liczbę dostępnych walut fiducjarnych',
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ window.localisation.pt = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Apenas contas de administrador podem instalar extensões.',
|
'Apenas contas de administrador podem instalar extensões.',
|
||||||
admin_only: 'Apenas para administradores',
|
admin_only: 'Apenas para administradores',
|
||||||
|
super_user_only: 'Apenas para superusuário',
|
||||||
new_version: 'Nova Versão',
|
new_version: 'Nova Versão',
|
||||||
extension_depends_on: 'Depende de:',
|
extension_depends_on: 'Depende de:',
|
||||||
extension_rating_soon: 'Avaliações em breve',
|
extension_rating_soon: 'Avaliações em breve',
|
||||||
@@ -375,6 +376,16 @@ window.localisation.pt = {
|
|||||||
auth_keycloak_ci_hint:
|
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',
|
'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_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',
|
currency_settings: 'Configurações de Moeda',
|
||||||
allowed_currencies: 'Moedas Permitidas',
|
allowed_currencies: 'Moedas Permitidas',
|
||||||
allowed_currencies_hint: 'Limite o número de moedas fiduciárias disponíveis',
|
allowed_currencies_hint: 'Limite o número de moedas fiduciárias disponíveis',
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ window.localisation.sk = {
|
|||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'(Iba administrátorské účty môžu inštalovať rozšírenia)',
|
'(Iba administrátorské účty môžu inštalovať rozšírenia)',
|
||||||
admin_only: 'Iba pre administrátorov',
|
admin_only: 'Iba pre administrátorov',
|
||||||
|
super_user_only: 'Iba pre superužívateľa',
|
||||||
new_version: 'Nová verzia',
|
new_version: 'Nová verzia',
|
||||||
extension_depends_on: 'Závisí na:',
|
extension_depends_on: 'Závisí na:',
|
||||||
extension_rating_soon: 'Hodnotenia budú čoskoro dostupné',
|
extension_rating_soon: 'Hodnotenia budú čoskoro dostupné',
|
||||||
@@ -371,6 +372,16 @@ window.localisation.sk = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'Uistite sa, že URL spätného volania autorizácie je nastavená na https://{domain}/api/v1/auth/keycloak/token',
|
'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_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',
|
currency_settings: 'Nastavenia meny',
|
||||||
allowed_currencies: 'Povolené meny',
|
allowed_currencies: 'Povolené meny',
|
||||||
allowed_currencies_hint: 'Obmedzte počet dostupných fiat mien',
|
allowed_currencies_hint: 'Obmedzte počet dostupných fiat mien',
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ window.localisation.we = {
|
|||||||
all: 'Pob',
|
all: 'Pob',
|
||||||
only_admins_can_install: 'Dim ond cyfrifon gweinyddwr all osod estyniadau',
|
only_admins_can_install: 'Dim ond cyfrifon gweinyddwr all osod estyniadau',
|
||||||
admin_only: 'Dim ond Gweinyddwr',
|
admin_only: 'Dim ond Gweinyddwr',
|
||||||
|
super_user_only: 'Dim ond Uwchddefnyddiwr',
|
||||||
new_version: 'Fersiwn Newydd',
|
new_version: 'Fersiwn Newydd',
|
||||||
extension_depends_on: 'Dibynnu ar:',
|
extension_depends_on: 'Dibynnu ar:',
|
||||||
extension_rating_soon: 'Sgôr yn dod yn fuan',
|
extension_rating_soon: 'Sgôr yn dod yn fuan',
|
||||||
@@ -370,6 +371,16 @@ window.localisation.we = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
"Gwnewch yn siŵr bod URL adalw awdurdodiad wedi'i osod i https://{domain}/api/v1/auth/keycloak/token",
|
"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_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',
|
currency_settings: 'Gosodiadau Arian Cyfred',
|
||||||
allowed_currencies: 'Ariannau a Ganiateir',
|
allowed_currencies: 'Ariannau a Ganiateir',
|
||||||
allowed_currencies_hint: 'Cyfyngu nifer yr arian cyfred fiat sydd ar gael',
|
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',
|
'nostr-auth-nip98',
|
||||||
'google-auth',
|
'google-auth',
|
||||||
'github-auth',
|
'github-auth',
|
||||||
'keycloak-auth'
|
'keycloak-auth',
|
||||||
|
'oidc-auth'
|
||||||
],
|
],
|
||||||
username: this.userName,
|
username: this.userName,
|
||||||
password: this.password_1,
|
password: this.password_1,
|
||||||
@@ -691,12 +692,10 @@ window.app.component('lnbits-node-qrcode', {
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
<div style="text-align: center">
|
<div style="text-align: center">
|
||||||
<vue-qrcode
|
<lnbits-qrcode
|
||||||
:value="info.addresses[0]"
|
|
||||||
:options="{width: 250}"
|
|
||||||
v-if='info.addresses[0]'
|
v-if='info.addresses[0]'
|
||||||
class="rounded-borders"
|
:value="info.addresses[0]"
|
||||||
></vue-qrcode>
|
></lnbits-qrcode>
|
||||||
<div v-else class='text-subtitle1'>
|
<div v-else class='text-subtitle1'>
|
||||||
No addresses available
|
No addresses available
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -202,7 +202,18 @@ window.app.component('lnbits-admin-funding-sources', {
|
|||||||
'Phoenixd',
|
'Phoenixd',
|
||||||
{
|
{
|
||||||
phoenixd_api_endpoint: 'Endpoint',
|
phoenixd_api_endpoint: 'Endpoint',
|
||||||
phoenixd_api_password: 'Key'
|
phoenixd_api_password: 'Key',
|
||||||
|
phoenixd_data_dir: {
|
||||||
|
label: 'Data Directory',
|
||||||
|
hint: 'Directory where phoenixd stores its data, including the seed phrase.'
|
||||||
|
},
|
||||||
|
phoenixd_mnemonic: {
|
||||||
|
label: 'Phoenixd Seed Phrase',
|
||||||
|
hint: 'Only available if phoenixd data-dir is specified',
|
||||||
|
readonly: true,
|
||||||
|
copy: true,
|
||||||
|
qrcode: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -41,5 +41,37 @@ window.app.component('lnbits-admin-site-customisation', {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {}
|
methods: {
|
||||||
|
onBackgroundImageInput(e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
this.uploadBackgroundImage(file)
|
||||||
|
}
|
||||||
|
e.target.value = null
|
||||||
|
},
|
||||||
|
async uploadBackgroundImage(file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/api/v1/assets?public_asset=true',
|
||||||
|
null,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {'Content-Type': 'multipart/form-data'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const assetUrl = `${window.location.origin}/api/v1/assets/${data.id}/thumbnail`
|
||||||
|
this.formData.lnbits_default_bgimage = assetUrl
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Background image uploaded.',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -165,5 +165,26 @@ window.app.component('lnbits-qrcode', {
|
|||||||
this.$refs.qrCode.$el.style.maxWidth = this.maxWidth + 'px'
|
this.$refs.qrCode.$el.style.maxWidth = this.maxWidth + 'px'
|
||||||
this.$refs.qrCode.$el.setAttribute('width', '100%')
|
this.$refs.qrCode.$el.setAttribute('width', '100%')
|
||||||
this.$refs.qrCode.$el.removeAttribute('height')
|
this.$refs.qrCode.$el.removeAttribute('height')
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
optimizedValue() {
|
||||||
|
const separatorIndex = this.value.indexOf(':')
|
||||||
|
const type =
|
||||||
|
separatorIndex === -1 ? '' : this.value.substring(0, separatorIndex)
|
||||||
|
const value =
|
||||||
|
separatorIndex === -1
|
||||||
|
? this.value
|
||||||
|
: this.value.substring(separatorIndex + 1)
|
||||||
|
|
||||||
|
if (this.utils.isValidBech32(value)) {
|
||||||
|
const normalizedValue = value.toUpperCase()
|
||||||
|
if (type) {
|
||||||
|
return `${type.toUpperCase()}:${normalizedValue}`
|
||||||
|
}
|
||||||
|
return normalizedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ window.PageAccount = {
|
|||||||
name: 'bitcoin',
|
name: 'bitcoin',
|
||||||
color: 'deep-orange'
|
color: 'deep-orange'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'classic',
|
||||||
|
color: 'purple'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'mint',
|
name: 'mint',
|
||||||
color: 'green'
|
color: 'green'
|
||||||
@@ -541,31 +545,56 @@ window.PageAccount = {
|
|||||||
if (file) {
|
if (file) {
|
||||||
this.uploadAsset(file)
|
this.uploadAsset(file)
|
||||||
}
|
}
|
||||||
|
e.target.value = null
|
||||||
},
|
},
|
||||||
async uploadAsset(file) {
|
onBackgroundImageInput(e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
this.uploadBackgroundImage(file)
|
||||||
|
}
|
||||||
|
e.target.value = null
|
||||||
|
},
|
||||||
|
async uploadAsset(
|
||||||
|
file,
|
||||||
|
{isPublic = this.assetsUploadToPublic, notifySuccess = true} = {}
|
||||||
|
) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
try {
|
try {
|
||||||
await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'POST',
|
'POST',
|
||||||
`/api/v1/assets?public_asset=${this.assetsUploadToPublic}`,
|
`/api/v1/assets?public_asset=${isPublic}`,
|
||||||
null,
|
null,
|
||||||
formData,
|
formData,
|
||||||
{
|
{
|
||||||
headers: {'Content-Type': 'multipart/form-data'}
|
headers: {'Content-Type': 'multipart/form-data'}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
this.$q.notify({
|
if (notifySuccess) {
|
||||||
type: 'positive',
|
this.$q.notify({
|
||||||
message: 'Upload successful!',
|
type: 'positive',
|
||||||
icon: null
|
message: 'Upload successful!',
|
||||||
})
|
icon: null
|
||||||
|
})
|
||||||
|
}
|
||||||
await this.getUserAssets()
|
await this.getUserAssets()
|
||||||
|
return data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async uploadBackgroundImage(file) {
|
||||||
|
const asset = await this.uploadAsset(file, {
|
||||||
|
isPublic: false,
|
||||||
|
notifySuccess: false
|
||||||
|
})
|
||||||
|
if (!asset) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/thumbnail`
|
||||||
|
await this.siteCustomisationChanged({bgimageChoice: assetUrl})
|
||||||
|
},
|
||||||
async deleteAsset(asset) {
|
async deleteAsset(asset) {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this asset?')
|
.confirmDialog('Are you sure you want to delete this asset?')
|
||||||
|
|||||||
@@ -160,6 +160,46 @@ window._lnbitsUtils = {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isValidBech32(value) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = value.trim()
|
||||||
|
if (
|
||||||
|
!candidate ||
|
||||||
|
(candidate !== candidate.toLowerCase() &&
|
||||||
|
candidate !== candidate.toUpperCase())
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = candidate.toLowerCase()
|
||||||
|
const splitPosition = normalized.lastIndexOf('1')
|
||||||
|
if (splitPosition <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const humanReadablePart = normalized.substring(0, splitPosition)
|
||||||
|
const data = normalized.substring(splitPosition + 1)
|
||||||
|
if (data.length < 6) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof bech32ToFiveBitArray !== 'function' ||
|
||||||
|
typeof verify_checksum !== 'function'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const words = bech32ToFiveBitArray(data)
|
||||||
|
if (words.some(word => word < 0)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return verify_checksum(humanReadablePart, words)
|
||||||
|
},
|
||||||
async notifyApiError(error) {
|
async notifyApiError(error) {
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
return console.error(error)
|
return console.error(error)
|
||||||
|
|||||||
@@ -5,12 +5,8 @@ from collections.abc import Callable, Coroutine
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
from lnbits.core.models import Payment
|
||||||
get_standalone_payment,
|
from lnbits.core.services.payments import update_invoice_callback
|
||||||
update_payment,
|
|
||||||
)
|
|
||||||
from lnbits.core.models import Payment, PaymentState
|
|
||||||
from lnbits.core.services.fiat_providers import handle_fiat_payment_confirmation
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
|
|
||||||
@@ -116,7 +112,10 @@ async def internal_invoice_listener() -> None:
|
|||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
checking_id = await internal_invoice_queue.get()
|
checking_id = await internal_invoice_queue.get()
|
||||||
logger.info(f"got an internal payment notification {checking_id}")
|
logger.info(f"got an internal payment notification {checking_id}")
|
||||||
await invoice_callback_dispatcher(checking_id, is_internal=True)
|
payment = await update_invoice_callback(checking_id)
|
||||||
|
if payment:
|
||||||
|
logger.success(f"internal invoice {checking_id} settled")
|
||||||
|
await invoice_callback_dispatcher(payment)
|
||||||
|
|
||||||
|
|
||||||
async def invoice_listener() -> None:
|
async def invoice_listener() -> None:
|
||||||
@@ -129,7 +128,10 @@ async def invoice_listener() -> None:
|
|||||||
funding_source = get_funding_source()
|
funding_source = get_funding_source()
|
||||||
async for checking_id in funding_source.paid_invoices_stream():
|
async for checking_id in funding_source.paid_invoices_stream():
|
||||||
logger.info(f"got a payment notification {checking_id}")
|
logger.info(f"got a payment notification {checking_id}")
|
||||||
await invoice_callback_dispatcher(checking_id)
|
payment = await update_invoice_callback(checking_id)
|
||||||
|
if payment:
|
||||||
|
logger.success(f"fundingsource invoice {checking_id} settled")
|
||||||
|
await invoice_callback_dispatcher(payment)
|
||||||
|
|
||||||
|
|
||||||
def wait_for_paid_invoices(
|
def wait_for_paid_invoices(
|
||||||
@@ -165,33 +167,7 @@ def run_interval(
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = False):
|
async def invoice_callback_dispatcher(payment: Payment):
|
||||||
"""
|
|
||||||
Takes an incoming payment, checks its status, and dispatches it to
|
|
||||||
invoice_listeners from core and extensions.
|
|
||||||
"""
|
|
||||||
payment = await get_standalone_payment(checking_id, incoming=True)
|
|
||||||
if not payment:
|
|
||||||
logger.warning(f"No payment found for '{checking_id}'.")
|
|
||||||
return
|
|
||||||
if not payment.is_in:
|
|
||||||
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
|
|
||||||
return
|
|
||||||
|
|
||||||
from lnbits.core.services.payments import check_payment_status
|
|
||||||
|
|
||||||
status = await check_payment_status(
|
|
||||||
payment, skip_internal_payment_notifications=True
|
|
||||||
)
|
|
||||||
payment.fee = status.fee_msat or payment.fee
|
|
||||||
# only overwrite preimage if status.preimage provides it
|
|
||||||
payment.preimage = status.preimage or payment.preimage
|
|
||||||
payment.status = PaymentState.SUCCESS
|
|
||||||
await update_payment(payment)
|
|
||||||
if payment.fiat_provider:
|
|
||||||
await handle_fiat_payment_confirmation(payment)
|
|
||||||
internal = "internal" if is_internal else ""
|
|
||||||
logger.success(f"{internal} invoice {checking_id} settled")
|
|
||||||
for name, send_chan in invoice_listeners.items():
|
for name, send_chan in invoice_listeners.items():
|
||||||
logger.trace(f"invoice listeners: sending to `{name}`")
|
logger.trace(f"invoice listeners: sending to `{name}`")
|
||||||
await send_chan.put(payment)
|
await send_chan.put(payment)
|
||||||
|
|||||||
@@ -82,9 +82,8 @@
|
|||||||
<!-- scripts libraries -->
|
<!-- scripts libraries -->
|
||||||
{% for url in INCLUDED_JS %}
|
{% for url in INCLUDED_JS %}
|
||||||
<script src="{{ static_url_for('static', url) }}"></script>
|
<script src="{{ static_url_for('static', url) }}"></script>
|
||||||
{% endfor %}
|
{% endfor %} {% if user %}
|
||||||
<!-- user init -->
|
<!-- user init -->
|
||||||
{% if user %}
|
|
||||||
<script>
|
<script>
|
||||||
window.g.user = LNbits.map.user(JSON.parse({{ user | tojson | safe }}));
|
window.g.user = LNbits.map.user(JSON.parse({{ user | tojson | safe }}));
|
||||||
{% if not public %}
|
{% if not public %}
|
||||||
@@ -92,6 +91,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<!-- app init -->
|
||||||
|
<script>
|
||||||
|
window.app = Vue.createApp({
|
||||||
|
el: '#vue'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
<!-- scripts from extensions -->
|
<!-- scripts from extensions -->
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
<!-- components js -->
|
<!-- components js -->
|
||||||
|
|||||||
@@ -1050,6 +1050,34 @@ include('components/lnbits-error.vue') %}
|
|||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</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>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -75,7 +75,35 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md">
|
<div v-if="isSuperUser">
|
||||||
|
<lnbits-admin-funding-sources
|
||||||
|
:form-data="formData"
|
||||||
|
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
||||||
|
/>
|
||||||
|
<div class="row q-col-gutter-md q-my-md">
|
||||||
|
<div class="col-12 col-sm-8">
|
||||||
|
<q-item tag="div">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
v-text="$t('funding_source_retries')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('funding_source_retries_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.funding_source_max_retries"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-col-gutter-md q-mt-lg">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none">
|
||||||
<span v-text="$t('routing_fee_reserve_calculations')"></span>
|
<span v-text="$t('routing_fee_reserve_calculations')"></span>
|
||||||
@@ -160,33 +188,26 @@
|
|||||||
min="0"
|
min="0"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-12 col-md-4">
|
||||||
<div v-if="isSuperUser">
|
<p>
|
||||||
<lnbits-admin-funding-sources
|
<span v-text="$t('payment_pending_interval')"></span>
|
||||||
:form-data="formData"
|
<sup>
|
||||||
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
<q-icon name="info" size="16px" class="q-ml-xs"></q-icon>
|
||||||
/>
|
<q-tooltip max-width="150px">
|
||||||
<div class="row q-col-gutter-md q-my-md">
|
<span v-text="$t('payment_pending_interval_tooltip')"></span>
|
||||||
<div class="col-12 col-sm-8">
|
</q-tooltip>
|
||||||
<q-item tag="div">
|
</sup>
|
||||||
<q-item-section>
|
</p>
|
||||||
<q-item-label
|
<q-input
|
||||||
v-text="$t('funding_source_retries')"
|
type="number"
|
||||||
></q-item-label>
|
filled
|
||||||
<q-item-label
|
name="lnbits_funding_source_pending_interval_seconds"
|
||||||
caption
|
v-model="formData.lnbits_funding_source_pending_interval_seconds"
|
||||||
v-text="$t('funding_source_retries_desc')"
|
:label="$t('payment_pending_interval')"
|
||||||
></q-item-label>
|
:hint="$t('payment_pending_interval_desc')"
|
||||||
</q-item-section>
|
step="1"
|
||||||
<q-item-section>
|
min="0"
|
||||||
<q-input
|
></q-input>
|
||||||
filled
|
|
||||||
v-model="formData.funding_source_max_retries"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
:label="prop.label"
|
:label="prop.label"
|
||||||
:hint="prop.hint"
|
:hint="prop.hint"
|
||||||
:value="prop.value"
|
:value="prop.value"
|
||||||
|
:readonly="prop.readonly || false"
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="prop.copy"
|
v-if="prop.copy"
|
||||||
@@ -73,6 +74,15 @@
|
|||||||
></q-btn>
|
></q-btn>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="fund === 'PhoenixdWallet' && key === 'phoenixd_mnemonic'"
|
||||||
|
class="col-12 q-my-md"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<q-icon name="warning" color="orange" size="xs"></q-icon>
|
||||||
|
<span v-text="$t('phoenixd_warning')"></span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="
|
v-if="
|
||||||
|
|||||||
@@ -192,6 +192,57 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</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-separator></q-separator>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -251,10 +251,27 @@
|
|||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_default_bgimage"
|
v-model="formData.lnbits_default_bgimage"
|
||||||
label="Background Image"
|
label="Background Image"
|
||||||
@update:model-value="applyGlobalBgimage"
|
|
||||||
hint="This must be a trusted source. It can change the content and it can log your IP address."
|
hint="This must be a trusted source. It can change the content and it can log your IP address."
|
||||||
>
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
icon="upload"
|
||||||
|
@click="$refs.adminBackgroundImageInput.click()"
|
||||||
|
>
|
||||||
|
<q-tooltip>Upload background image</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="adminBackgroundImageInput"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
@change="onBackgroundImageInput"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
<div class="row q-col-gutter-md q-mb-md">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
>
|
>
|
||||||
<qrcode-vue
|
<qrcode-vue
|
||||||
ref="qrCode"
|
ref="qrCode"
|
||||||
:value="value"
|
:value="optimizedValue"
|
||||||
:margin="margin"
|
:margin="margin"
|
||||||
:size="size"
|
:size="size"
|
||||||
level="Q"
|
level="Q"
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
|
{% extends "base.html" %} {% block page_container %}
|
||||||
context %} {% block scripts %} {{ window_vars() }} {% endblock %} {% block
|
|
||||||
page_container %}
|
|
||||||
<lnbits-error
|
<lnbits-error
|
||||||
code="{{ status_code | safe }}"
|
code="{{ status_code | safe }}"
|
||||||
message="{{ message | safe }}"
|
message="{{ message | safe }}"
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
{% macro window_vars(user) -%}
|
{% macro window_vars(user) -%}
|
||||||
<script>
|
<script>
|
||||||
//Needed for Vue to create the app on first load (although called on every page, its only loaded once)
|
// deprecated dont use window_vars anymore
|
||||||
window.app = Vue.createApp({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [window.windowMixin]
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|||||||
@@ -262,7 +262,9 @@
|
|||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
'google-auth' in g.settings.authMethods ||
|
'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"
|
class="col q-pa-sm text-h6"
|
||||||
>
|
>
|
||||||
@@ -310,6 +312,58 @@
|
|||||||
<div>GitHub</div>
|
<div>GitHub</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
@@ -438,10 +492,28 @@
|
|||||||
siteCustomisationChanged({bgimageChoice: $event})
|
siteCustomisationChanged({bgimageChoice: $event})
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
icon="upload"
|
||||||
|
@click="$refs.backgroundImageInput.click()"
|
||||||
|
>
|
||||||
|
<q-tooltip>Upload background image</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('background_image')"></span
|
><span v-text="$t('background_image')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="backgroundImageInput"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
@change="onBackgroundImageInput"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
@@ -1054,82 +1126,130 @@
|
|||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-btn-dropdown
|
<div
|
||||||
color="grey"
|
class="row items-center no-wrap q-col-gutter-sm"
|
||||||
dense
|
|
||||||
outline
|
|
||||||
no-caps
|
|
||||||
:label="props.row.name"
|
|
||||||
:icon="props.row.is_public ? 'public' : ''"
|
|
||||||
>
|
>
|
||||||
<q-list>
|
<div class="col">
|
||||||
<q-item
|
<q-btn-dropdown
|
||||||
clickable
|
color="grey"
|
||||||
v-close-popup
|
dense
|
||||||
@click="copyAssetLinkToClipboard(props.row)"
|
outline
|
||||||
|
no-caps
|
||||||
|
class="full-width"
|
||||||
|
:label="props.row.name"
|
||||||
|
:icon="props.row.is_public ? 'public' : ''"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-list>
|
||||||
<q-avatar icon="content_copy" />
|
<q-item
|
||||||
</q-item-section>
|
clickable
|
||||||
<q-item-section>
|
v-close-popup
|
||||||
<q-item-label>Copy Link</q-item-label>
|
@click="
|
||||||
<q-item-label caption
|
copyAssetLinkToClipboard(props.row)
|
||||||
>Copy asset link to
|
|
||||||
clipboard</q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
v-close-popup
|
|
||||||
@click="toggleAssetPublicAccess(props.row)"
|
|
||||||
>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar
|
|
||||||
:icon="
|
|
||||||
props.row.is_public
|
|
||||||
? 'public_off'
|
|
||||||
: 'public'
|
|
||||||
"
|
"
|
||||||
text-color="primary"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section v-if="props.row.is_public">
|
|
||||||
<q-item-label>Unpublish</q-item-label>
|
|
||||||
<q-item-label caption
|
|
||||||
>Make this asset private</q-item-label
|
|
||||||
>
|
>
|
||||||
</q-item-section>
|
<q-item-section avatar>
|
||||||
<q-item-section v-else>
|
<q-avatar icon="content_copy" />
|
||||||
<q-item-label>Publish</q-item-label>
|
</q-item-section>
|
||||||
<q-item-label caption
|
<q-item-section>
|
||||||
>Make this asset public</q-item-label
|
<q-item-label>Copy Link</q-item-label>
|
||||||
>
|
<q-item-label caption
|
||||||
</q-item-section>
|
>Copy asset link to
|
||||||
</q-item>
|
clipboard</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
<q-item
|
<q-item
|
||||||
clickable
|
clickable
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="deleteAsset(props.row)"
|
@click="
|
||||||
>
|
toggleAssetPublicAccess(props.row)
|
||||||
<q-item-section avatar>
|
"
|
||||||
<q-avatar
|
|
||||||
icon="delete"
|
|
||||||
text-color="negative"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Delete</q-item-label>
|
|
||||||
<q-item-label caption
|
|
||||||
>Permanently delete this
|
|
||||||
asset</q-item-label
|
|
||||||
>
|
>
|
||||||
</q-item-section>
|
<q-item-section avatar>
|
||||||
</q-item>
|
<q-avatar
|
||||||
</q-list>
|
:icon="
|
||||||
</q-btn-dropdown>
|
props.row.is_public
|
||||||
|
? 'public_off'
|
||||||
|
: 'public'
|
||||||
|
"
|
||||||
|
text-color="primary"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section
|
||||||
|
v-if="props.row.is_public"
|
||||||
|
>
|
||||||
|
<q-item-label>Unpublish</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Make this asset
|
||||||
|
private</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section v-else>
|
||||||
|
<q-item-label>Publish</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Make this asset
|
||||||
|
public</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="deleteAsset(props.row)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar
|
||||||
|
icon="delete"
|
||||||
|
text-color="negative"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Permanently delete this
|
||||||
|
asset</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn
|
||||||
|
type="a"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
color="primary"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
icon="image"
|
||||||
|
:href="`/api/v1/assets/${props.row.id}/data`"
|
||||||
|
>
|
||||||
|
<q-tooltip>Full image</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-auto"
|
||||||
|
v-if="props.row.thumbnail_base64"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
type="a"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
color="secondary"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
icon="photo_size_select_small"
|
||||||
|
:href="`/api/v1/assets/${props.row.id}/thumbnail`"
|
||||||
|
>
|
||||||
|
<q-tooltip>Thumbnail</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -307,7 +307,13 @@
|
|||||||
:label="$t('disable')"
|
:label="$t('disable')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="extension.isAdminOnly && !g.user.admin"
|
v-if="extension.isSuperUserOnly && !g.user.super_user"
|
||||||
|
v-text="$t('super_user_only')"
|
||||||
|
>
|
||||||
|
</q-badge>
|
||||||
|
|
||||||
|
<q-badge
|
||||||
|
v-else-if="extension.isAdminOnly && !g.user.admin"
|
||||||
v-text="$t('admin_only')"
|
v-text="$t('admin_only')"
|
||||||
>
|
>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
@@ -316,7 +322,9 @@
|
|||||||
v-else-if="
|
v-else-if="
|
||||||
extension.isInstalled &&
|
extension.isInstalled &&
|
||||||
extension.isActive &&
|
extension.isActive &&
|
||||||
!g.user.extensions.includes(extension.id)
|
!g.user.extensions.includes(extension.id) &&
|
||||||
|
(!extension.isAdminOnly || g.user.admin) &&
|
||||||
|
(!extension.isSuperUserOnly || g.user.super_user)
|
||||||
"
|
"
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -438,7 +446,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div v-if="selectedRelease.paymentRequest">
|
<div v-if="selectedRelease.paymentRequest">
|
||||||
<lnbits-qrcode
|
<lnbits-qrcode
|
||||||
:value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()"
|
:value="'LIGHTNING:' + selectedRelease.paymentRequest.toUpperCase()"
|
||||||
:href="'lightning:' + selectedRelease.paymentRequest"
|
:href="'lightning:' + selectedRelease.paymentRequest"
|
||||||
></lnbits-qrcode>
|
></lnbits-qrcode>
|
||||||
</div>
|
</div>
|
||||||
@@ -836,7 +844,7 @@
|
|||||||
<div v-if="selectedExtension.payToEnable.paymentRequest" class="col">
|
<div v-if="selectedExtension.payToEnable.paymentRequest" class="col">
|
||||||
<lnbits-qrcode
|
<lnbits-qrcode
|
||||||
:value="
|
:value="
|
||||||
'lightning:' +
|
'LIGHTNING:' +
|
||||||
selectedExtension.payToEnable.paymentRequest.toUpperCase()
|
selectedExtension.payToEnable.paymentRequest.toUpperCase()
|
||||||
"
|
"
|
||||||
:href="
|
:href="
|
||||||
@@ -1247,13 +1255,10 @@
|
|||||||
<q-dialog v-model="paymentDialog.show" position="top">
|
<q-dialog v-model="paymentDialog.show" position="top">
|
||||||
<q-card class="q-pa-md lnbits__dialog-card">
|
<q-card class="q-pa-md lnbits__dialog-card">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-xl">
|
<lnbits-qrcode
|
||||||
<lnbits-qrcode
|
:value="'LIGHTNING:' + paymentDialog.invoice.toUpperCase()"
|
||||||
:value="paymentDialog.invoice"
|
:href="'lightning:' + paymentDialog.invoice"
|
||||||
:options="{width: 800}"
|
></lnbits-qrcode>
|
||||||
class="rounded-borders"
|
|
||||||
></lnbits-qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="between">
|
<q-card-actions align="between">
|
||||||
<q-btn v-close-popup flat color="grey" :label="$t('close')"></q-btn>
|
<q-btn v-close-popup flat color="grey" :label="$t('close')"></q-btn>
|
||||||
|
|||||||
@@ -589,23 +589,16 @@
|
|||||||
v-if="transactionDetailsDialog.data.bolt11"
|
v-if="transactionDetailsDialog.data.bolt11"
|
||||||
class="text-center q-mb-lg"
|
class="text-center q-mb-lg"
|
||||||
>
|
>
|
||||||
<a
|
<lnbits-qrcode
|
||||||
:href="
|
:href="
|
||||||
'lightning:' +
|
'lightning:' +
|
||||||
transactionDetailsDialog.data.bolt11
|
transactionDetailsDialog.data.bolt11
|
||||||
"
|
"
|
||||||
>
|
:value="
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
'LIGHTNING:' +
|
||||||
<qrcode-vue
|
transactionDetailsDialog.data.bolt11.toUpperCase()
|
||||||
:value="
|
"
|
||||||
'lightning:' +
|
></lnbits-qrcode>
|
||||||
transactionDetailsDialog.data.bolt11.toUpperCase()
|
|
||||||
"
|
|
||||||
:options="{width: 340}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode-vue>
|
|
||||||
</q-responsive>
|
|
||||||
</a>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
color="grey"
|
color="grey"
|
||||||
@@ -698,25 +691,15 @@
|
|||||||
v-if="props.row.bolt11"
|
v-if="props.row.bolt11"
|
||||||
class="text-center q-mb-lg"
|
class="text-center q-mb-lg"
|
||||||
>
|
>
|
||||||
<a
|
<lnbits-qrcode
|
||||||
|
:value="
|
||||||
|
'LIGHTNING:' +
|
||||||
|
props.row.bolt11.toUpperCase()
|
||||||
|
"
|
||||||
:href="
|
:href="
|
||||||
'lightning:' + props.row.bolt11
|
'lightning:' + props.row.bolt11
|
||||||
"
|
"
|
||||||
>
|
></lnbits-qrcode>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|||||||
@@ -438,7 +438,7 @@
|
|||||||
<lnbits-qrcode
|
<lnbits-qrcode
|
||||||
v-else
|
v-else
|
||||||
:href="'lightning:' + receive.paymentReq"
|
:href="'lightning:' + receive.paymentReq"
|
||||||
:value="'lightning:' + receive.paymentReq"
|
:value="'LIGHTNING:' + receive.paymentReq.toUpperCase()"
|
||||||
>
|
>
|
||||||
</lnbits-qrcode>
|
</lnbits-qrcode>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ from lnbits.settings import settings
|
|||||||
def log_server_info():
|
def log_server_info():
|
||||||
logger.info("LNbits Info")
|
logger.info("LNbits Info")
|
||||||
if settings.first_install:
|
if settings.first_install:
|
||||||
logger.success("This is a fresh install of LNbits.")
|
if settings.has_first_install_token_changed():
|
||||||
|
logger.success("This is a first install token reset.")
|
||||||
|
else:
|
||||||
|
logger.success("This is a fresh install of LNbits.")
|
||||||
|
|
||||||
if settings.first_install_token:
|
if settings.first_install_token:
|
||||||
logger.success(
|
logger.success(
|
||||||
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
|
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ def decrypt_content(
|
|||||||
1:
|
1:
|
||||||
]
|
]
|
||||||
# extract iv and content
|
# 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"))
|
encrypted_content = base64.b64decode(encrypted_content_b64.encode("ascii"))
|
||||||
iv = base64.b64decode(iv_b64.encode("ascii"))
|
iv = base64.b64decode(iv_b64.encode("ascii"))
|
||||||
# Decrypt
|
# Decrypt
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from embit.bip39 import mnemonic_is_valid
|
||||||
from httpx import RequestError, TimeoutException
|
from httpx import RequestError, TimeoutException
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets import connect
|
from websockets import connect
|
||||||
@@ -61,6 +63,8 @@ class PhoenixdWallet(Wallet):
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
||||||
|
self._seed_mnemonic_to_persist: str | None = None
|
||||||
|
self._load_mnemonic_from_seed_file()
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
try:
|
try:
|
||||||
@@ -69,6 +73,7 @@ class PhoenixdWallet(Wallet):
|
|||||||
logger.warning(f"Error closing wallet connection: {e}")
|
logger.warning(f"Error closing wallet connection: {e}")
|
||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
|
await self._persist_loaded_mnemonic()
|
||||||
try:
|
try:
|
||||||
r = await self.client.get("/getinfo", timeout=10)
|
r = await self.client.get("/getinfo", timeout=10)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -101,7 +106,6 @@ class PhoenixdWallet(Wallet):
|
|||||||
unhashed_description: bytes | None = None,
|
unhashed_description: bytes | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msats_amount = amount
|
msats_amount = amount
|
||||||
data: dict[str, Any] = {
|
data: dict[str, Any] = {
|
||||||
@@ -309,7 +313,7 @@ class PhoenixdWallet(Wallet):
|
|||||||
and message_json.get("type") == "payment_received"
|
and message_json.get("type") == "payment_received"
|
||||||
):
|
):
|
||||||
logger.info(
|
logger.info(
|
||||||
f'payment-received: {message_json["paymentHash"]}'
|
f"payment-received: {message_json['paymentHash']}"
|
||||||
)
|
)
|
||||||
yield message_json["paymentHash"]
|
yield message_json["paymentHash"]
|
||||||
|
|
||||||
@@ -319,3 +323,49 @@ class PhoenixdWallet(Wallet):
|
|||||||
"retrying in 5 seconds"
|
"retrying in 5 seconds"
|
||||||
)
|
)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
def _load_mnemonic_from_seed_file(self):
|
||||||
|
data_dir = settings.phoenixd_data_dir
|
||||||
|
if not data_dir:
|
||||||
|
return
|
||||||
|
|
||||||
|
seed_path = Path(data_dir).expanduser() / "seed.dat"
|
||||||
|
if not seed_path.is_file():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
mnemonic = seed_path.read_text(encoding="utf-8").strip()
|
||||||
|
if mnemonic == settings.phoenixd_mnemonic:
|
||||||
|
return
|
||||||
|
except OSError as exc:
|
||||||
|
logger.warning(f"Failed to read Phoenixd seed file '{seed_path}': {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not mnemonic:
|
||||||
|
logger.warning(f"Phoenixd seed file '{seed_path}' is empty.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not mnemonic_is_valid(mnemonic):
|
||||||
|
logger.warning(
|
||||||
|
f"Phoenixd seed file '{seed_path}' does not contain a valid "
|
||||||
|
"BIP39 mnemonic."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
settings.phoenixd_mnemonic = mnemonic
|
||||||
|
self._seed_mnemonic_to_persist = mnemonic
|
||||||
|
|
||||||
|
async def _persist_loaded_mnemonic(self):
|
||||||
|
if not self._seed_mnemonic_to_persist:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Updating 'PHOENIXD_MNEMONIC' mnemonic settings.")
|
||||||
|
try:
|
||||||
|
from lnbits.core.crud.settings import set_settings_field
|
||||||
|
|
||||||
|
await set_settings_field(
|
||||||
|
"phoenixd_mnemonic", self._seed_mnemonic_to_persist
|
||||||
|
)
|
||||||
|
self._seed_mnemonic_to_persist = None
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Failed to persist Phoenixd mnemonic: {exc}")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "lnbits",
|
"name": "lnbits",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.15.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nostr-tools": "^2.18.2",
|
"nostr-tools": "^2.18.2",
|
||||||
@@ -734,14 +734,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.5",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.15.11",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
@@ -1498,9 +1498,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1555,9 +1555,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pyright": {
|
"node_modules/pyright": {
|
||||||
"version": "1.1.289",
|
"version": "1.1.289",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"terser": "^5.44.1"
|
"terser": "^5.44.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.15.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nostr-tools": "^2.18.2",
|
"nostr-tools": "^2.18.2",
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.5.2-rc3"
|
version = "1.5.4"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||||
urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/lnbits" }
|
urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/lnbits" }
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bech32==1.2.0",
|
"bech32~=1.2.0",
|
||||||
"click==8.3.1",
|
"click~=8.3.1",
|
||||||
"fastapi==0.116.1",
|
"fastapi~=0.116.1",
|
||||||
"starlette==0.47.1",
|
"starlette~=0.47.1",
|
||||||
"httpx==0.27.2",
|
"httpx~=0.27.2",
|
||||||
"jinja2==3.1.6",
|
"jinja2~=3.1.6",
|
||||||
"lnurl==0.8.3",
|
"lnurl~=0.10.0",
|
||||||
"pydantic==1.10.26",
|
"pydantic~=1.10.26",
|
||||||
"pyqrcode==1.2.1",
|
"pyqrcode~=1.2.1",
|
||||||
"shortuuid==1.0.13",
|
"shortuuid~=1.0.13",
|
||||||
"sse-starlette==2.3.6",
|
"sse-starlette~=2.3.6",
|
||||||
"typing-extensions==4.15.0",
|
"typing-extensions~=4.15.0",
|
||||||
"uvicorn==0.40.0",
|
"uvicorn~=0.40.0",
|
||||||
"sqlalchemy==1.4.54",
|
"sqlalchemy~=1.4.54",
|
||||||
"aiosqlite==0.22.1",
|
"aiosqlite~=0.22.1",
|
||||||
"asyncpg==0.31.0",
|
"asyncpg~=0.31.0",
|
||||||
"uvloop==0.22.1",
|
"uvloop~=0.22.1",
|
||||||
"websockets==15.0.1",
|
"websockets~=15.0.1",
|
||||||
"loguru==0.7.3",
|
"loguru~=0.7.3",
|
||||||
"grpcio==1.76.0",
|
"grpcio~=1.76.0",
|
||||||
"protobuf==6.33.2",
|
"protobuf~=6.33.5",
|
||||||
"pyln-client==25.12",
|
"pyln-client~=25.12.0",
|
||||||
"pywebpush==2.2.0",
|
"pywebpush~=2.2.0",
|
||||||
"slowapi==0.1.9",
|
"slowapi~=0.1.9",
|
||||||
"websocket-client==1.9.0",
|
"websocket-client~=1.9.0",
|
||||||
"pycryptodomex==3.23.0",
|
"pycryptodomex~=3.23.0",
|
||||||
"packaging==25.0",
|
"packaging~=25.0.0",
|
||||||
"bolt11==2.1.1",
|
"bolt11~=2.1.1",
|
||||||
"pyjwt==2.10.1",
|
"pyjwt~=2.12.0",
|
||||||
"itsdangerous==2.2.0",
|
"itsdangerous~=2.2.0",
|
||||||
"fastapi-sso==0.19.0",
|
"fastapi-sso~=0.19.0",
|
||||||
# needed for boltz, lnurldevice, watchonly extensions
|
# needed for boltz, lnurldevice, watchonly extensions
|
||||||
"embit==0.8.0",
|
"embit~=0.8.0",
|
||||||
# needed for scheduler extension
|
# needed for scheduler extension
|
||||||
"python-crontab==3.3.0",
|
"python-crontab~=3.3.0",
|
||||||
"pynostr==0.7.0",
|
"pynostr~=0.7.0",
|
||||||
"python-multipart==0.0.21",
|
"python-multipart~=0.0.22",
|
||||||
"filetype==1.2.0",
|
"filetype~=1.2.0",
|
||||||
"nostr-sdk==0.44.0",
|
"nostr-sdk~=0.44.0",
|
||||||
"bcrypt==5.0.0",
|
"bcrypt~=5.0.0",
|
||||||
"jsonpath-ng==1.7.0",
|
"jsonpath-ng~=1.7.0",
|
||||||
"pillow>=12.1.0",
|
"pillow~=12.1.0",
|
||||||
"python-dotenv>=1.2.1",
|
"python-dotenv~=1.2.1",
|
||||||
"greenlet (>=3.3.0,<4.0.0)",
|
"greenlet~=3.3.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -58,31 +58,31 @@ lnbits = "lnbits.server:main"
|
|||||||
lnbits-cli = "lnbits.commands:main"
|
lnbits-cli = "lnbits.commands:main"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
breez = ["breez-sdk==0.8.0", "breez-sdk-liquid==0.11.11"]
|
breez = ["breez-sdk~=0.8.0", "breez-sdk-liquid~=0.11.11"]
|
||||||
liquid = ["wallycore==1.5.1"]
|
liquid = ["wallycore~=1.5.1"]
|
||||||
migration = ["psycopg2-binary==2.9.11"]
|
migration = ["psycopg2-binary~=2.9.11"]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"black>=25.12.0,<26.0.0",
|
"black~=26.3.1",
|
||||||
"mypy==1.17.1",
|
"mypy~=1.17.1",
|
||||||
"types-protobuf>=6.32.1.20251210,<7.0.0",
|
"types-protobuf~=6.32.1.20251210",
|
||||||
"pre-commit>=4.5.1,<5.0.0",
|
"pre-commit~=4.5.1",
|
||||||
"openapi-spec-validator>=0.7.2,<1.0.0",
|
"openapi-spec-validator~=0.7.2",
|
||||||
"ruff>=0.14.10,<1.0.0",
|
"ruff~=0.14.10",
|
||||||
"types-passlib>=1.7.7.20250602,<2.0.0",
|
"types-passlib~=1.7.7.20250602",
|
||||||
"openai>=2.14.0",
|
"openai~=2.14.0",
|
||||||
"json5>=0.13.0,<1.0.0",
|
"json5~=0.13.0",
|
||||||
"asgi-lifespan>=2.1.0,<3.0.0",
|
"asgi-lifespan~=2.1.0",
|
||||||
"anyio>=4.12.1",
|
"anyio~=4.12.1",
|
||||||
"pytest>=9.0.2",
|
"pytest~=9.0.2",
|
||||||
"pytest-cov>=7.0.0",
|
"pytest-cov~=7.0.0",
|
||||||
"pytest-md>=0.2.0,<0.3.0",
|
"pytest-md~=0.2.0",
|
||||||
"pytest-httpserver>=1.1.3,<2.0.0",
|
"pytest-httpserver~=1.1.3",
|
||||||
"pytest-mock>=3.15.1,<4.0.0",
|
"pytest-mock~=3.15.1",
|
||||||
"types-mock>=5.2.0.20250924,<6.0.0",
|
"types-mock~=5.2.0.20250924",
|
||||||
"mock>=5.2.0,<6.0.0",
|
"mock~=5.2.0",
|
||||||
"grpcio-tools>=1.76.0,<2.0.0"
|
"grpcio-tools~=1.76.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
|
||||||
|
from lnbits.server import server_restart
|
||||||
from lnbits.settings import Settings
|
from lnbits.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
@@ -49,3 +52,109 @@ async def test_admin_update_noneditable_settings(
|
|||||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_admin_audit_monitor_and_test_email(
|
||||||
|
client: AsyncClient, superuser_token: str, mocker
|
||||||
|
):
|
||||||
|
mocker.patch(
|
||||||
|
"lnbits.core.views.admin_api.get_balance_delta",
|
||||||
|
mocker.AsyncMock(
|
||||||
|
return_value={"lnbits_balance_sats": 21, "node_balance_sats": 13}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
"lnbits.core.views.admin_api.send_email_notification",
|
||||||
|
mocker.AsyncMock(return_value={"status": "queued"}),
|
||||||
|
)
|
||||||
|
|
||||||
|
audit = await client.get(
|
||||||
|
"/admin/api/v1/audit",
|
||||||
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||||
|
)
|
||||||
|
assert audit.status_code == 200
|
||||||
|
assert audit.json()["lnbits_balance_sats"] == 21
|
||||||
|
|
||||||
|
monitor = await client.get(
|
||||||
|
"/admin/api/v1/monitor",
|
||||||
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||||
|
)
|
||||||
|
assert monitor.status_code == 200
|
||||||
|
assert "invoice_listeners" in monitor.json()
|
||||||
|
|
||||||
|
test_email = await client.get(
|
||||||
|
"/admin/api/v1/testemail",
|
||||||
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||||
|
)
|
||||||
|
assert test_email.status_code == 200
|
||||||
|
assert test_email.json()["status"] == "queued"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_admin_partial_reset_restart_and_backup(
|
||||||
|
client: AsyncClient,
|
||||||
|
superuser_token: str,
|
||||||
|
settings: Settings,
|
||||||
|
tmp_path,
|
||||||
|
):
|
||||||
|
response = await client.patch(
|
||||||
|
"/admin/api/v1/settings",
|
||||||
|
json={"lnbits_site_title": "PATCHED TITLE"},
|
||||||
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "Success"
|
||||||
|
|
||||||
|
default_value = await client.get(
|
||||||
|
"/admin/api/v1/settings/default",
|
||||||
|
params={"field_name": "lnbits_site_title"},
|
||||||
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||||
|
)
|
||||||
|
assert default_value.status_code == 200
|
||||||
|
assert "default_value" in default_value.json()
|
||||||
|
|
||||||
|
backup_path = Path("lnbits-backup.zip")
|
||||||
|
original_data_folder = settings.lnbits_data_folder
|
||||||
|
|
||||||
|
try:
|
||||||
|
data_folder = tmp_path / "backup_data"
|
||||||
|
data_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
(data_folder / "sample.txt").write_text("backup me")
|
||||||
|
settings.lnbits_data_folder = str(data_folder)
|
||||||
|
|
||||||
|
backup = await client.get(
|
||||||
|
"/admin/api/v1/backup",
|
||||||
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||||
|
)
|
||||||
|
assert backup.status_code == 200
|
||||||
|
assert backup.headers["content-type"] == "application/zip"
|
||||||
|
assert backup.content.startswith(b"PK")
|
||||||
|
assert backup_path.is_file()
|
||||||
|
finally:
|
||||||
|
settings.lnbits_data_folder = original_data_folder
|
||||||
|
backup_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
server_restart.clear()
|
||||||
|
restart = await client.get(
|
||||||
|
"/admin/api/v1/restart",
|
||||||
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||||
|
)
|
||||||
|
assert restart.status_code == 200
|
||||||
|
assert restart.json()["status"] == "Success"
|
||||||
|
assert server_restart.is_set() is True
|
||||||
|
server_restart.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_admin_delete_settings_requires_superuser(
|
||||||
|
client: AsyncClient, superuser_token: str
|
||||||
|
):
|
||||||
|
server_restart.clear()
|
||||||
|
response = await client.delete(
|
||||||
|
"/admin/api/v1/settings",
|
||||||
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert server_restart.is_set() is True
|
||||||
|
server_restart.clear()
|
||||||
|
|||||||