Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83334d67c8 | ||
|
|
ef9adc077b | ||
|
|
ffad5d79c4 | ||
|
|
09c35eaca0 | ||
|
|
52adc62bc4 | ||
|
|
0efa173084 | ||
|
|
724f8c9f63 | ||
|
|
c6981089e5 | ||
|
|
13480027dd | ||
|
|
93f1006d3a | ||
|
|
5490367179 | ||
|
|
a185197e34 | ||
|
|
aa1bddcdd5 | ||
|
|
6d24ba55aa | ||
|
|
5b2937672c | ||
|
|
957d9ce419 | ||
|
|
0e21b77b74 | ||
|
|
24e28911ab | ||
|
|
1e252d0485 | ||
|
|
e98550a125 | ||
|
|
bc55474859 | ||
|
|
d985bdaadb | ||
|
|
8e0d1cc100 | ||
|
|
cb3fd56647 | ||
|
|
9f383bfee6 | ||
|
|
3b2e28dd60 | ||
|
|
132192bc94 | ||
|
|
281c3df826 | ||
|
|
08591f34c2 | ||
|
|
f0e8ae0f5c | ||
|
|
168cb726b1 | ||
|
|
91ac245307 | ||
|
|
d098e2f710 | ||
|
|
a0f65f4cda | ||
|
|
606ee215b4 | ||
|
|
7716f819f6 | ||
|
|
8325d880cb | ||
|
|
b090470fc1 | ||
|
|
c7297d2e77 | ||
|
|
b6e111b21c | ||
|
|
7747d7b741 | ||
|
|
f3a5a8e002 | ||
|
|
157a6485b4 | ||
|
|
8867b27b09 | ||
|
|
68b607ecbc | ||
|
|
f411fd13dc | ||
|
|
baee90da67 | ||
|
|
661b713993 | ||
|
|
5a5f253fa5 | ||
|
|
77a5d7fe50 | ||
|
|
07dd4fc685 | ||
|
|
3761f7922c | ||
|
|
327b9d7f63 | ||
|
|
cacffc67ee | ||
|
|
b7fdf99a8d | ||
|
|
cd6cfff9cf | ||
|
|
7b635a31e5 | ||
|
|
b4c0cdbc7c | ||
|
|
fd765e2060 | ||
|
|
5f86627eae | ||
|
|
3af3838995 | ||
|
|
a762529fef | ||
|
|
245569d0b9 | ||
|
|
fcaeb0ac7a | ||
|
|
d2aedde21b | ||
|
|
5adc419c74 | ||
|
|
5d79327906 | ||
|
|
71e0b396d2 | ||
|
|
89d673448a | ||
|
|
aed3f3b569 | ||
|
|
d9a2a7bb95 | ||
|
|
15ede13104 | ||
|
|
62047ea758 | ||
|
|
e5e589fc40 | ||
|
|
46406792fe | ||
|
|
dbf71fed53 | ||
|
|
850087a8ec | ||
|
|
d9b045c526 | ||
|
|
e1ca6ef82a | ||
|
|
17c40d539e | ||
|
|
625fa6503c | ||
|
|
73634e5161 | ||
|
|
1fe35070f4 | ||
|
|
ad268516a9 | ||
|
|
ca94909aab | ||
|
|
b3efb4d378 | ||
|
|
5213508dc1 | ||
|
|
af9331eede | ||
|
|
9b8fe42102 | ||
|
|
a0e4a59454 | ||
|
|
eefaf3c50c | ||
|
|
89cabda123 | ||
|
|
6122a03f32 | ||
|
|
ad8b70a098 | ||
|
|
6449276003 | ||
|
|
4da651b74a | ||
|
|
b5f3a46feb | ||
|
|
1057b4693f | ||
|
|
5711b4b804 | ||
|
|
704e5a1b73 | ||
|
|
e6ca8a33c6 | ||
|
|
9c257aa23d | ||
|
|
ef752cbeca | ||
|
|
a509eb8fdb | ||
|
|
49cc8104fc | ||
|
|
f1f6af0e35 | ||
|
|
5f1cfc0e37 | ||
|
|
730ab59578 | ||
|
|
c8bdba8e53 | ||
|
|
c6c67c52db | ||
|
|
da31e3caaa | ||
|
|
d0bf47163b | ||
|
|
37ad515427 | ||
|
|
8c77f75cf1 | ||
|
|
21505471d5 | ||
|
|
baa9a35773 | ||
|
|
7f114ddcc0 | ||
|
|
92aad20dd7 | ||
|
|
0f4ae5da86 | ||
|
|
7a796c6510 | ||
|
|
0910687328 | ||
|
|
33e2fc2ea8 | ||
|
|
0c6e8394c8 | ||
|
|
d55e2a0e1f | ||
|
|
148ba9d275 | ||
|
|
d2ca774f6f | ||
|
|
233398b512 | ||
|
|
152c1dbb74 | ||
|
|
44985cb0d1 | ||
|
|
b1a7692ce4 | ||
|
|
31bff37b1e | ||
|
|
3f7a1798d5 | ||
|
|
98d4dbab8b | ||
|
|
7c7a04da9d | ||
|
|
3ccefb70fa | ||
|
|
5346ac47b1 | ||
|
|
9f4ed53888 | ||
|
|
faac56c14c | ||
|
|
3e0d0d9896 | ||
|
|
b9a004a5e4 | ||
|
|
da3cb548bd | ||
|
|
4144359617 | ||
|
|
07268b87d2 | ||
|
|
c356874bcb | ||
|
|
f9b5c7db7a | ||
|
|
20f9ddb57c | ||
|
|
ee57ba4c1a | ||
|
|
ff7f5c1ca5 | ||
|
|
1463d75ee2 | ||
|
|
f1fc4710ee | ||
|
|
00eaec8290 | ||
|
|
28c4235c94 | ||
|
|
b7d178c08e | ||
|
|
9d0ec97d39 | ||
|
|
b6f533be24 | ||
|
|
5e36bb4fcd | ||
|
|
f9b0604711 | ||
|
|
39c33699af | ||
|
|
c89721223f | ||
|
|
1e6e97c12d | ||
|
|
783efb19db | ||
|
|
9ffc63b5dc | ||
|
|
0ec8139e5c | ||
|
|
a8c3181852 | ||
|
|
e038ceb9be | ||
|
|
b4eccb9e5d | ||
|
|
691b7ad055 | ||
|
|
c84d1b66c6 | ||
|
|
cf7726ddfd | ||
|
|
bd50a3c546 | ||
|
|
c9270bbf7f | ||
|
|
6474aeb982 | ||
|
|
37ba437ad1 | ||
|
|
35af181e24 | ||
|
|
f8c58aef0e | ||
|
|
8755984bd8 | ||
|
|
0d5661cda7 | ||
|
|
c2a3fbc6c0 | ||
|
|
6bac34fc6b | ||
|
|
b54eedee84 | ||
|
|
bd07f7a5ef | ||
|
|
a40306f5cd | ||
|
|
2fecec2623 | ||
|
|
4ad89396cc | ||
|
|
82307c4839 | ||
|
|
8506199c2f | ||
|
|
d142d76148 | ||
|
|
98e9a61b98 | ||
|
|
877af0c21e | ||
|
|
dd27b190f3 | ||
|
|
2856803ca7 | ||
|
|
ca8264b1f5 | ||
|
|
39ca9da870 | ||
|
|
2b603bdc48 | ||
|
|
26780df065 | ||
|
|
248fcc06ab | ||
|
|
f794373d30 | ||
|
|
87f25ea715 | ||
|
|
b9de754598 | ||
|
|
785fb7af8e | ||
|
|
55c5ab3a6d |
@@ -23,3 +23,10 @@ mypy.ini
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
package.json
|
package.json
|
||||||
pytest.ini
|
pytest.ini
|
||||||
|
|
||||||
|
.mypy_cache
|
||||||
|
.github
|
||||||
|
.pytest_cache
|
||||||
|
.vscode
|
||||||
|
bin
|
||||||
|
dist
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ ENABLE_LOG_TO_FILE=true
|
|||||||
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
|
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
|
||||||
LOG_ROTATION="100 MB"
|
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
|
||||||
|
|
||||||
@@ -187,7 +186,6 @@ BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
|
|||||||
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
||||||
# HEXSTRING instead of path also possible
|
# HEXSTRING instead of path also possible
|
||||||
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
||||||
BOLTZ_CLIENT_WALLET="lnbits"
|
|
||||||
|
|
||||||
# StrikeWallet
|
# StrikeWallet
|
||||||
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
||||||
@@ -333,4 +331,3 @@ LNBITS_RESERVE_FEE_PERCENT=1.0
|
|||||||
######################################
|
######################################
|
||||||
###### Logging and Development #######
|
###### Logging and Development #######
|
||||||
######################################
|
######################################
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,26 @@ name: Build LNbits AppImage
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: 'The tag name for the release'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
upload_url:
|
||||||
|
description: 'The upload URL for the release'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: 'The tag name for the release'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
upload_url:
|
||||||
|
description: 'The upload URL for the release'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-linux-package:
|
build-linux-package:
|
||||||
@@ -75,7 +94,7 @@ jobs:
|
|||||||
# Build AppImage
|
# Build AppImage
|
||||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||||
chmod +x appimagetool-x86_64.AppImage
|
chmod +x appimagetool-x86_64.AppImage
|
||||||
TAG_NAME=${{ github.event.release.tag_name }}
|
TAG_NAME=${{ inputs.tag_name }}
|
||||||
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
|
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
|
||||||
./appimagetool-x86_64.AppImage \
|
./appimagetool-x86_64.AppImage \
|
||||||
--updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" \
|
--updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" \
|
||||||
@@ -93,7 +112,7 @@ jobs:
|
|||||||
- name: Upload Linux Release Asset
|
- name: Upload Linux Release Asset
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ github.event.release.upload_url }}
|
upload_url: ${{ inputs.upload_url }}
|
||||||
asset_path: ${{ env.APPIMAGE_NAME }}
|
asset_path: ${{ env.APPIMAGE_NAME }}
|
||||||
asset_name: ${{ env.APPIMAGE_NAME }}
|
asset_name: ${{ env.APPIMAGE_NAME }}
|
||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
|||||||
@@ -75,7 +75,14 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10"]
|
python-version: ["3.10"]
|
||||||
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
|
backend-wallet-class:
|
||||||
|
- BoltzWallet
|
||||||
|
- LndRestWallet
|
||||||
|
- LndWallet
|
||||||
|
- CoreLightningWallet
|
||||||
|
- CoreLightningRestWallet
|
||||||
|
- LNbitsWallet
|
||||||
|
- EclairWallet
|
||||||
with:
|
with:
|
||||||
custom-pytest: "uv run pytest tests/regtest"
|
custom-pytest: "uv run pytest tests/regtest"
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|||||||
@@ -62,3 +62,4 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
build-args: LNBITS_TAG=${{ inputs.tag }}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||||
cd docker
|
cd docker
|
||||||
chmod +x ./tests
|
chmod +x ./start-regtest
|
||||||
./tests
|
./start-regtest
|
||||||
sudo chmod -R a+rwx .
|
sudo chmod -R a+rwx .
|
||||||
|
|
||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
@@ -63,6 +63,8 @@ jobs:
|
|||||||
LNBITS_ENDPOINT: http://localhost:5001
|
LNBITS_ENDPOINT: http://localhost:5001
|
||||||
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
|
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
|
||||||
ECLAIR_URL: http://127.0.0.1:8082
|
ECLAIR_URL: http://127.0.0.1:8082
|
||||||
|
BOLTZ_CLIENT_ENDPOINT: 127.0.0.1:9002
|
||||||
|
BOLTZ_MNEMONIC: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
|
||||||
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
|
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
|
||||||
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
|
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
|
||||||
ECLAIR_PASS: lnbits
|
ECLAIR_PASS: lnbits
|
||||||
|
|||||||
@@ -10,7 +10,29 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
upload_url: ${{ steps.get_upload_url.outputs.upload_url }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Create github pre-release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
gh release create "$tag" --prerelease --generate-notes --draft
|
||||||
|
- id: get_upload_url
|
||||||
|
name: Get upload url of Github release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
upload_url=$(gh release view "$tag" --json uploadUrl -q ".uploadUrl")
|
||||||
|
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
|
if: github.repository == 'lnbits/lnbits'
|
||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.ref_name }}
|
tag: ${{ github.ref_name }}
|
||||||
@@ -19,6 +41,7 @@ jobs:
|
|||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
pypi:
|
pypi:
|
||||||
|
if: github.repository == 'lnbits/lnbits'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies for building secp256k1
|
- name: Install dependencies for building secp256k1
|
||||||
@@ -30,3 +53,10 @@ jobs:
|
|||||||
uses: JRubics/poetry-publish@v1.15
|
uses: JRubics/poetry-publish@v1.15
|
||||||
with:
|
with:
|
||||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||||
|
|
||||||
|
appimage:
|
||||||
|
needs: [ release ]
|
||||||
|
uses: ./.github/workflows/appimage.yml
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
upload_url: ${{ steps.get_upload_url.outputs.upload_url }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Create github release
|
- name: Create github release
|
||||||
@@ -21,8 +23,17 @@ jobs:
|
|||||||
tag: ${{ github.ref_name }}
|
tag: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
gh release create "$tag" --generate-notes --draft
|
gh release create "$tag" --generate-notes --draft
|
||||||
|
- id: get_upload_url
|
||||||
|
name: Get upload url of Github release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
upload_url=$(gh release view "$tag" --json uploadUrl -q ".uploadUrl")
|
||||||
|
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
|
if: github.repository == 'lnbits/lnbits'
|
||||||
needs: [ release ]
|
needs: [ release ]
|
||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
@@ -32,6 +43,7 @@ jobs:
|
|||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
docker-latest:
|
docker-latest:
|
||||||
|
if: github.repository == 'lnbits/lnbits'
|
||||||
needs: [ release ]
|
needs: [ release ]
|
||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
@@ -41,6 +53,7 @@ jobs:
|
|||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
pypi:
|
pypi:
|
||||||
|
if: github.repository == 'lnbits/lnbits'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies for building secp256k1
|
- name: Install dependencies for building secp256k1
|
||||||
@@ -56,3 +69,6 @@ jobs:
|
|||||||
appimage:
|
appimage:
|
||||||
needs: [ release ]
|
needs: [ release ]
|
||||||
uses: ./.github/workflows/appimage.yml
|
uses: ./.github/workflows/appimage.yml
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ repos:
|
|||||||
- id: ruff
|
- id: ruff
|
||||||
args: [ --fix, --exit-non-zero-on-fix ]
|
args: [ --fix, --exit-non-zero-on-fix ]
|
||||||
- repo: https://github.com/rbubley/mirrors-prettier
|
- repo: https://github.com/rbubley/mirrors-prettier
|
||||||
rev: v3.6.2
|
rev: v3.7.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
types_or: [css, javascript, html, json]
|
types_or: [css, javascript, html, json]
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
FROM boltz/boltz-client:latest AS boltz
|
ARG LNBITS_TAG=latest
|
||||||
|
|
||||||
FROM lnbits/lnbits:latest
|
FROM boltz/boltz-client:latest AS boltz
|
||||||
|
FROM lnbits/lnbits:${LNBITS_TAG}
|
||||||
|
|
||||||
COPY --from=boltz /bin/boltzd /bin/boltzcli /usr/local/bin/
|
COPY --from=boltz /bin/boltzd /bin/boltzcli /usr/local/bin/
|
||||||
RUN ls -l /usr/local/bin/boltzd
|
RUN ls -l /usr/local/bin/boltzd
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
</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 width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
||||||
|
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||||
|
|
||||||
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
||||||
|
|
||||||
@@ -77,13 +78,11 @@ As well as working great in a browser, LNbits has native IoS and Android apps as
|
|||||||
|
|
||||||
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
|
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
|
||||||
|
|
||||||
If you like this project [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
|
||||||
|
|
||||||
[](https://shop.lnbits.com/)
|
[](https://shop.lnbits.com/)
|
||||||
[](https://shop.lnbits.com/)
|
[](https://shop.lnbits.com/)
|
||||||
[](https://my.lnbits.com/login)
|
[](https://my.lnbits.com/login)
|
||||||
[](https://news.lnbits.com/)
|
[](https://news.lnbits.com/)
|
||||||
[](https://extensions.lnbits.com/)
|
[](https://extensions.lnbits.com/) [](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||||
|
|
||||||
[docs]: https://github.com/lnbits/lnbits/wiki
|
[docs]: https://github.com/lnbits/lnbits/wiki
|
||||||
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ color_scheme: dark
|
|||||||
logo: "/logos/lnbits-full-inverse.png"
|
logo: "/logos/lnbits-full-inverse.png"
|
||||||
search_enabled: true
|
search_enabled: true
|
||||||
url: https://docs.lnbits.org
|
url: https://docs.lnbits.org
|
||||||
|
markdown: gfm
|
||||||
aux_links:
|
aux_links:
|
||||||
"LNbits on GitHub":
|
"LNbits on GitHub":
|
||||||
- "//github.com/lnbits/lnbits"
|
- "//github.com/lnbits/lnbits"
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: FastAPI extension upgrade
|
||||||
|
nav_order: 1
|
||||||
|
---
|
||||||
|
|
||||||
## Defining a route with path parameters
|
## Defining a route with path parameters
|
||||||
|
|
||||||
**old:**
|
**old:**
|
||||||
+34
-22
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Admin UI
|
||||||
|
nav_order: 1
|
||||||
|
---
|
||||||
|
|
||||||
<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="https://i.imgur.com/QE6SIrs.png">
|
||||||
@@ -12,11 +18,13 @@
|
|||||||
|
|
||||||
# LNBits Admin UI
|
# LNBits Admin UI
|
||||||
|
|
||||||
We introduced the Admin UI as the new default to make setup simpler and more straightforward. Instead of hand editing the `.env` file, you configure key server settings directly in the frontend with clear labels and guardrails.
|
[What you can do](#what-you-can-do-with-the-admin-ui) · [First Run](#first-run-and-super-user-id) · [Enable/Disable](#enabling-or-disabling-the-admin-ui) · [Reset](#reset-to-defaults) · [Allowed Users](#allowed-users) · [Guides](#additional-guides)
|
||||||
|
|
||||||
|
**We introduced the Admin UI as the new default to make setup simpler and more straightforward**. Instead of hand editing the `.env` file, you configure key server settings directly in the frontend with clear labels and guardrails.
|
||||||
|
|
||||||
<ins>On a fresh install the Admin UI is enabled by default</ins>, and at first launch you are prompted to create **Super User** credentials so that sensitive operations, such as switching funding sources, remain in trusted hands. When the Admin UI is enabled, configuration is written to and read from the database; for all settings managed by the UI, the parameters in `.env` are largely no longer used. If you disable the Admin UI, the `.env` file becomes the single source of truth again.
|
<ins>On a fresh install the Admin UI is enabled by default</ins>, and at first launch you are prompted to create **Super User** credentials so that sensitive operations, such as switching funding sources, remain in trusted hands. When the Admin UI is enabled, configuration is written to and read from the database; for all settings managed by the UI, the parameters in `.env` are largely no longer used. If you disable the Admin UI, the `.env` file becomes the single source of truth again.
|
||||||
|
|
||||||
For privileged actions and role details see **[Super User](./super_user.md)** & [User Roles](./user_roles.md)
|
For privileged actions and role details see **[Super User](./super_user.md)** & [User Roles](./user_roles.md)
|
||||||
For a complete reference of legacy variables consult **[.env.example](../../.env.example)**.
|
For a complete reference of legacy variables consult **[.env.example](../../.env.example)**.
|
||||||
|
|
||||||
<img width="900" height="640" alt="grafik" src="https://github.com/user-attachments/assets/d8852b4b-21be-446f-a1e7-d3eb794d3505" />
|
<img width="900" height="640" alt="grafik" src="https://github.com/user-attachments/assets/d8852b4b-21be-446f-a1e7-d3eb794d3505" />
|
||||||
@@ -36,6 +44,27 @@ For a complete reference of legacy variables consult **[.env.example](../../.env
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> See **[Super User](./super_user.md)** for the role and permission differences compared to Admin Users.
|
> See **[Super User](./super_user.md)** for the role and permission differences compared to Admin Users.
|
||||||
|
|
||||||
|
## First run and Super User ID
|
||||||
|
|
||||||
|
On first start with the Admin UI enabled you will be prompted to generate a Super User.
|
||||||
|
|
||||||
|
<img width="1573" height="976" alt="Admin_UI_first_install" src="https://github.com/user-attachments/assets/05aa634f-06ec-4a4d-a5c6-d90927c90991" />
|
||||||
|
|
||||||
|
If you need to read it from disk later:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat /lnbits/data/.super_user
|
||||||
|
# example
|
||||||
|
123de4bfdddddbbeb48c8bc8382fe123
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> For security reasons, Super Users and Admin users must authenticate with credentials (username and password).
|
||||||
|
|
||||||
|
After login you will see **Settings** and **Users** in the sidebar between **Wallets** and **Extensions**, plus a role badge in the top left.
|
||||||
|
|
||||||
|
<img width="1353" height="914" alt="grafik" src="https://github.com/user-attachments/assets/06bb4f36-a23a-4058-87ec-60440d322c25" />
|
||||||
|
|
||||||
## Enabling or disabling the Admin UI
|
## Enabling or disabling the Admin UI
|
||||||
|
|
||||||
The Admin UI is enabled by default on new installs. To change the state:
|
The Admin UI is enabled by default on new installs. To change the state:
|
||||||
@@ -76,23 +105,6 @@ The Admin UI is enabled by default on new installs. To change the state:
|
|||||||
|
|
||||||
Using `Reset to defaults` in the Admin UI wipes stored settings. After a restart, a new `Super User` is created and the old one is no longer valid.
|
Using `Reset to defaults` in the Admin UI wipes stored settings. After a restart, a new `Super User` is created and the old one is no longer valid.
|
||||||
|
|
||||||
## First run and Super User ID
|
|
||||||
|
|
||||||
On first start with the Admin UI enabled you will be prompted to generate a Super User. If you need to read it from disk later:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat /lnbits/data/.super_user
|
|
||||||
# example
|
|
||||||
123de4bfdddddbbeb48c8bc8382fe123
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> For security reasons, Super Users and Admin users must authenticate with credentials (username and password).
|
|
||||||
|
|
||||||
After login you will see **Settings** and **Users** in the sidebar between **Wallets** and **Extensions**, plus a role badge in the top left.
|
|
||||||
|
|
||||||
<img width="1353" height="914" alt="grafik" src="https://github.com/user-attachments/assets/06bb4f36-a23a-4058-87ec-60440d322c25" />
|
|
||||||
|
|
||||||
## Allowed Users
|
## Allowed Users
|
||||||
|
|
||||||
When set **at least one**, LNbits becomes private: only the listed users and Admins can access the frontend. Account creation is disabled automatically. You can also disable account creation explicitly.
|
When set **at least one**, LNbits becomes private: only the listed users and Admins can access the frontend. Account creation is disabled automatically. You can also disable account creation explicitly.
|
||||||
@@ -105,8 +117,8 @@ When set **at least one**, LNbits becomes private: only the listed users and Adm
|
|||||||
## Additional Guides
|
## Additional Guides
|
||||||
|
|
||||||
- **[Backend Wallets](./wallets.md)** — Explore options to fund your LNbits instance.
|
- **[Backend Wallets](./wallets.md)** — Explore options to fund your LNbits instance.
|
||||||
- **[User Roles](./User_Roles.md)** — Overview of existing roles in LNbits.
|
- **[User Roles](./user_roles.md)** — Overview of existing roles in LNbits.
|
||||||
- **[Funding sources](./funding-sources_table.md)** — What is available and how to configure each.
|
- **[Funding sources](./funding-sources-table.md)** — What is available and how to configure each.
|
||||||
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
|
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
|
||||||
|
|
||||||
## Powered by LNbits
|
## Powered by LNbits
|
||||||
@@ -115,7 +127,7 @@ LNbits empowers everyone with modular, open source tools for building Bitcoin ba
|
|||||||
|
|
||||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||||
|
|
||||||
[](https://shop.lnbits.com/)
|
[](https://shop.lnbits.com/)
|
||||||
[](https://shop.lnbits.com/)
|
[](https://shop.lnbits.com/)
|
||||||
[](https://my.lnbits.com/login)
|
[](https://my.lnbits.com/login)
|
||||||
[](https://news.lnbits.com/)
|
[](https://news.lnbits.com/)
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Extension Install
|
||||||
|
nav_order: 1
|
||||||
|
---
|
||||||
|
|
||||||
# Extension Install
|
# Extension Install
|
||||||
|
|
||||||
Anyone can create an extension by following the [example extension](https://github.com/lnbits/example) and [making extensions](https://github.com/lnbits/lnbits/blob/main/docs/devs/extensions.md) dev guide.
|
Anyone can create an extension by following the [example extension](https://github.com/lnbits/example) and [making extensions](https://github.com/lnbits/lnbits/blob/main/docs/devs/extensions.md) dev guide.
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Wallet comparison
|
||||||
|
nav_order: 1
|
||||||
|
---
|
||||||
|
|
||||||
<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="https://i.imgur.com/QE6SIrs.png">
|
||||||
@@ -72,7 +78,7 @@ LNbits empowers everyone with modular, open-source tools for building Bitcoin-ba
|
|||||||
|
|
||||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||||
|
|
||||||
[](https://shop.lnbits.com/)
|
[](https://shop.lnbits.com/)
|
||||||
[](https://shop.lnbits.com/)
|
[](https://shop.lnbits.com/)
|
||||||
[](https://my.lnbits.com/login)
|
[](https://my.lnbits.com/login)
|
||||||
[](https://news.lnbits.com/)
|
[](https://news.lnbits.com/)
|
||||||
|
|||||||
+33
-27
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Installation
|
||||||
|
nav_order: 1
|
||||||
|
---
|
||||||
|
|
||||||
<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="https://i.imgur.com/QE6SIrs.png">
|
||||||
@@ -51,8 +57,8 @@ LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most syst
|
|||||||
- LNbits will create a folder for DB and extension files **in the same directory** as the AppImage.
|
- LNbits will create a folder for DB and extension files **in the same directory** as the AppImage.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
## Option 2: UV (recommended for developers)
|
## Option 2: UV (recommended for developers)
|
||||||
@@ -117,8 +123,8 @@ uv run lnbits
|
|||||||
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
## Option 2a (Legacy): Poetry — _Replaced by UV_
|
## Option 2a (Legacy): Poetry — _Replaced by UV_
|
||||||
@@ -197,8 +203,8 @@ poetry run lnbits
|
|||||||
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -217,8 +223,8 @@ chmod +x lnbits.sh &&
|
|||||||
- You can use `./lnbits.sh` to run, but for more control: `cd lnbits` and use `uv run lnbits` (see Option 2).
|
- You can use `./lnbits.sh` to run, but for more control: `cd lnbits` and use `uv run lnbits` (see Option 2).
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -286,7 +292,7 @@ SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/ln
|
|||||||
```
|
```
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -340,8 +346,8 @@ docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .
|
|||||||
```
|
```
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -380,14 +386,14 @@ You'll be prompted to enter an app name, region, postgres (choose no), deploy no
|
|||||||
You'll now find a file in the directory called `fly.toml`. Open that file and modify/add the following settings.
|
You'll now find a file in the directory called `fly.toml`. Open that file and modify/add the following settings.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> Be sure to replace `${PUT_YOUR_LNBITS_ENV_VARS_HERE}` with all relevant environment variables in `.env` or `.env.example`.
|
> Be sure to replace `${PUT_YOUR_LNBITS_ENV_VARS_HERE}` with all relevant environment variables in `.env` or `.env.example`.
|
||||||
> Environment variable strings should be quoted here. For example, if `.env` has
|
> Environment variable strings should be quoted here. For example, if `.env` has
|
||||||
> `LNBITS_ENDPOINT=https://demo.lnbits.com`, then in `fly.toml` use
|
> `LNBITS_ENDPOINT=https://demo.lnbits.com`, then in `fly.toml` use
|
||||||
> `LNBITS_ENDPOINT="https://demo.lnbits.com"`.
|
> `LNBITS_ENDPOINT="https://demo.lnbits.com"`.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> Don't enter secret environment variables here. Fly.io offers **secrets** (via `fly secrets`) that are exposed as env vars at runtime.
|
> Don't enter secret environment variables here. Fly.io offers **secrets** (via `fly secrets`) that are exposed as env vars at runtime.
|
||||||
> Example (LND REST funding source):
|
> Example (LND REST funding source):
|
||||||
> `fly secrets set LND_REST_MACAROON=<hex_macaroon_data>`
|
> `fly secrets set LND_REST_MACAROON=<hex_macaroon_data>`
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -445,8 +451,8 @@ poetry add setuptools wheel
|
|||||||
|
|
||||||
> 
|
> 
|
||||||
>
|
>
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNbits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNbits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
@@ -524,22 +530,22 @@ Visit **[http://localhost:5000/](http://localhost:5000/)** (or `0.0.0.0:5000`).
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Paths overview**
|
> **Paths overview**
|
||||||
>
|
>
|
||||||
> - **SuperUser file:** `<lnbits_root>/data/.super_user`
|
> - **SuperUser file:** `<lnbits_root>/data/.super_user`
|
||||||
> Example: `~/lnbits/data/.super_user` • View: `cat ~/lnbits/data/.super_user`
|
> Example: `~/lnbits/data/.super_user` • View: `cat ~/lnbits/data/.super_user`
|
||||||
> - **Environment file:** `<lnbits_root>/.env` (for bare-metal installs)
|
> - **Environment file:** `<lnbits_root>/.env` (for bare-metal installs)
|
||||||
> - **Docker:** bind a host directory to `/app/data`.
|
> - **Docker:** bind a host directory to `/app/data`.
|
||||||
> On the host the SuperUser file is at `<host_data_dir>/.super_user`.
|
> On the host the SuperUser file is at `<host_data_dir>/.super_user`.
|
||||||
> The container reads `/app/.env` (usually bind-mounted from your project root).
|
> The container reads `/app/.env` (usually bind-mounted from your project root).
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> **Local Lightning test network**
|
> **Local Lightning test network**
|
||||||
> Use **Polar** to spin up a safe local Lightning environment and test LNbits without touching your live setup.
|
> Use **Polar** to spin up a safe local Lightning environment and test LNbits without touching your live setup.
|
||||||
> https://lightningpolar.com/
|
> https://lightningpolar.com/
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> **API comparison before updates**
|
> **API comparison before updates**
|
||||||
> Use **TableTown** to diff your LNbits instance against another (dev vs prod) or the upstream dev branch. Spot endpoint changes before updating.
|
> Use **TableTown** to diff your LNbits instance against another (dev vs prod) or the upstream dev branch. Spot endpoint changes before updating.
|
||||||
> Crafted by [Arbadacarbayk](https://github.com/arbadacarbaYK) - a standout contribution that makes pre-release reviews fast and reliable.
|
> Crafted by [Arbadacarbayk](https://github.com/arbadacarbaYK) - a standout contribution that makes pre-release reviews fast and reliable.
|
||||||
> https://arbadacarbayk.github.io/LNbits_TableTown/
|
> https://arbadacarbayk.github.io/LNbits_TableTown/
|
||||||
|
|
||||||
# Additional guides
|
# Additional guides
|
||||||
@@ -884,5 +890,5 @@ LNbits empowers everyone with modular, open-source tools for building Bitcoin-ba
|
|||||||
|
|
||||||
If you like this project [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visiting our [Shop](https://shop.lnbits.com)
|
If you like this project [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visiting our [Shop](https://shop.lnbits.com)
|
||||||
|
|
||||||
[](https://shop.lnbits.com/)
|
[](https://shop.lnbits.com/)
|
||||||
[](https://shop.lnbits.com/) [](https://my.lnbits.com/login) [](https://news.lnbits.com/) [](https://extensions.lnbits.com/)
|
[](https://shop.lnbits.com/) [](https://my.lnbits.com/login) [](https://news.lnbits.com/) [](https://extensions.lnbits.com/)
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Super user
|
||||||
|
nav_order: 1
|
||||||
|
---
|
||||||
|
|
||||||
<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="https://i.imgur.com/QE6SIrs.png">
|
||||||
@@ -110,8 +116,8 @@ These are practical tips for running a safe and friendly instance.
|
|||||||
## Additional guides
|
## Additional guides
|
||||||
|
|
||||||
- **[Admin UI](./admin_ui.md)** — Manage server settings in the browser instead of editing `.env` or using the CLI for routine tasks.
|
- **[Admin UI](./admin_ui.md)** — Manage server settings in the browser instead of editing `.env` or using the CLI for routine tasks.
|
||||||
- **[User Roles](./User_Roles.md)** — Overview of roles and what they can do.
|
- **[User Roles](./user_roles.md)** — Overview of roles and what they can do.
|
||||||
- **[Funding sources](./funding-sources_table.md)** — Available options and how to enable and configure them.
|
- **[Funding sources](./funding-sources-table.md)** — Available options and how to enable and configure them.
|
||||||
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
|
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
|
||||||
|
|
||||||
## Powered by LNbits
|
## Powered by LNbits
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: User Roles
|
||||||
|
nav_order: 1
|
||||||
|
---
|
||||||
|
|
||||||
<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="https://i.imgur.com/QE6SIrs.png">
|
||||||
@@ -14,7 +20,7 @@
|
|||||||
|
|
||||||
### Understand **who can do what** in seconds: `Super User`, `Admin`, and `Regular User`.
|
### Understand **who can do what** in seconds: `Super User`, `Admin`, and `Regular User`.
|
||||||
|
|
||||||
**Jump to:**
|
**Jump to:**
|
||||||
[Roles at a Glance](#roles-at-a-glance) •
|
[Roles at a Glance](#roles-at-a-glance) •
|
||||||
[Super User](#super-user--master-control) •
|
[Super User](#super-user--master-control) •
|
||||||
[Admin](#admin--day-to-day-manager) •
|
[Admin](#admin--day-to-day-manager) •
|
||||||
@@ -81,7 +87,7 @@ For using LNbits, not administering it.
|
|||||||
|
|
||||||
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
||||||
- **[Super User](./super_user.md)** — Deep dive on responsibilities and safe usage patterns.
|
- **[Super User](./super_user.md)** — Deep dive on responsibilities and safe usage patterns.
|
||||||
- **[Funding sources](./funding-sources_table.md)** — What’s available and how to enable/configure each.
|
- **[Funding sources](./funding-sources-table.md)** — What’s available and how to enable/configure each.
|
||||||
|
|
||||||
## Powered by LNbits
|
## Powered by LNbits
|
||||||
|
|
||||||
@@ -89,7 +95,7 @@ LNbits empowers everyone with modular, open-source tools for building Bitcoin-ba
|
|||||||
|
|
||||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||||
|
|
||||||
[](https://shop.lnbits.com/)
|
[](https://shop.lnbits.com/)
|
||||||
[](https://shop.lnbits.com/)
|
[](https://shop.lnbits.com/)
|
||||||
[](https://my.lnbits.com/login)
|
[](https://my.lnbits.com/login)
|
||||||
[](https://news.lnbits.com/)
|
[](https://news.lnbits.com/)
|
||||||
|
|||||||
@@ -354,8 +354,8 @@ Get more info here [https://docs.strike.me/strike-oauth-connect/](https://docs.s
|
|||||||
## Additional Guides
|
## Additional Guides
|
||||||
|
|
||||||
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
||||||
- **[User Roles](./User_Roles.md)** — Quick Overview of existing Roles in LNBits.
|
- **[User Roles](./user_roles.md)** — Quick Overview of existing Roles in LNBits.
|
||||||
- **[Funding sources](./funding-sources_table.md)** — What’s available and how to enable/configure each.
|
- **[Funding sources](./funding-sources-table.md)** — What’s available and how to enable/configure each.
|
||||||
|
|
||||||
## Powered by LNbits
|
## Powered by LNbits
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,7 @@
|
|||||||
nixpkgs.overlays = [ self.overlays.${system}.default ];
|
nixpkgs.overlays = [ self.overlays.${system}.default ];
|
||||||
};
|
};
|
||||||
|
|
||||||
checks = { };
|
checks =
|
||||||
|
import ./nix/tests { inherit pkgs; flake = self; };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from .core.services import create_invoice, pay_invoice
|
from .core.services import create_invoice, pay_invoice
|
||||||
from .decorators import (
|
from .decorators import (
|
||||||
|
check_account_exists,
|
||||||
check_admin,
|
check_admin,
|
||||||
check_super_user,
|
check_super_user,
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
@@ -11,6 +12,7 @@ from .exceptions import InvoiceError, PaymentError
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"InvoiceError",
|
"InvoiceError",
|
||||||
"PaymentError",
|
"PaymentError",
|
||||||
|
"check_account_exists",
|
||||||
"check_admin",
|
"check_admin",
|
||||||
"check_super_user",
|
"check_super_user",
|
||||||
"check_user_exists",
|
"check_user_exists",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from fastapi import APIRouter, FastAPI
|
|||||||
from .db import core_app_extra, db
|
from .db import core_app_extra, db
|
||||||
from .views.admin_api import admin_router
|
from .views.admin_api import admin_router
|
||||||
from .views.api import api_router
|
from .views.api import api_router
|
||||||
|
from .views.asset_api import asset_router
|
||||||
from .views.audit_api import audit_router
|
from .views.audit_api import audit_router
|
||||||
from .views.auth_api import auth_router
|
from .views.auth_api import auth_router
|
||||||
from .views.callback_api import callback_router
|
from .views.callback_api import callback_router
|
||||||
@@ -44,6 +45,7 @@ def init_core_routers(app: FastAPI):
|
|||||||
app.include_router(webpush_router)
|
app.include_router(webpush_router)
|
||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
app.include_router(audit_router)
|
app.include_router(audit_router)
|
||||||
|
app.include_router(asset_router)
|
||||||
app.include_router(fiat_router)
|
app.include_router(fiat_router)
|
||||||
app.include_router(lnurl_router)
|
app.include_router(lnurl_router)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
from lnbits.core.db import db
|
||||||
|
from lnbits.core.models.assets import Asset, AssetFilters, AssetInfo
|
||||||
|
from lnbits.db import Connection, Filters, Page
|
||||||
|
|
||||||
|
|
||||||
|
async def create_asset(
|
||||||
|
entry: Asset,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> None:
|
||||||
|
await (conn or db).insert("assets", entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_asset_info(
|
||||||
|
user_id: str,
|
||||||
|
asset_id: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> AssetInfo | None:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
query="SELECT * from assets WHERE id = :asset_id AND user_id = :user_id",
|
||||||
|
values={"asset_id": asset_id, "user_id": user_id},
|
||||||
|
model=AssetInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_asset_info(
|
||||||
|
asset_id: str, conn: Connection | None = None
|
||||||
|
) -> AssetInfo | None:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
query="SELECT * from assets WHERE id = :asset_id",
|
||||||
|
values={"asset_id": asset_id},
|
||||||
|
model=AssetInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_asset(
|
||||||
|
user_id: str,
|
||||||
|
asset_id: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Asset | None:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
query="SELECT * from assets WHERE id = :asset_id AND user_id = :user_id",
|
||||||
|
values={"asset_id": asset_id, "user_id": user_id},
|
||||||
|
model=Asset,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_public_asset(
|
||||||
|
asset_id: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Asset | None:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
query="SELECT * from assets WHERE id = :asset_id AND is_public = true",
|
||||||
|
values={"asset_id": asset_id},
|
||||||
|
model=Asset,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_public_asset_info(
|
||||||
|
asset_id: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> AssetInfo | None:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
query="SELECT * from assets WHERE id = :asset_id AND is_public = true",
|
||||||
|
values={"asset_id": asset_id},
|
||||||
|
model=AssetInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_user_asset_info(
|
||||||
|
asset: AssetInfo,
|
||||||
|
) -> AssetInfo:
|
||||||
|
await db.update("assets", asset)
|
||||||
|
return asset
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_user_asset(
|
||||||
|
user_id: str, asset_id: str, conn: Connection | None = None
|
||||||
|
) -> None:
|
||||||
|
await (conn or db).execute(
|
||||||
|
query="DELETE FROM assets WHERE id = :asset_id AND user_id = :user_id",
|
||||||
|
values={"asset_id": asset_id, "user_id": user_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_assets(
|
||||||
|
user_id: str,
|
||||||
|
filters: Filters[AssetFilters] | None = None,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Page[AssetInfo]:
|
||||||
|
filters = filters or Filters()
|
||||||
|
filters.sortby = filters.sortby or "created_at"
|
||||||
|
return await (conn or db).fetch_page(
|
||||||
|
query="SELECT * FROM assets",
|
||||||
|
where=["user_id = :user_id"],
|
||||||
|
values={"user_id": user_id},
|
||||||
|
filters=filters,
|
||||||
|
model=AssetInfo,
|
||||||
|
table_name="assets",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_assets_count(user_id: str) -> int:
|
||||||
|
result = await db.execute(
|
||||||
|
query="SELECT COUNT(*) as count FROM assets WHERE user_id = :user_id",
|
||||||
|
values={"user_id": user_id},
|
||||||
|
)
|
||||||
|
row = result.mappings().first()
|
||||||
|
return row.get("count", 0)
|
||||||
@@ -16,11 +16,12 @@ async def get_audit_entries(
|
|||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Page[AuditEntry]:
|
) -> Page[AuditEntry]:
|
||||||
return await (conn or db).fetch_page(
|
return await (conn or db).fetch_page(
|
||||||
"SELECT * from audit",
|
"SELECT * FROM audit",
|
||||||
[],
|
[],
|
||||||
{},
|
{},
|
||||||
filters=filters,
|
filters=filters,
|
||||||
model=AuditEntry,
|
model=AuditEntry,
|
||||||
|
table_name="audit",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -33,13 +34,12 @@ async def get_payment(checking_id: str, conn: Connection | None = None) -> Payme
|
|||||||
|
|
||||||
async def get_standalone_payment(
|
async def get_standalone_payment(
|
||||||
checking_id_or_hash: str,
|
checking_id_or_hash: str,
|
||||||
conn: Connection | None = None,
|
|
||||||
incoming: bool | None = False,
|
incoming: bool | None = False,
|
||||||
wallet_id: str | None = None,
|
wallet_id: str | None = None,
|
||||||
|
conn: Connection | None = None,
|
||||||
) -> Payment | None:
|
) -> Payment | None:
|
||||||
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
||||||
values = {
|
values = {
|
||||||
"wallet_id": wallet_id,
|
|
||||||
"checking_id": checking_id_or_hash,
|
"checking_id": checking_id_or_hash,
|
||||||
"hash": checking_id_or_hash,
|
"hash": checking_id_or_hash,
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,10 @@ async def get_standalone_payment(
|
|||||||
clause = f"({clause}) AND amount > 0"
|
clause = f"({clause}) AND amount > 0"
|
||||||
|
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
|
if not wallet or not wallet.can_view_payments:
|
||||||
|
return None
|
||||||
|
values["wallet_id"] = wallet.source_wallet_id
|
||||||
clause = f"({clause}) AND wallet_id = :wallet_id"
|
clause = f"({clause}) AND wallet_id = :wallet_id"
|
||||||
|
|
||||||
row = await (conn or db).fetchone(
|
row = await (conn or db).fetchone(
|
||||||
@@ -66,13 +70,16 @@ async def get_standalone_payment(
|
|||||||
async def get_wallet_payment(
|
async def get_wallet_payment(
|
||||||
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
||||||
) -> Payment | None:
|
) -> Payment | None:
|
||||||
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
|
if not wallet or not wallet.can_view_payments:
|
||||||
|
return None
|
||||||
payment = await (conn or db).fetchone(
|
payment = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM apipayments
|
FROM apipayments
|
||||||
WHERE wallet_id = :wallet AND payment_hash = :hash
|
WHERE wallet_id = :wallet AND payment_hash = :hash
|
||||||
""",
|
""",
|
||||||
{"wallet": wallet_id, "hash": payment_hash},
|
{"wallet": wallet.source_wallet_id, "hash": payment_hash},
|
||||||
Payment,
|
Payment,
|
||||||
)
|
)
|
||||||
return payment
|
return payment
|
||||||
@@ -118,7 +125,6 @@ async def get_payments_paginated( # noqa: C901
|
|||||||
Filters payments to be returned by:
|
Filters payments to be returned by:
|
||||||
- complete | pending | failed | outgoing | incoming.
|
- complete | pending | failed | outgoing | incoming.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
values: dict[str, Any] = {
|
values: dict[str, Any] = {
|
||||||
"time": since,
|
"time": since,
|
||||||
}
|
}
|
||||||
@@ -128,7 +134,11 @@ async def get_payments_paginated( # noqa: C901
|
|||||||
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
||||||
|
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
values["wallet_id"] = wallet_id
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
|
if not wallet or not wallet.can_view_payments:
|
||||||
|
return Page(data=[], total=0)
|
||||||
|
|
||||||
|
values["wallet_id"] = wallet.source_wallet_id
|
||||||
clause.append("wallet_id = :wallet_id")
|
clause.append("wallet_id = :wallet_id")
|
||||||
elif user_id:
|
elif user_id:
|
||||||
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||||
@@ -171,6 +181,7 @@ async def get_payments_paginated( # noqa: C901
|
|||||||
values,
|
values,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
model=Payment,
|
model=Payment,
|
||||||
|
table_name="apipayments",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -282,6 +293,7 @@ async def create_payment(
|
|||||||
fee=-abs(data.fee),
|
fee=-abs(data.fee),
|
||||||
tag=extra.get("tag", None),
|
tag=extra.get("tag", None),
|
||||||
extra=extra,
|
extra=extra,
|
||||||
|
labels=data.labels or [],
|
||||||
)
|
)
|
||||||
|
|
||||||
await (conn or db).insert("apipayments", payment)
|
await (conn or db).insert("apipayments", payment)
|
||||||
@@ -293,8 +305,16 @@ async def update_payment_checking_id(
|
|||||||
checking_id: str, new_checking_id: str, conn: Connection | None = None
|
checking_id: str, new_checking_id: str, conn: Connection | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"UPDATE apipayments SET checking_id = :new_id WHERE checking_id = :old_id",
|
f"""
|
||||||
{"new_id": new_checking_id, "old_id": checking_id},
|
UPDATE apipayments
|
||||||
|
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder('now')}
|
||||||
|
WHERE checking_id = :old_id
|
||||||
|
""", # noqa: S608
|
||||||
|
{
|
||||||
|
"new_id": new_checking_id,
|
||||||
|
"old_id": checking_id,
|
||||||
|
"now": int(time()),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -303,6 +323,7 @@ async def update_payment(
|
|||||||
new_checking_id: str | None = None,
|
new_checking_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
payment.updated_at = datetime.now(timezone.utc)
|
||||||
await (conn or db).update(
|
await (conn or db).update(
|
||||||
"apipayments", payment, "WHERE checking_id = :checking_id"
|
"apipayments", payment, "WHERE checking_id = :checking_id"
|
||||||
)
|
)
|
||||||
@@ -314,13 +335,14 @@ async def get_payments_history(
|
|||||||
wallet_id: str | None = None,
|
wallet_id: str | None = None,
|
||||||
group: DateTrunc = "day",
|
group: DateTrunc = "day",
|
||||||
filters: Filters | None = None,
|
filters: Filters | None = None,
|
||||||
|
conn: Connection | None = None,
|
||||||
) -> list[PaymentHistoryPoint]:
|
) -> list[PaymentHistoryPoint]:
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|
||||||
date_trunc = db.datetime_grouping(group)
|
date_trunc = db.datetime_grouping(group)
|
||||||
|
|
||||||
values = {
|
values: dict[str, Any] = {
|
||||||
"wallet_id": wallet_id,
|
"wallet_id": wallet_id,
|
||||||
}
|
}
|
||||||
# count outgoing payments if they are still pending
|
# count outgoing payments if they are still pending
|
||||||
@@ -349,13 +371,13 @@ async def get_payments_history(
|
|||||||
filters.values(values),
|
filters.values(values),
|
||||||
)
|
)
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
wallet = await get_wallet(wallet_id)
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
if wallet:
|
if not wallet or not wallet.can_view_payments:
|
||||||
balance = wallet.balance_msat
|
return []
|
||||||
else:
|
balance = wallet.balance_msat
|
||||||
raise ValueError("Unknown wallet")
|
values["wallet_id"] = wallet.source_wallet_id
|
||||||
else:
|
else:
|
||||||
balance = await get_total_balance()
|
balance = await get_total_balance(conn=conn)
|
||||||
|
|
||||||
# since we dont know the balance at the starting point,
|
# since we dont know the balance at the starting point,
|
||||||
# we take the current balance and walk backwards
|
# we take the current balance and walk backwards
|
||||||
|
|||||||
+21
-11
@@ -4,7 +4,7 @@ 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 get_wallets
|
from lnbits.core.crud.wallets import create_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
|
||||||
@@ -30,9 +30,9 @@ async def create_account(
|
|||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def update_account(account: Account) -> 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 db.update("accounts", account)
|
await (conn or db).update("accounts", account)
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
@@ -89,6 +89,7 @@ async def get_accounts(
|
|||||||
filters=filters,
|
filters=filters,
|
||||||
model=AccountOverview,
|
model=AccountOverview,
|
||||||
group_by=["accounts.id"],
|
group_by=["accounts.id"],
|
||||||
|
table_name="accounts",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -170,17 +171,24 @@ async def get_account_by_username_or_email(
|
|||||||
|
|
||||||
|
|
||||||
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
||||||
account = await get_account(user_id, conn)
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
if not account:
|
account = await get_account(user_id, conn=conn)
|
||||||
return None
|
if not account:
|
||||||
return await get_user_from_account(account, conn)
|
return None
|
||||||
|
return await get_user_from_account(account, conn=conn)
|
||||||
|
|
||||||
|
|
||||||
async def get_user_from_account(
|
async def get_user_from_account(
|
||||||
account: Account, conn: Connection | None = None
|
account: Account, conn: Connection | None = None
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
extensions = await get_user_active_extensions_ids(account.id, conn)
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
wallets = await get_wallets(account.id, False, conn=conn)
|
extensions = await get_user_active_extensions_ids(account.id, conn=conn)
|
||||||
|
wallets = await get_wallets(account.id, deleted=False, conn=conn)
|
||||||
|
|
||||||
|
if len(wallets) == 0:
|
||||||
|
wallet = await create_wallet(user_id=account.id, conn=conn)
|
||||||
|
wallets.append(wallet)
|
||||||
|
|
||||||
return User(
|
return User(
|
||||||
id=account.id,
|
id=account.id,
|
||||||
email=account.email,
|
email=account.email,
|
||||||
@@ -199,9 +207,11 @@ async def get_user_from_account(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def update_user_access_control_list(user_acls: UserAcls):
|
async def update_user_access_control_list(
|
||||||
|
user_acls: UserAcls, conn: Connection | None = None
|
||||||
|
):
|
||||||
user_acls.updated_at = datetime.now(timezone.utc)
|
user_acls.updated_at = datetime.now(timezone.utc)
|
||||||
await db.update("accounts", user_acls)
|
await (conn or db).update("accounts", user_acls)
|
||||||
|
|
||||||
|
|
||||||
async def get_user_access_control_lists(
|
async def get_user_access_control_lists(
|
||||||
|
|||||||
+111
-14
@@ -3,9 +3,10 @@ from time import time
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.wallets import WalletsFilters
|
from lnbits.core.models.wallets import BaseWallet, WalletsFilters, WalletType
|
||||||
from lnbits.db import Connection, Filters, Page
|
from lnbits.db import Connection, Filters, Page
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
from ..models import Wallet
|
from ..models import Wallet
|
||||||
|
|
||||||
@@ -14,17 +15,22 @@ async def create_wallet(
|
|||||||
*,
|
*,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
wallet_name: str | None = None,
|
wallet_name: str | None = None,
|
||||||
|
wallet_type: WalletType = WalletType.LIGHTNING,
|
||||||
|
shared_wallet_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
wallet_id = uuid4().hex
|
wallet_id = uuid4().hex
|
||||||
wallet = Wallet(
|
wallet = Wallet(
|
||||||
id=wallet_id,
|
id=wallet_id,
|
||||||
name=wallet_name or settings.lnbits_default_wallet_name,
|
name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
|
wallet_type=wallet_type.value,
|
||||||
|
shared_wallet_id=shared_wallet_id,
|
||||||
user=user_id,
|
user=user_id,
|
||||||
adminkey=uuid4().hex,
|
adminkey=uuid4().hex,
|
||||||
inkey=uuid4().hex,
|
inkey=uuid4().hex,
|
||||||
currency=settings.lnbits_default_accounting_currency or "USD",
|
currency=settings.lnbits_default_accounting_currency or "USD",
|
||||||
)
|
)
|
||||||
|
|
||||||
await (conn or db).insert("wallets", wallet)
|
await (conn or db).insert("wallets", wallet)
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
@@ -39,13 +45,14 @@ async def update_wallet(
|
|||||||
|
|
||||||
|
|
||||||
async def delete_wallet(
|
async def delete_wallet(
|
||||||
*,
|
|
||||||
user_id: str,
|
user_id: str,
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
deleted: bool = True,
|
deleted: bool = True,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
_clear_wallet_cache(wallet_id)
|
||||||
now = int(time())
|
now = int(time())
|
||||||
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
@@ -58,6 +65,7 @@ async def delete_wallet(
|
|||||||
|
|
||||||
|
|
||||||
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
||||||
|
_clear_wallet_cache(wallet_id)
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE FROM wallets WHERE id = :wallet",
|
"DELETE FROM wallets WHERE id = :wallet",
|
||||||
{"wallet": wallet_id},
|
{"wallet": wallet_id},
|
||||||
@@ -67,6 +75,7 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
|
|||||||
async def delete_wallet_by_id(
|
async def delete_wallet_by_id(
|
||||||
wallet_id: str, conn: Connection | None = None
|
wallet_id: str, conn: Connection | None = None
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
|
_clear_wallet_cache(wallet_id)
|
||||||
now = int(time())
|
now = int(time())
|
||||||
result = await (conn or db).execute(
|
result = await (conn or db).execute(
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
@@ -103,8 +112,8 @@ async def delete_unused_wallets(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_wallet(
|
async def get_standalone_wallet(
|
||||||
wallet_id: str, deleted: bool | None = None, conn: Connection | None = None
|
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
query = """
|
query = """
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
@@ -121,8 +130,23 @@ async def get_wallet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_wallet(
|
||||||
|
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||||
|
) -> Wallet | None:
|
||||||
|
wallet = await get_standalone_wallet(wallet_id, deleted, conn)
|
||||||
|
if not wallet:
|
||||||
|
return None
|
||||||
|
if wallet.is_lightning_shared_wallet:
|
||||||
|
return await get_source_wallet(wallet, conn)
|
||||||
|
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets(
|
async def get_wallets(
|
||||||
user_id: str, deleted: bool | None = None, conn: Connection | None = None
|
user_id: str,
|
||||||
|
deleted: bool | None = False,
|
||||||
|
wallet_type: WalletType | None = None,
|
||||||
|
conn: Connection | None = None,
|
||||||
) -> list[Wallet]:
|
) -> list[Wallet]:
|
||||||
query = """
|
query = """
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
@@ -132,12 +156,20 @@ async def get_wallets(
|
|||||||
"""
|
"""
|
||||||
if deleted is not None:
|
if deleted is not None:
|
||||||
query += " AND deleted = :deleted "
|
query += " AND deleted = :deleted "
|
||||||
return await (conn or db).fetchall(
|
if wallet_type is not None:
|
||||||
|
query += " AND wallet_type = :wallet_type "
|
||||||
|
wallets = await (conn or db).fetchall(
|
||||||
query,
|
query,
|
||||||
{"user": user_id, "deleted": deleted},
|
{
|
||||||
|
"user": user_id,
|
||||||
|
"deleted": deleted,
|
||||||
|
"wallet_type": wallet_type.value if wallet_type else None,
|
||||||
|
},
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return await get_source_wallets(wallets, conn)
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_paginated(
|
async def get_wallets_paginated(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
@@ -149,7 +181,7 @@ async def get_wallets_paginated(
|
|||||||
deleted = False
|
deleted = False
|
||||||
|
|
||||||
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
|
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
|
||||||
return await (conn or db).fetch_page(
|
wallets = await (conn or db).fetch_page(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
@@ -159,20 +191,27 @@ async def get_wallets_paginated(
|
|||||||
values={"user": user_id, "deleted": deleted},
|
values={"user": user_id, "deleted": deleted},
|
||||||
filters=filters,
|
filters=filters,
|
||||||
model=Wallet,
|
model=Wallet,
|
||||||
|
table_name="wallets",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
wallets.data = await get_source_wallets(wallets.data, conn)
|
||||||
|
return wallets
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_ids(
|
async def get_wallets_ids(
|
||||||
user_id: str, deleted: bool | None = None, conn: Connection | None = None
|
user_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
query = """SELECT id FROM wallets WHERE "user" = :user"""
|
query = """SELECT * FROM wallets WHERE "user" = :user"""
|
||||||
if deleted is not None:
|
if deleted is not None:
|
||||||
query += "AND deleted = :deleted"
|
query += " AND deleted = :deleted "
|
||||||
result: list[dict] = await (conn or db).fetchall(
|
wallets = await (conn or db).fetchall(
|
||||||
query,
|
query,
|
||||||
{"user": user_id, "deleted": deleted},
|
{"user": user_id, "deleted": deleted},
|
||||||
|
Wallet,
|
||||||
)
|
)
|
||||||
return [row["id"] for row in result]
|
|
||||||
|
wallets = await get_source_wallets(wallets, conn)
|
||||||
|
return [w.source_wallet_id for w in wallets if w.can_view_payments]
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_count():
|
async def get_wallets_count():
|
||||||
@@ -185,7 +224,7 @@ async def get_wallet_for_key(
|
|||||||
key: str,
|
key: str,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
return await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
@@ -196,9 +235,67 @@ async def get_wallet_for_key(
|
|||||||
{"key": key},
|
{"key": key},
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
if not wallet:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if wallet.is_lightning_shared_wallet:
|
||||||
|
mw = await get_source_wallet(wallet, conn)
|
||||||
|
return mw
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
|
async def get_base_wallet_for_key(
|
||||||
|
key: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> BaseWallet | None:
|
||||||
|
wallet = await (conn or db).fetchone(
|
||||||
|
"""
|
||||||
|
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
|
||||||
|
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
||||||
|
""",
|
||||||
|
{"key": key},
|
||||||
|
BaseWallet,
|
||||||
|
)
|
||||||
|
if not wallet:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
|
async def get_source_wallet(
|
||||||
|
wallet: Wallet, conn: Connection | None = None
|
||||||
|
) -> Wallet | None:
|
||||||
|
if not wallet.is_lightning_shared_wallet:
|
||||||
|
return wallet
|
||||||
|
if not wallet.shared_wallet_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
shared_wallet = await get_standalone_wallet(wallet.shared_wallet_id, False, conn)
|
||||||
|
if not shared_wallet:
|
||||||
|
return None
|
||||||
|
wallet.mirror_shared_wallet(shared_wallet)
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
|
async def get_source_wallets(
|
||||||
|
wallet: list[Wallet], conn: Connection | None = None
|
||||||
|
) -> list[Wallet]:
|
||||||
|
source_wallets = []
|
||||||
|
for w in wallet:
|
||||||
|
source_wallet = await get_source_wallet(w, conn)
|
||||||
|
if source_wallet:
|
||||||
|
source_wallets.append(source_wallet)
|
||||||
|
return source_wallets
|
||||||
|
|
||||||
|
|
||||||
async def get_total_balance(conn: Connection | None = None):
|
async def get_total_balance(conn: Connection | None = None):
|
||||||
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
||||||
row = result.mappings().first()
|
row = result.mappings().first()
|
||||||
return row.get("balance", 0) or 0
|
return row.get("balance", 0) or 0
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_wallet_cache(wallet_id):
|
||||||
|
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
||||||
|
if cached_wallet:
|
||||||
|
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
||||||
|
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
||||||
|
|||||||
@@ -743,3 +743,108 @@ async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
|||||||
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):
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
ALTER TABLE wallets ADD COLUMN wallet_type TEXT DEFAULT 'lightning'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m036_add_shared_wallet_column(db: Connection):
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m037_create_assets_table(db: Connection):
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS assets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
mime_type TEXT NOT NULL,
|
||||||
|
is_public BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
size_bytes INT NOT NULL,
|
||||||
|
thumbnail_base64 TEXT,
|
||||||
|
thumbnail {db.blob},
|
||||||
|
data {db.blob} NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m038_add_labels_for_payments(db: Connection):
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
ALTER TABLE apipayments ADD COLUMN labels TEXT
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m039_index_payments(db: Connection):
|
||||||
|
indexes = [
|
||||||
|
"wallet_id",
|
||||||
|
"checking_id",
|
||||||
|
"payment_hash",
|
||||||
|
"amount",
|
||||||
|
"fee",
|
||||||
|
"labels",
|
||||||
|
"time",
|
||||||
|
"status",
|
||||||
|
"memo",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
for index in indexes:
|
||||||
|
logger.debug(f"Creating index idx_payments_{index}...")
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_payments_{index} ON apipayments ({index});
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m040_index_wallets(db: Connection):
|
||||||
|
indexes = [
|
||||||
|
"id",
|
||||||
|
"user",
|
||||||
|
"deleted",
|
||||||
|
"adminkey",
|
||||||
|
"inkey",
|
||||||
|
"wallet_type",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
for index in indexes:
|
||||||
|
logger.debug(f"Creating index idx_wallets_{index}...")
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_wallets_{index} ON wallets ("{index}");
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m042_index_accounts(db: Connection):
|
||||||
|
indexes = [
|
||||||
|
"id",
|
||||||
|
"email",
|
||||||
|
"username",
|
||||||
|
"pubkey",
|
||||||
|
"external_id",
|
||||||
|
]
|
||||||
|
|
||||||
|
for index in indexes:
|
||||||
|
logger.debug(f"Creating index idx_wallets_{index}...")
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ from .users import (
|
|||||||
UserAcls,
|
UserAcls,
|
||||||
UserExtra,
|
UserExtra,
|
||||||
)
|
)
|
||||||
from .wallets import BaseWallet, CreateWallet, KeyType, Wallet, WalletTypeInfo
|
from .wallets import CreateWallet, KeyType, Wallet, WalletInfo, WalletTypeInfo
|
||||||
from .webpush import CreateWebPushSubscription, WebPushSubscription
|
from .webpush import CreateWebPushSubscription, WebPushSubscription
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -57,7 +57,6 @@ __all__ = [
|
|||||||
"AuditEntry",
|
"AuditEntry",
|
||||||
"AuditFilters",
|
"AuditFilters",
|
||||||
"BalanceDelta",
|
"BalanceDelta",
|
||||||
"BaseWallet",
|
|
||||||
"Callback",
|
"Callback",
|
||||||
"CancelInvoice",
|
"CancelInvoice",
|
||||||
"ConversionData",
|
"ConversionData",
|
||||||
@@ -99,6 +98,7 @@ __all__ = [
|
|||||||
"UserAcls",
|
"UserAcls",
|
||||||
"UserExtra",
|
"UserExtra",
|
||||||
"Wallet",
|
"Wallet",
|
||||||
|
"WalletInfo",
|
||||||
"WalletTypeInfo",
|
"WalletTypeInfo",
|
||||||
"WebPushSubscription",
|
"WebPushSubscription",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from lnbits.db import FilterModel
|
||||||
|
|
||||||
|
|
||||||
|
class AssetInfo(BaseModel):
|
||||||
|
id: str
|
||||||
|
mime_type: str
|
||||||
|
name: str
|
||||||
|
is_public: bool = False
|
||||||
|
size_bytes: int
|
||||||
|
thumbnail_base64: str | None = None
|
||||||
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
|
||||||
|
class Asset(AssetInfo):
|
||||||
|
user_id: str
|
||||||
|
data: bytes
|
||||||
|
|
||||||
|
|
||||||
|
class AssetUpdate(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
is_public: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AssetFilters(FilterModel):
|
||||||
|
__search_fields__ = ["name"]
|
||||||
|
__sort_fields__ = [
|
||||||
|
"created_at",
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
name: str | None = None
|
||||||
@@ -6,12 +6,13 @@ import json
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from asyncio.tasks import create_task
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
download_url,
|
download_url,
|
||||||
@@ -20,6 +21,7 @@ from lnbits.helpers import (
|
|||||||
version_parse,
|
version_parse,
|
||||||
)
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
|
|
||||||
class ExplicitRelease(BaseModel):
|
class ExplicitRelease(BaseModel):
|
||||||
@@ -606,6 +608,37 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_installable_extensions(
|
async def get_installable_extensions(
|
||||||
|
cls, post_refresh_cache: bool = False
|
||||||
|
) -> list[InstallableExtension]:
|
||||||
|
extension_list: list[InstallableExtension] = []
|
||||||
|
|
||||||
|
cache_key = "extensions:installable"
|
||||||
|
cache_value = cache.value(cache_key)
|
||||||
|
if not cache_value:
|
||||||
|
extension_list = await cls._get_installable_extensions()
|
||||||
|
cache.set(cache_key, extension_list, expiry=3600) # one hour
|
||||||
|
return extension_list
|
||||||
|
|
||||||
|
if cache_value.older_than(10 * 60) or post_refresh_cache:
|
||||||
|
# refresh cache in background if older than 10 minutes or requested
|
||||||
|
create_task(cls._refresh_installable_extensions_cache())
|
||||||
|
|
||||||
|
extension_list = cache_value.value # type: ignore
|
||||||
|
return extension_list
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _refresh_installable_extensions_cache(
|
||||||
|
cls,
|
||||||
|
) -> None:
|
||||||
|
cache_key = "extensions:installable"
|
||||||
|
extension_list: list[InstallableExtension] = (
|
||||||
|
await cls._get_installable_extensions()
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.set(cache_key, extension_list, expiry=3600)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _get_installable_extensions(
|
||||||
cls,
|
cls,
|
||||||
) -> list[InstallableExtension]:
|
) -> list[InstallableExtension]:
|
||||||
extension_list: list[InstallableExtension] = []
|
extension_list: list[InstallableExtension] = []
|
||||||
@@ -744,6 +777,32 @@ class ExtensionDetailsRequest(BaseModel):
|
|||||||
version: str
|
version: str
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionReviewsStatus(BaseModel):
|
||||||
|
tag: str
|
||||||
|
avg_rating: float
|
||||||
|
review_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class CreateExtensionReview(BaseModel):
|
||||||
|
tag: str
|
||||||
|
name: str | None = Field(None)
|
||||||
|
rating: int = Field(..., ge=0, le=1000)
|
||||||
|
comment: str | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionReviewPaymentRequest(BaseModel):
|
||||||
|
payment_hash: str
|
||||||
|
payment_request: str
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionReview(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str | None = Field(default=None)
|
||||||
|
tag: str | None = Field(default=None)
|
||||||
|
rating: int = Field(default=0, ge=0, le=1000)
|
||||||
|
comment: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
async def github_api_get(url: str, error_msg: str | None) -> Any:
|
async def github_api_get(url: str, error_msg: str | None) -> Any:
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
if settings.lnbits_ext_github_token:
|
if settings.lnbits_ext_github_token:
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class CreatePayment(BaseModel):
|
|||||||
expiry: datetime | None = None
|
expiry: datetime | None = None
|
||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
fee: int = 0
|
fee: int = 0
|
||||||
|
labels: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
class Payment(BaseModel):
|
class Payment(BaseModel):
|
||||||
@@ -81,6 +82,7 @@ class Payment(BaseModel):
|
|||||||
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
labels: list[str] = []
|
||||||
extra: dict = {}
|
extra: dict = {}
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, **data):
|
||||||
@@ -184,9 +186,25 @@ class Payment(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PaymentFilters(FilterModel):
|
class PaymentFilters(FilterModel):
|
||||||
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
|
__search_fields__ = [
|
||||||
|
"memo",
|
||||||
|
"amount",
|
||||||
|
"wallet_id",
|
||||||
|
"tag",
|
||||||
|
"status",
|
||||||
|
"time",
|
||||||
|
"labels",
|
||||||
|
]
|
||||||
|
|
||||||
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
__sort_fields__ = [
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"amount",
|
||||||
|
"fee",
|
||||||
|
"memo",
|
||||||
|
"time",
|
||||||
|
"tag",
|
||||||
|
]
|
||||||
|
|
||||||
status: str | None
|
status: str | None
|
||||||
tag: str | None
|
tag: str | None
|
||||||
@@ -198,6 +216,7 @@ class PaymentFilters(FilterModel):
|
|||||||
preimage: str | None
|
preimage: str | None
|
||||||
payment_hash: str | None
|
payment_hash: str | None
|
||||||
wallet_id: str | None
|
wallet_id: str | None
|
||||||
|
labels: str | None
|
||||||
|
|
||||||
|
|
||||||
class PaymentDataPoint(BaseModel):
|
class PaymentDataPoint(BaseModel):
|
||||||
@@ -272,6 +291,7 @@ class CreateInvoice(BaseModel):
|
|||||||
bolt11: str | None = None
|
bolt11: str | None = None
|
||||||
lnurl_withdraw: LnurlWithdrawResponse | None = None
|
lnurl_withdraw: LnurlWithdrawResponse | None = None
|
||||||
fiat_provider: str | None = None
|
fiat_provider: str | None = None
|
||||||
|
labels: list[str] = []
|
||||||
|
|
||||||
@validator("payment_hash")
|
@validator("payment_hash")
|
||||||
def check_hex(cls, v):
|
def check_hex(cls, v):
|
||||||
@@ -320,3 +340,7 @@ class CancelInvoice(BaseModel):
|
|||||||
def check_hex(cls, v):
|
def check_hex(cls, v):
|
||||||
_ = bytes.fromhex(v)
|
_ = bytes.fromhex(v)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePaymentLabels(BaseModel):
|
||||||
|
labels: list[str] = []
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from lnbits.db import FilterModel
|
|||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
is_valid_email_address,
|
is_valid_email_address,
|
||||||
is_valid_external_id,
|
is_valid_external_id,
|
||||||
|
is_valid_label,
|
||||||
is_valid_pubkey,
|
is_valid_pubkey,
|
||||||
is_valid_username,
|
is_valid_username,
|
||||||
)
|
)
|
||||||
@@ -29,6 +30,21 @@ class UserNotifications(BaseModel):
|
|||||||
incoming_payments_sats: int = 0
|
incoming_payments_sats: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class WalletInviteRequest(BaseModel):
|
||||||
|
request_id: str
|
||||||
|
from_user_name: str | None = None
|
||||||
|
to_wallet_id: str
|
||||||
|
to_wallet_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserLabel(BaseModel):
|
||||||
|
name: str = Field(regex=r"([A-Za-z0-9 ._-]{1,100}$)")
|
||||||
|
description: str | None = Field(default=None, max_length=250)
|
||||||
|
color: str | None = Field(
|
||||||
|
default=None, regex=r"^#[0-9A-Fa-f]{6}$"
|
||||||
|
) # e.g., "#RRGGBB"
|
||||||
|
|
||||||
|
|
||||||
class UserExtra(BaseModel):
|
class UserExtra(BaseModel):
|
||||||
email_verified: bool | None = False
|
email_verified: bool | None = False
|
||||||
first_name: str | None = None
|
first_name: str | None = None
|
||||||
@@ -46,6 +62,55 @@ class UserExtra(BaseModel):
|
|||||||
|
|
||||||
notifications: UserNotifications = UserNotifications()
|
notifications: UserNotifications = UserNotifications()
|
||||||
|
|
||||||
|
wallet_invite_requests: list[WalletInviteRequest] = []
|
||||||
|
|
||||||
|
labels: list[UserLabel] = []
|
||||||
|
|
||||||
|
def add_wallet_invite_request(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
to_wallet_id: str,
|
||||||
|
to_wallet_name: str,
|
||||||
|
from_user_name: str | None = None,
|
||||||
|
) -> WalletInviteRequest:
|
||||||
|
self.remove_wallet_invite_request(request_id)
|
||||||
|
invite = WalletInviteRequest(
|
||||||
|
request_id=request_id,
|
||||||
|
from_user_name=from_user_name,
|
||||||
|
to_wallet_id=to_wallet_id,
|
||||||
|
to_wallet_name=to_wallet_name,
|
||||||
|
)
|
||||||
|
self.wallet_invite_requests.append(invite)
|
||||||
|
return invite
|
||||||
|
|
||||||
|
def find_wallet_invite_request(self, request_id: str) -> WalletInviteRequest | None:
|
||||||
|
for invite in self.wallet_invite_requests:
|
||||||
|
if invite.request_id == request_id:
|
||||||
|
return invite
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_labels(self):
|
||||||
|
seen_labels = set()
|
||||||
|
for label in self.labels:
|
||||||
|
if not label.name:
|
||||||
|
raise ValueError("Label name cannot be empty.")
|
||||||
|
# apply the same rule for labels as for usernames
|
||||||
|
if not is_valid_label(label.name):
|
||||||
|
raise ValueError(f"Invalid label name: {label.name}")
|
||||||
|
if label.name in seen_labels:
|
||||||
|
raise ValueError(f"Duplicate label name: {label.name}")
|
||||||
|
seen_labels.add(label.name)
|
||||||
|
|
||||||
|
def remove_wallet_invite_request(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
):
|
||||||
|
self.wallet_invite_requests = [
|
||||||
|
invite
|
||||||
|
for invite in self.wallet_invite_requests
|
||||||
|
if invite.request_id != request_id
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class EndpointAccess(BaseModel):
|
class EndpointAccess(BaseModel):
|
||||||
path: str
|
path: str
|
||||||
@@ -107,8 +172,15 @@ class UserAcls(BaseModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Account(BaseModel):
|
class AccountId(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_admin_id(self) -> bool:
|
||||||
|
return settings.is_admin_user(self.id)
|
||||||
|
|
||||||
|
|
||||||
|
class Account(AccountId):
|
||||||
external_id: str | None = None # for external account linking
|
external_id: str | None = None # for external account linking
|
||||||
username: str | None = None
|
username: str | None = None
|
||||||
password_hash: str | None = None
|
password_hash: str | None = None
|
||||||
@@ -129,6 +201,10 @@ class Account(BaseModel):
|
|||||||
self.is_admin = settings.is_admin_user(self.id)
|
self.is_admin = settings.is_admin_user(self.id)
|
||||||
self.fiat_providers = settings.get_fiat_providers_for_user(self.id)
|
self.fiat_providers = settings.get_fiat_providers_for_user(self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_password(self) -> bool:
|
||||||
|
return self.password_hash is not None
|
||||||
|
|
||||||
def hash_password(self, password: str) -> str:
|
def hash_password(self, password: str) -> str:
|
||||||
"""sets and returns the hashed password"""
|
"""sets and returns the hashed password"""
|
||||||
salt = gensalt()
|
salt = gensalt()
|
||||||
@@ -160,6 +236,8 @@ class Account(BaseModel):
|
|||||||
if user_uuid4.hex != self.id:
|
if user_uuid4.hex != self.id:
|
||||||
raise ValueError("User ID is not valid UUID4 hex string.")
|
raise ValueError("User ID is not valid UUID4 hex string.")
|
||||||
|
|
||||||
|
self.extra.validate_labels()
|
||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
transaction_count: int | None = 0
|
transaction_count: int | None = 0
|
||||||
@@ -170,7 +248,7 @@ class AccountOverview(Account):
|
|||||||
|
|
||||||
class AccountFilters(FilterModel):
|
class AccountFilters(FilterModel):
|
||||||
__search_fields__ = [
|
__search_fields__ = [
|
||||||
"user",
|
"id",
|
||||||
"email",
|
"email",
|
||||||
"username",
|
"username",
|
||||||
"pubkey",
|
"pubkey",
|
||||||
@@ -178,17 +256,18 @@ class AccountFilters(FilterModel):
|
|||||||
"wallet_id",
|
"wallet_id",
|
||||||
]
|
]
|
||||||
__sort_fields__ = [
|
__sort_fields__ = [
|
||||||
"balance_msat",
|
"id",
|
||||||
"email",
|
"email",
|
||||||
"username",
|
"username",
|
||||||
"transaction_count",
|
"pubkey",
|
||||||
"wallet_count",
|
"external_id",
|
||||||
"last_payment",
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
|
||||||
email: str | None = None
|
id: str | None = None
|
||||||
user: str | None = None
|
|
||||||
username: str | None = None
|
username: str | None = None
|
||||||
|
email: str | None = None
|
||||||
pubkey: str | None = None
|
pubkey: str | None = None
|
||||||
external_id: str | None = None
|
external_id: str | None = None
|
||||||
wallet_id: str | None = None
|
wallet_id: str | None = None
|
||||||
|
|||||||
+172
-11
@@ -4,16 +4,14 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from lnurl import encode as lnurl_encode
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.core.models.lnurl import StoredPayLinks
|
from lnbits.core.models.lnurl import StoredPayLinks
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
from lnbits.helpers import url_for
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class BaseWallet(BaseModel):
|
class WalletInfo(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
adminkey: str
|
adminkey: str
|
||||||
@@ -21,18 +19,109 @@ class BaseWallet(BaseModel):
|
|||||||
balance_msat: int
|
balance_msat: int
|
||||||
|
|
||||||
|
|
||||||
|
class WalletType(Enum):
|
||||||
|
LIGHTNING = "lightning"
|
||||||
|
LIGHTNING_SHARED = "lightning-shared"
|
||||||
|
|
||||||
|
|
||||||
|
class WalletPermission(Enum):
|
||||||
|
VIEW_PAYMENTS = "view-payments"
|
||||||
|
RECEIVE_PAYMENTS = "receive-payments"
|
||||||
|
SEND_PAYMENTS = "send-payments"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class WalletShareStatus(Enum):
|
||||||
|
INVITE_SENT = "invite_sent"
|
||||||
|
APPROVED = "approved"
|
||||||
|
|
||||||
|
|
||||||
|
class WalletSharePermission(BaseModel):
|
||||||
|
# unique identifier for this share request
|
||||||
|
request_id: str | None = None
|
||||||
|
# username of the invited user
|
||||||
|
username: str
|
||||||
|
# ID of the wallet being shared with
|
||||||
|
shared_with_wallet_id: str | None = None
|
||||||
|
# permissions being granted
|
||||||
|
permissions: list[WalletPermission] = []
|
||||||
|
# status of the share request
|
||||||
|
status: WalletShareStatus
|
||||||
|
comment: str | None = None
|
||||||
|
|
||||||
|
def approve(
|
||||||
|
self,
|
||||||
|
permissions: list[WalletPermission] | None = None,
|
||||||
|
shared_with_wallet_id: str | None = None,
|
||||||
|
):
|
||||||
|
self.status = WalletShareStatus.APPROVED
|
||||||
|
if permissions is not None:
|
||||||
|
self.permissions = permissions
|
||||||
|
if shared_with_wallet_id is not None:
|
||||||
|
self.shared_with_wallet_id = shared_with_wallet_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_approved(self) -> bool:
|
||||||
|
return self.status == WalletShareStatus.APPROVED
|
||||||
|
|
||||||
|
|
||||||
class WalletExtra(BaseModel):
|
class WalletExtra(BaseModel):
|
||||||
icon: str = "flash_on"
|
icon: str = "flash_on"
|
||||||
color: str = "primary"
|
color: str = "primary"
|
||||||
pinned: bool = False
|
pinned: bool = False
|
||||||
|
# What permissions this wallet grants when it's shared with other users
|
||||||
|
shared_with: list[WalletSharePermission] = []
|
||||||
|
|
||||||
|
def invite_user_to_shared_wallet(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
request_type: WalletShareStatus,
|
||||||
|
username: str,
|
||||||
|
permissions: list[WalletPermission] | None = None,
|
||||||
|
) -> WalletSharePermission:
|
||||||
|
share = WalletSharePermission(
|
||||||
|
request_id=request_id,
|
||||||
|
username=username,
|
||||||
|
status=request_type,
|
||||||
|
permissions=permissions or [],
|
||||||
|
)
|
||||||
|
self.shared_with.append(share)
|
||||||
|
return share
|
||||||
|
|
||||||
|
def find_share_by_id(self, request_id: str) -> WalletSharePermission | None:
|
||||||
|
for share in self.shared_with:
|
||||||
|
if share.request_id == request_id:
|
||||||
|
return share
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_share_for_wallet(
|
||||||
|
self, shared_with_wallet_id: str
|
||||||
|
) -> WalletSharePermission | None:
|
||||||
|
for share in self.shared_with:
|
||||||
|
if share.shared_with_wallet_id == shared_with_wallet_id:
|
||||||
|
return share
|
||||||
|
return None
|
||||||
|
|
||||||
|
def remove_share_by_id(self, request_id: str):
|
||||||
|
self.shared_with = [
|
||||||
|
share for share in self.shared_with if share.request_id != request_id
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Wallet(BaseModel):
|
class BaseWallet(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
user: str
|
user: str
|
||||||
name: str
|
wallet_type: str = WalletType.LIGHTNING.value
|
||||||
adminkey: str
|
adminkey: str
|
||||||
inkey: str
|
inkey: str
|
||||||
|
|
||||||
|
|
||||||
|
class Wallet(BaseWallet):
|
||||||
|
name: str
|
||||||
|
# Must be set only for shared wallets
|
||||||
|
shared_wallet_id: str | None = None
|
||||||
deleted: bool = False
|
deleted: bool = False
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
@@ -40,6 +129,65 @@ class Wallet(BaseModel):
|
|||||||
balance_msat: int = Field(default=0, no_database=True)
|
balance_msat: int = Field(default=0, no_database=True)
|
||||||
extra: WalletExtra = WalletExtra()
|
extra: WalletExtra = WalletExtra()
|
||||||
stored_paylinks: StoredPayLinks = StoredPayLinks()
|
stored_paylinks: StoredPayLinks = StoredPayLinks()
|
||||||
|
# What permission this wallet has when it's a shared wallet
|
||||||
|
share_permissions: list[WalletPermission] = Field(default=[], no_database=True)
|
||||||
|
|
||||||
|
def __init__(self, **data):
|
||||||
|
super().__init__(**data)
|
||||||
|
self._validate_data()
|
||||||
|
|
||||||
|
def mirror_shared_wallet(
|
||||||
|
self,
|
||||||
|
shared_wallet: Wallet,
|
||||||
|
):
|
||||||
|
if not shared_wallet.is_lightning_wallet:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.wallet_type = WalletType.LIGHTNING_SHARED.value
|
||||||
|
self.shared_wallet_id = shared_wallet.id
|
||||||
|
self.name = shared_wallet.name
|
||||||
|
self.share_permissions = shared_wallet.get_share_permissions(self.id)
|
||||||
|
|
||||||
|
if len(self.share_permissions):
|
||||||
|
self.currency = shared_wallet.currency
|
||||||
|
self.balance_msat = shared_wallet.balance_msat
|
||||||
|
|
||||||
|
self.stored_paylinks = shared_wallet.stored_paylinks
|
||||||
|
self.extra.icon = shared_wallet.extra.icon
|
||||||
|
self.extra.color = shared_wallet.extra.color
|
||||||
|
|
||||||
|
def get_share_permissions(self, wallet_id: str) -> list[WalletPermission]:
|
||||||
|
for share in self.extra.shared_with:
|
||||||
|
if share.shared_with_wallet_id == wallet_id and share.is_approved:
|
||||||
|
return share.permissions
|
||||||
|
return []
|
||||||
|
|
||||||
|
def has_permission(self, permission: WalletPermission) -> bool:
|
||||||
|
if self.is_lightning_wallet:
|
||||||
|
return True
|
||||||
|
if self.is_lightning_shared_wallet:
|
||||||
|
return permission in self.share_permissions
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_wallet_id(self) -> str:
|
||||||
|
"""For shared wallets return the original wallet ID, else return own ID."""
|
||||||
|
if self.is_lightning_shared_wallet and len(self.share_permissions):
|
||||||
|
return self.shared_wallet_id or self.id
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_receive_payments(self) -> bool:
|
||||||
|
return self.has_permission(WalletPermission.RECEIVE_PAYMENTS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_send_payments(self) -> bool:
|
||||||
|
return self.has_permission(WalletPermission.SEND_PAYMENTS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_view_payments(self) -> bool:
|
||||||
|
return self.has_permission(WalletPermission.VIEW_PAYMENTS)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def balance(self) -> int:
|
def balance(self) -> int:
|
||||||
@@ -50,16 +198,23 @@ class Wallet(BaseModel):
|
|||||||
return self.balance_msat - settings.fee_reserve(self.balance_msat)
|
return self.balance_msat - settings.fee_reserve(self.balance_msat)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lnurlwithdraw_full(self) -> str:
|
def is_lightning_wallet(self) -> bool:
|
||||||
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
|
return self.wallet_type == WalletType.LIGHTNING.value
|
||||||
try:
|
|
||||||
return lnurl_encode(url)
|
@property
|
||||||
except Exception:
|
def is_lightning_shared_wallet(self) -> bool:
|
||||||
return ""
|
return self.wallet_type == WalletType.LIGHTNING_SHARED.value
|
||||||
|
|
||||||
|
def _validate_data(self):
|
||||||
|
if self.is_lightning_shared_wallet:
|
||||||
|
if not self.shared_wallet_id:
|
||||||
|
raise ValueError("Shared wallet ID must be set for shared wallets.")
|
||||||
|
|
||||||
|
|
||||||
class CreateWallet(BaseModel):
|
class CreateWallet(BaseModel):
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
|
wallet_type: WalletType = WalletType.LIGHTNING
|
||||||
|
shared_wallet_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class KeyType(Enum):
|
class KeyType(Enum):
|
||||||
@@ -78,6 +233,12 @@ class WalletTypeInfo:
|
|||||||
wallet: Wallet
|
wallet: Wallet
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BaseWalletTypeInfo:
|
||||||
|
key_type: KeyType
|
||||||
|
wallet: BaseWallet
|
||||||
|
|
||||||
|
|
||||||
class WalletsFilters(FilterModel):
|
class WalletsFilters(FilterModel):
|
||||||
__search_fields__ = ["id", "name", "currency"]
|
__search_fields__ = ["id", "name", "currency"]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import base64
|
||||||
|
import io
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from fastapi import UploadFile
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from lnbits.core.crud.assets import create_asset, get_user_assets_count
|
||||||
|
from lnbits.core.models.assets import Asset
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) -> Asset:
|
||||||
|
if not file.content_type:
|
||||||
|
raise ValueError("File must have a content type.")
|
||||||
|
if file.content_type.lower() not in settings.lnbits_assets_allowed_mime_types:
|
||||||
|
raise ValueError(f"File type '{file.content_type}' not allowed.")
|
||||||
|
|
||||||
|
if not settings.is_unlimited_assets_user(user_id):
|
||||||
|
user_assets_count = await get_user_assets_count(user_id)
|
||||||
|
if user_assets_count >= settings.lnbits_max_assets_per_user:
|
||||||
|
raise ValueError(
|
||||||
|
f"Max upload count of {settings.lnbits_max_assets_per_user} exceeded."
|
||||||
|
)
|
||||||
|
|
||||||
|
contents = await file.read()
|
||||||
|
if len(contents) > settings.lnbits_max_asset_size_mb * 1024 * 1024:
|
||||||
|
raise ValueError(
|
||||||
|
f"File limit of {settings.lnbits_max_asset_size_mb}MB exceeded."
|
||||||
|
)
|
||||||
|
|
||||||
|
image = Image.open(io.BytesIO(contents))
|
||||||
|
|
||||||
|
thumbnail_width = min(256, settings.lnbits_asset_thumbnail_width)
|
||||||
|
thumbnail_height = min(256, settings.lnbits_asset_thumbnail_height)
|
||||||
|
image.thumbnail((thumbnail_width, thumbnail_height))
|
||||||
|
|
||||||
|
# Save thumbnail to an in-memory buffer
|
||||||
|
thumb_buffer = io.BytesIO()
|
||||||
|
thumbnail_format = settings.lnbits_asset_thumbnail_format or "PNG"
|
||||||
|
image.save(thumb_buffer, format=thumbnail_format)
|
||||||
|
thumb_buffer.seek(0)
|
||||||
|
|
||||||
|
asset = Asset(
|
||||||
|
id=uuid4().hex,
|
||||||
|
user_id=user_id,
|
||||||
|
mime_type=file.content_type,
|
||||||
|
is_public=is_public,
|
||||||
|
name=file.filename or "unnamed",
|
||||||
|
size_bytes=len(contents),
|
||||||
|
thumbnail_base64=base64.b64encode(thumb_buffer.getvalue()).decode("utf-8"),
|
||||||
|
data=contents,
|
||||||
|
)
|
||||||
|
|
||||||
|
await create_asset(asset)
|
||||||
|
return asset
|
||||||
@@ -16,6 +16,7 @@ from lnbits.core.crud.extensions import (
|
|||||||
update_installed_extension,
|
update_installed_extension,
|
||||||
)
|
)
|
||||||
from lnbits.core.helpers import migrate_extension_database
|
from lnbits.core.helpers import migrate_extension_database
|
||||||
|
from lnbits.db import Connection
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
||||||
@@ -149,9 +150,9 @@ async def start_extension_background_work(ext_id: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def get_valid_extensions(
|
async def get_valid_extensions(
|
||||||
include_deactivated: bool | None = True,
|
include_deactivated: bool | None = True, conn: Connection | None = None
|
||||||
) -> list[Extension]:
|
) -> list[Extension]:
|
||||||
installed_extensions = await get_installed_extensions()
|
installed_extensions = await get_installed_extensions(conn=conn)
|
||||||
valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions]
|
valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions]
|
||||||
|
|
||||||
if include_deactivated:
|
if include_deactivated:
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
@@ -70,6 +72,63 @@ def check_stripe_signature(
|
|||||||
raise ValueError("Stripe signature verification failed.")
|
raise ValueError("Stripe signature verification failed.")
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_paypal_webhook(headers, payload: bytes):
|
||||||
|
"""
|
||||||
|
Validate PayPal webhook signatures using the PayPal verify API.
|
||||||
|
"""
|
||||||
|
webhook_id = settings.paypal_webhook_id
|
||||||
|
if not webhook_id:
|
||||||
|
logger.warning("PayPal webhook ID not set; skipping verification.")
|
||||||
|
return
|
||||||
|
|
||||||
|
required_headers = {
|
||||||
|
"PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"),
|
||||||
|
"PAYPAL-TRANSMISSION-TIME": headers.get("PAYPAL-TRANSMISSION-TIME"),
|
||||||
|
"PAYPAL-TRANSMISSION-SIG": headers.get("PAYPAL-TRANSMISSION-SIG"),
|
||||||
|
"PAYPAL-CERT-URL": headers.get("PAYPAL-CERT-URL"),
|
||||||
|
"PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"),
|
||||||
|
}
|
||||||
|
if not all(required_headers.values()):
|
||||||
|
logger.warning("Missing PayPal webhook headers; skipping verification.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client:
|
||||||
|
token_resp = await client.post(
|
||||||
|
"/v1/oauth2/token",
|
||||||
|
data={"grant_type": "client_credentials"},
|
||||||
|
auth=(
|
||||||
|
settings.paypal_client_id or "",
|
||||||
|
settings.paypal_client_secret or "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
token_resp.raise_for_status()
|
||||||
|
access_token = token_resp.json().get("access_token")
|
||||||
|
if not access_token:
|
||||||
|
raise ValueError("PayPal token missing in verification flow.")
|
||||||
|
|
||||||
|
verify_resp = await client.post(
|
||||||
|
"/v1/notifications/verify-webhook-signature",
|
||||||
|
json={
|
||||||
|
"auth_algo": required_headers["PAYPAL-AUTH-ALGO"],
|
||||||
|
"cert_url": required_headers["PAYPAL-CERT-URL"],
|
||||||
|
"transmission_id": required_headers["PAYPAL-TRANSMISSION-ID"],
|
||||||
|
"transmission_sig": required_headers["PAYPAL-TRANSMISSION-SIG"],
|
||||||
|
"transmission_time": required_headers["PAYPAL-TRANSMISSION-TIME"],
|
||||||
|
"webhook_id": webhook_id,
|
||||||
|
"webhook_event": json.loads(payload.decode()),
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
|
)
|
||||||
|
verify_resp.raise_for_status()
|
||||||
|
verification_status = verify_resp.json().get("verification_status")
|
||||||
|
if verification_status != "SUCCESS":
|
||||||
|
raise ValueError("PayPal webhook verification failed.")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
raise ValueError("PayPal webhook cannot be verified.") from exc
|
||||||
|
|
||||||
|
|
||||||
async def test_connection(provider: str) -> SimpleStatus:
|
async def test_connection(provider: str) -> SimpleStatus:
|
||||||
"""
|
"""
|
||||||
Test the connection to Stripe by checking if the API key is valid.
|
Test the connection to Stripe by checking if the API key is valid.
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ async def check_balance_delta_changed():
|
|||||||
if settings.latest_balance_delta_sats is None:
|
if settings.latest_balance_delta_sats is None:
|
||||||
settings.latest_balance_delta_sats = status.delta_sats
|
settings.latest_balance_delta_sats = status.delta_sats
|
||||||
return
|
return
|
||||||
if status.delta_sats != settings.latest_balance_delta_sats:
|
delta_change = abs(status.delta_sats - settings.latest_balance_delta_sats)
|
||||||
|
if delta_change >= settings.notification_balance_delta_threshold_sats:
|
||||||
enqueue_admin_notification(
|
enqueue_admin_notification(
|
||||||
NotificationType.balance_delta,
|
NotificationType.balance_delta,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import smtplib
|
import smtplib
|
||||||
|
from asyncio.tasks import create_task
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
@@ -16,6 +17,7 @@ from lnbits.core.crud import (
|
|||||||
mark_webhook_sent,
|
mark_webhook_sent,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import get_user
|
from lnbits.core.crud.users import get_user
|
||||||
|
from lnbits.core.crud.wallets import get_wallet
|
||||||
from lnbits.core.models import Payment, Wallet
|
from lnbits.core.models import Payment, Wallet
|
||||||
from lnbits.core.models.notifications import (
|
from lnbits.core.models.notifications import (
|
||||||
NOTIFICATION_TEMPLATES,
|
NOTIFICATION_TEMPLATES,
|
||||||
@@ -240,6 +242,10 @@ async def dispatch_webhook(payment: Payment):
|
|||||||
async with httpx.AsyncClient(headers=headers) as client:
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
try:
|
try:
|
||||||
check_callback_url(payment.webhook)
|
check_callback_url(payment.webhook)
|
||||||
|
except ValueError as exc:
|
||||||
|
await mark_webhook_sent(payment.payment_hash, "-1")
|
||||||
|
logger.warning(f"Invalid webhook URL {payment.webhook}: {exc!s}")
|
||||||
|
try:
|
||||||
r = await client.post(payment.webhook, json=payment.json(), timeout=40)
|
r = await client.post(payment.webhook, json=payment.json(), timeout=40)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
await mark_webhook_sent(payment.payment_hash, str(r.status_code))
|
await mark_webhook_sent(payment.payment_hash, str(r.status_code))
|
||||||
@@ -257,6 +263,12 @@ async def dispatch_webhook(payment: Payment):
|
|||||||
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
try:
|
try:
|
||||||
await send_ws_payment_notification(wallet, payment)
|
await send_ws_payment_notification(wallet, payment)
|
||||||
|
for shared in wallet.extra.shared_with:
|
||||||
|
if not shared.shared_with_wallet_id:
|
||||||
|
continue
|
||||||
|
shared_wallet = await get_wallet(shared.shared_with_wallet_id)
|
||||||
|
if shared_wallet and shared_wallet.can_view_payments:
|
||||||
|
await send_ws_payment_notification(shared_wallet, payment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending websocket payment notification {e!s}")
|
logger.error(f"Error sending websocket payment notification {e!s}")
|
||||||
try:
|
try:
|
||||||
@@ -275,6 +287,13 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
logger.error(f"Error dispatching webhook: {e!s}")
|
logger.error(f"Error dispatching webhook: {e!s}")
|
||||||
|
|
||||||
|
|
||||||
|
def send_payment_notification_in_background(wallet: Wallet, payment: Payment):
|
||||||
|
try:
|
||||||
|
create_task(send_payment_notification(wallet, payment))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error sending payment notification: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
# TODO: websocket message should be a clean payment model
|
# TODO: websocket message should be a clean payment model
|
||||||
# await websocket_manager.send(wallet.inkey, payment.json())
|
# await websocket_manager.send(wallet.inkey, payment.json())
|
||||||
@@ -381,7 +400,7 @@ def _is_message_type_enabled(message_type: NotificationType) -> bool:
|
|||||||
if message_type == NotificationType.watchdog_check:
|
if message_type == NotificationType.watchdog_check:
|
||||||
return settings.lnbits_notification_watchdog
|
return settings.lnbits_notification_watchdog
|
||||||
if message_type == NotificationType.balance_delta:
|
if message_type == NotificationType.balance_delta:
|
||||||
return settings.notification_balance_delta_changed
|
return settings.notification_balance_delta_threshold_sats > 0
|
||||||
if message_type == NotificationType.server_start_stop:
|
if message_type == NotificationType.server_start_stop:
|
||||||
return settings.lnbits_notification_server_start_stop
|
return settings.lnbits_notification_server_start_stop
|
||||||
if message_type == NotificationType.server_status:
|
if message_type == NotificationType.server_status:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ from ..models import (
|
|||||||
PaymentState,
|
PaymentState,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from .notifications import send_payment_notification
|
from .notifications import send_payment_notification_in_background
|
||||||
|
|
||||||
payment_lock = asyncio.Lock()
|
payment_lock = asyncio.Lock()
|
||||||
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
||||||
@@ -61,11 +61,13 @@ async def pay_invoice(
|
|||||||
extra: dict | None = None,
|
extra: dict | None = None,
|
||||||
description: str = "",
|
description: str = "",
|
||||||
tag: str = "",
|
tag: str = "",
|
||||||
|
labels: list[str] | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
if settings.lnbits_only_allow_incoming_payments:
|
if settings.lnbits_only_allow_incoming_payments:
|
||||||
raise PaymentError("Only incoming payments allowed.", status="failed")
|
raise PaymentError("Only incoming payments allowed.", status="failed")
|
||||||
invoice = _validate_payment_request(payment_request, max_sat)
|
invoice = _validate_payment_request(payment_request, max_sat)
|
||||||
|
|
||||||
if not invoice.amount_msat:
|
if not invoice.amount_msat:
|
||||||
raise ValueError("Missig invoice amount.")
|
raise ValueError("Missig invoice amount.")
|
||||||
|
|
||||||
@@ -73,25 +75,34 @@ async def pay_invoice(
|
|||||||
amount_msat = invoice.amount_msat
|
amount_msat = invoice.amount_msat
|
||||||
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
|
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
|
||||||
|
|
||||||
|
if not wallet.can_send_payments:
|
||||||
|
raise PaymentError(
|
||||||
|
"Wallet does not have permission to pay invoices.",
|
||||||
|
status="failed",
|
||||||
|
)
|
||||||
|
|
||||||
if await is_internal_status_success(invoice.payment_hash, new_conn):
|
if await is_internal_status_success(invoice.payment_hash, new_conn):
|
||||||
raise PaymentError("Internal invoice already paid.", status="failed")
|
raise PaymentError("Internal invoice already paid.", status="failed")
|
||||||
|
|
||||||
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
|
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet.source_wallet_id,
|
||||||
bolt11=payment_request,
|
bolt11=payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
amount_msat=-amount_msat,
|
amount_msat=-amount_msat,
|
||||||
expiry=invoice.expiry_date,
|
expiry=invoice.expiry_date,
|
||||||
memo=description or invoice.description or "",
|
memo=description or invoice.description or "",
|
||||||
extra=extra,
|
extra=extra,
|
||||||
|
labels=labels,
|
||||||
)
|
)
|
||||||
|
|
||||||
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
|
||||||
|
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
await _credit_service_fee_wallet(wallet, payment, new_conn)
|
payment = await _pay_invoice(
|
||||||
|
wallet.source_wallet_id, create_payment_model, conn=new_conn
|
||||||
|
)
|
||||||
|
|
||||||
|
await _credit_service_fee_wallet(wallet, payment, conn=new_conn)
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
@@ -190,19 +201,22 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
|||||||
# do not save memo if description_hash or unhashed_description is set
|
# do not save memo if description_hash or unhashed_description is set
|
||||||
memo = ""
|
memo = ""
|
||||||
|
|
||||||
payment = await create_invoice(
|
async with db.connect() as conn:
|
||||||
wallet_id=wallet_id,
|
payment = await create_invoice(
|
||||||
amount=data.amount,
|
wallet_id=wallet_id,
|
||||||
memo=memo,
|
amount=data.amount,
|
||||||
currency=data.unit,
|
memo=memo,
|
||||||
description_hash=description_hash,
|
currency=data.unit,
|
||||||
unhashed_description=unhashed_description,
|
description_hash=description_hash,
|
||||||
expiry=data.expiry,
|
unhashed_description=unhashed_description,
|
||||||
extra=data.extra,
|
expiry=data.expiry,
|
||||||
webhook=data.webhook,
|
extra=data.extra,
|
||||||
internal=data.internal,
|
webhook=data.webhook,
|
||||||
payment_hash=data.payment_hash,
|
internal=data.internal,
|
||||||
)
|
payment_hash=data.payment_hash,
|
||||||
|
labels=data.labels,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
|
||||||
if data.lnurl_withdraw:
|
if data.lnurl_withdraw:
|
||||||
try:
|
try:
|
||||||
@@ -241,6 +255,7 @@ async def create_invoice(
|
|||||||
webhook: str | None = None,
|
webhook: str | None = None,
|
||||||
internal: bool | None = False,
|
internal: bool | None = False,
|
||||||
payment_hash: str | None = None,
|
payment_hash: str | None = None,
|
||||||
|
labels: list[str] | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
if not amount > 0:
|
if not amount > 0:
|
||||||
@@ -250,6 +265,12 @@ async def create_invoice(
|
|||||||
if not user_wallet:
|
if not user_wallet:
|
||||||
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
|
|
||||||
|
if not user_wallet.can_receive_payments:
|
||||||
|
raise InvoiceError(
|
||||||
|
"Wallet does not have permission to create invoices.",
|
||||||
|
status="failed",
|
||||||
|
)
|
||||||
|
|
||||||
invoice_memo = None if description_hash else memo[:640]
|
invoice_memo = None if description_hash else memo[:640]
|
||||||
|
|
||||||
# use the fake wallet if the invoice is for internal use only
|
# use the fake wallet if the invoice is for internal use only
|
||||||
@@ -308,7 +329,7 @@ async def create_invoice(
|
|||||||
invoice = bolt11_decode(invoice_response.payment_request)
|
invoice = bolt11_decode(invoice_response.payment_request)
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=wallet_id,
|
wallet_id=user_wallet.source_wallet_id,
|
||||||
bolt11=invoice_response.payment_request,
|
bolt11=invoice_response.payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
preimage=invoice_response.preimage,
|
preimage=invoice_response.preimage,
|
||||||
@@ -318,6 +339,7 @@ async def create_invoice(
|
|||||||
extra=extra,
|
extra=extra,
|
||||||
webhook=webhook,
|
webhook=webhook,
|
||||||
fee=invoice_response.fee_msat or 0,
|
fee=invoice_response.fee_msat or 0,
|
||||||
|
labels=labels,
|
||||||
)
|
)
|
||||||
|
|
||||||
payment = await create_payment(
|
payment = await create_payment(
|
||||||
@@ -339,13 +361,15 @@ async def update_pending_payments(wallet_id: str):
|
|||||||
await update_pending_payment(payment)
|
await update_pending_payment(payment)
|
||||||
|
|
||||||
|
|
||||||
async def update_pending_payment(payment: Payment) -> Payment:
|
async def update_pending_payment(
|
||||||
|
payment: Payment, conn: Connection | None = None
|
||||||
|
) -> Payment:
|
||||||
status = await payment.check_status()
|
status = await payment.check_status()
|
||||||
if status.failed:
|
if status.failed:
|
||||||
payment.status = PaymentState.FAILED
|
payment.status = PaymentState.FAILED
|
||||||
await update_payment(payment)
|
await update_payment(payment, conn=conn)
|
||||||
elif status.success:
|
elif status.success:
|
||||||
payment = await update_payment_success_status(payment, status)
|
payment = await update_payment_success_status(payment, status, conn=conn)
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
|
||||||
@@ -456,7 +480,7 @@ async def update_wallet_balance(
|
|||||||
await create_payment(
|
await create_payment(
|
||||||
checking_id=f"internal_{payment_hash}",
|
checking_id=f"internal_{payment_hash}",
|
||||||
data=CreatePayment(
|
data=CreatePayment(
|
||||||
wallet_id=wallet.id,
|
wallet_id=wallet.source_wallet_id,
|
||||||
bolt11=bolt11,
|
bolt11=bolt11,
|
||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
amount_msat=amount * 1000,
|
amount_msat=amount * 1000,
|
||||||
@@ -475,7 +499,7 @@ async def update_wallet_balance(
|
|||||||
raise ValueError("Balance change failed, amount exceeds maximum balance.")
|
raise ValueError("Balance change failed, amount exceeds maximum balance.")
|
||||||
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:
|
||||||
payment = await create_invoice(
|
payment = await create_invoice(
|
||||||
wallet_id=wallet.id,
|
wallet_id=wallet.source_wallet_id,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
memo="Admin credit",
|
memo="Admin credit",
|
||||||
internal=True,
|
internal=True,
|
||||||
@@ -682,6 +706,7 @@ async def _pay_internal_invoice(
|
|||||||
internal_payment = await check_internal(
|
internal_payment = await check_internal(
|
||||||
create_payment_model.payment_hash, conn=conn
|
create_payment_model.payment_hash, conn=conn
|
||||||
)
|
)
|
||||||
|
|
||||||
if not internal_payment:
|
if not internal_payment:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -690,6 +715,7 @@ async def _pay_internal_invoice(
|
|||||||
internal_invoice = await get_standalone_payment(
|
internal_invoice = await get_standalone_payment(
|
||||||
internal_payment.checking_id, incoming=True, conn=conn
|
internal_payment.checking_id, incoming=True, conn=conn
|
||||||
)
|
)
|
||||||
|
|
||||||
if not internal_invoice:
|
if not internal_invoice:
|
||||||
raise PaymentError("Internal payment not found.", status="failed")
|
raise PaymentError("Internal payment not found.", status="failed")
|
||||||
|
|
||||||
@@ -711,6 +737,7 @@ async def _pay_internal_invoice(
|
|||||||
|
|
||||||
internal_id = f"internal_{create_payment_model.payment_hash}"
|
internal_id = f"internal_{create_payment_model.payment_hash}"
|
||||||
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
||||||
|
|
||||||
payment = await create_payment(
|
payment = await create_payment(
|
||||||
checking_id=internal_id,
|
checking_id=internal_id,
|
||||||
data=create_payment_model,
|
data=create_payment_model,
|
||||||
@@ -725,7 +752,7 @@ async def _pay_internal_invoice(
|
|||||||
await update_payment(internal_payment, conn=conn)
|
await update_payment(internal_payment, conn=conn)
|
||||||
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
||||||
|
|
||||||
await send_payment_notification(wallet, payment)
|
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
||||||
|
|
||||||
# notify receiver asynchronously
|
# notify receiver asynchronously
|
||||||
from lnbits.tasks import internal_invoice_queue
|
from lnbits.tasks import internal_invoice_queue
|
||||||
@@ -798,7 +825,8 @@ async def _pay_external_invoice(
|
|||||||
payment = await update_payment_success_status(
|
payment = await update_payment_success_status(
|
||||||
payment, payment_response, conn=conn
|
payment, payment_response, conn=conn
|
||||||
)
|
)
|
||||||
await send_payment_notification(wallet, payment)
|
|
||||||
|
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
||||||
logger.success(f"payment successful {payment_response.checking_id}")
|
logger.success(f"payment successful {payment_response.checking_id}")
|
||||||
|
|
||||||
payment.checking_id = payment_response.checking_id
|
payment.checking_id = payment_response.checking_id
|
||||||
@@ -910,7 +938,7 @@ async def _credit_service_fee_wallet(
|
|||||||
|
|
||||||
memo = f"""
|
memo = f"""
|
||||||
Service fee for payment of {abs(payment.sat)} sats.
|
Service fee for payment of {abs(payment.sat)} sats.
|
||||||
Wallet: '{wallet.name}' ({wallet.id})."""
|
Wallet: '{wallet.name}' ({wallet.source_wallet_id})."""
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=settings.lnbits_service_fee_wallet,
|
wallet_id=settings.lnbits_service_fee_wallet,
|
||||||
@@ -1006,3 +1034,13 @@ async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
|
|||||||
await update_payment(payment)
|
await update_payment(payment)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_payment_notification_in_background(
|
||||||
|
wallet_id: str, payment: Payment, conn: Connection | None = None
|
||||||
|
):
|
||||||
|
# fetch balance again
|
||||||
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
|
if not wallet:
|
||||||
|
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
|
send_payment_notification_in_background(wallet, payment)
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.extensions import UserExtension
|
from lnbits.core.models.extensions import UserExtension
|
||||||
|
from lnbits.db import Connection
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
SuperSettings,
|
SuperSettings,
|
||||||
@@ -48,37 +50,43 @@ async def create_user_account_no_ckeck(
|
|||||||
account: Account | None = None,
|
account: Account | None = None,
|
||||||
wallet_name: str | None = None,
|
wallet_name: str | None = None,
|
||||||
default_exts: list[str] | None = None,
|
default_exts: list[str] | None = None,
|
||||||
|
conn: Connection | None = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
|
if account:
|
||||||
|
account.validate_fields()
|
||||||
|
if account.username and await get_account_by_username(
|
||||||
|
account.username, conn=conn
|
||||||
|
):
|
||||||
|
raise ValueError("Username already exists.")
|
||||||
|
|
||||||
if account:
|
if account.email and await get_account_by_email(account.email, conn=conn):
|
||||||
account.validate_fields()
|
raise ValueError("Email already exists.")
|
||||||
if account.username and await get_account_by_username(account.username):
|
|
||||||
raise ValueError("Username already exists.")
|
|
||||||
|
|
||||||
if account.email and await get_account_by_email(account.email):
|
if account.pubkey and await get_account_by_pubkey(
|
||||||
raise ValueError("Email already exists.")
|
account.pubkey, conn=conn
|
||||||
|
):
|
||||||
|
raise ValueError("Pubkey already exists.")
|
||||||
|
|
||||||
if account.pubkey and await get_account_by_pubkey(account.pubkey):
|
if not account.id:
|
||||||
raise ValueError("Pubkey already exists.")
|
account.id = uuid4().hex
|
||||||
|
|
||||||
if not account.id:
|
account = await create_account(account, conn=conn)
|
||||||
account.id = uuid4().hex
|
await create_wallet(
|
||||||
|
user_id=account.id,
|
||||||
|
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
|
||||||
account = await create_account(account)
|
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
|
||||||
await create_wallet(
|
for ext_id in user_extensions:
|
||||||
user_id=account.id,
|
try:
|
||||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
|
||||||
)
|
await create_user_extension(user_ext, conn=conn)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error enabeling default extension {ext_id}: {e}")
|
||||||
|
|
||||||
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
|
user = await get_user_from_account(account, conn=conn)
|
||||||
for ext_id in user_extensions:
|
|
||||||
try:
|
|
||||||
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
|
|
||||||
await create_user_extension(user_ext)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error enabeling default extension {ext_id}: {e}")
|
|
||||||
|
|
||||||
user = await get_user_from_account(account)
|
|
||||||
if not user:
|
if not user:
|
||||||
raise ValueError("Cannot find user for account.")
|
raise ValueError("Cannot find user for account.")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
from lnbits.core.crud.users import (
|
||||||
|
get_account,
|
||||||
|
get_account_by_username_or_email,
|
||||||
|
update_account,
|
||||||
|
)
|
||||||
|
from lnbits.core.crud.wallets import (
|
||||||
|
create_wallet,
|
||||||
|
force_delete_wallet,
|
||||||
|
get_standalone_wallet,
|
||||||
|
get_wallet,
|
||||||
|
get_wallets,
|
||||||
|
update_wallet,
|
||||||
|
)
|
||||||
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
|
from lnbits.core.models.users import Account
|
||||||
|
from lnbits.core.models.wallets import (
|
||||||
|
Wallet,
|
||||||
|
WalletSharePermission,
|
||||||
|
WalletShareStatus,
|
||||||
|
WalletType,
|
||||||
|
)
|
||||||
|
from lnbits.db import Connection
|
||||||
|
from lnbits.helpers import sha256s
|
||||||
|
|
||||||
|
|
||||||
|
async def invite_to_wallet(
|
||||||
|
source_wallet: Wallet, data: WalletSharePermission
|
||||||
|
) -> WalletSharePermission:
|
||||||
|
if not source_wallet.is_lightning_wallet:
|
||||||
|
raise ValueError("Only lightning wallets can be shared.")
|
||||||
|
if not data.username:
|
||||||
|
raise ValueError("Username or email missing.")
|
||||||
|
invited_user = await get_account_by_username_or_email(data.username)
|
||||||
|
if not invited_user:
|
||||||
|
raise ValueError("Invited user not found.")
|
||||||
|
|
||||||
|
request_id = sha256s(invited_user.id + source_wallet.id)
|
||||||
|
share = source_wallet.extra.find_share_by_id(request_id)
|
||||||
|
if share:
|
||||||
|
raise ValueError("User already invited to this wallet.")
|
||||||
|
|
||||||
|
invite_request = source_wallet.extra.invite_user_to_shared_wallet(
|
||||||
|
request_id=request_id,
|
||||||
|
request_type=WalletShareStatus.INVITE_SENT,
|
||||||
|
username=data.username,
|
||||||
|
permissions=data.permissions,
|
||||||
|
)
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
|
||||||
|
wallet_owner = await get_account(source_wallet.user)
|
||||||
|
if not wallet_owner:
|
||||||
|
raise ValueError("Cannot find wallet owner.")
|
||||||
|
invited_user.extra.add_wallet_invite_request(
|
||||||
|
request_id=request_id,
|
||||||
|
from_user_name=wallet_owner.username or wallet_owner.email,
|
||||||
|
to_wallet_id=source_wallet.id,
|
||||||
|
to_wallet_name=source_wallet.name,
|
||||||
|
)
|
||||||
|
await update_account(invited_user)
|
||||||
|
|
||||||
|
return invite_request
|
||||||
|
|
||||||
|
|
||||||
|
async def reject_wallet_invitation(invited_user_id: str, share_request_id: str):
|
||||||
|
invited_user = await get_account(invited_user_id)
|
||||||
|
if not invited_user:
|
||||||
|
raise ValueError("Invited user not found.")
|
||||||
|
|
||||||
|
existing_request = invited_user.extra.find_wallet_invite_request(share_request_id)
|
||||||
|
if not existing_request:
|
||||||
|
raise ValueError("Invitation not found.")
|
||||||
|
|
||||||
|
invited_user.extra.remove_wallet_invite_request(share_request_id)
|
||||||
|
await update_account(invited_user)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_wallet_share_permissions(
|
||||||
|
source_wallet: Wallet, data: WalletSharePermission
|
||||||
|
) -> WalletSharePermission:
|
||||||
|
if not source_wallet.is_lightning_wallet:
|
||||||
|
raise ValueError("Only lightning wallets can be shared.")
|
||||||
|
if not data.shared_with_wallet_id:
|
||||||
|
raise ValueError("Wallet ID missing.")
|
||||||
|
|
||||||
|
share = source_wallet.extra.find_share_for_wallet(data.shared_with_wallet_id)
|
||||||
|
if not share:
|
||||||
|
raise ValueError("Share not found")
|
||||||
|
|
||||||
|
if not share.shared_with_wallet_id:
|
||||||
|
raise ValueError("Share does not have a mirror wallet ID.")
|
||||||
|
|
||||||
|
mirror_wallet = await get_wallet(share.shared_with_wallet_id)
|
||||||
|
if not mirror_wallet:
|
||||||
|
raise ValueError("Target wallet not found")
|
||||||
|
if not mirror_wallet.is_lightning_shared_wallet:
|
||||||
|
raise ValueError("Target wallet is not a shared wallet.")
|
||||||
|
if mirror_wallet.shared_wallet_id != source_wallet.id:
|
||||||
|
raise ValueError("Not the owner of the shared wallet.")
|
||||||
|
|
||||||
|
share.approve(permissions=data.permissions)
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
return share
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_wallet_share(source_wallet: Wallet, request_id: str) -> SimpleStatus:
|
||||||
|
if not source_wallet.is_lightning_wallet:
|
||||||
|
raise ValueError("Source wallet is not a lightning wallet.")
|
||||||
|
|
||||||
|
share = source_wallet.extra.find_share_by_id(request_id)
|
||||||
|
if not share:
|
||||||
|
raise ValueError("Wallet share not found.")
|
||||||
|
source_wallet.extra.remove_share_by_id(request_id)
|
||||||
|
|
||||||
|
invited_user = await get_account_by_username_or_email(share.username)
|
||||||
|
if not invited_user:
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True, message="Permission removed. Invited user not found."
|
||||||
|
)
|
||||||
|
if invited_user.extra.find_wallet_invite_request(request_id):
|
||||||
|
invited_user.extra.remove_wallet_invite_request(request_id)
|
||||||
|
await update_account(invited_user)
|
||||||
|
|
||||||
|
mirror_wallets = await get_wallets(
|
||||||
|
invited_user.id, wallet_type=WalletType.LIGHTNING_SHARED
|
||||||
|
)
|
||||||
|
mirror_wallet = next(
|
||||||
|
(w for w in mirror_wallets if w.shared_wallet_id == source_wallet.id), None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not mirror_wallet:
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True, message="Permission removed. Target wallet not found."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not mirror_wallet.is_lightning_shared_wallet:
|
||||||
|
raise ValueError("Target wallet is not a shared lightning wallet.")
|
||||||
|
|
||||||
|
if mirror_wallet.shared_wallet_id != source_wallet.id:
|
||||||
|
raise ValueError("Not the owner of the shared wallet.")
|
||||||
|
|
||||||
|
await force_delete_wallet(mirror_wallet.id)
|
||||||
|
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
return SimpleStatus(success=True, message="Permission removed.")
|
||||||
|
|
||||||
|
|
||||||
|
async def create_lightning_shared_wallet(
|
||||||
|
user_id: str,
|
||||||
|
source_wallet_id: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Wallet:
|
||||||
|
source_wallet = await get_standalone_wallet(source_wallet_id, conn=conn)
|
||||||
|
if not source_wallet:
|
||||||
|
raise ValueError("Shared wallet does not exist.")
|
||||||
|
|
||||||
|
if not source_wallet.is_lightning_wallet:
|
||||||
|
raise ValueError("Shared wallet is not a lightning wallet.")
|
||||||
|
|
||||||
|
if source_wallet.user == user_id:
|
||||||
|
raise ValueError("Cannot mirror your own wallet.")
|
||||||
|
|
||||||
|
invited_user = await get_account(user_id, conn=conn)
|
||||||
|
if not invited_user:
|
||||||
|
raise ValueError("Cannot find invited user.")
|
||||||
|
|
||||||
|
return await _accept_invitation_to_shared_wallet(
|
||||||
|
invited_user, source_wallet, conn=conn
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _accept_invitation_to_shared_wallet(
|
||||||
|
invited_user: Account,
|
||||||
|
source_wallet: Wallet,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Wallet:
|
||||||
|
request_id = sha256s(invited_user.id + source_wallet.id)
|
||||||
|
existing_request = source_wallet.extra.find_share_by_id(request_id)
|
||||||
|
if not existing_request:
|
||||||
|
raise ValueError("No invitation found for this invited user.")
|
||||||
|
if existing_request.status == WalletShareStatus.APPROVED:
|
||||||
|
raise ValueError("This wallet is already shared with you.")
|
||||||
|
if existing_request.status != WalletShareStatus.INVITE_SENT:
|
||||||
|
raise ValueError("Unknown request type.")
|
||||||
|
|
||||||
|
invited_user.extra.remove_wallet_invite_request(request_id)
|
||||||
|
await update_account(invited_user)
|
||||||
|
|
||||||
|
# todo: double check if user already has a mirror wallet for this source wallet
|
||||||
|
|
||||||
|
mirror_wallet = await create_wallet(
|
||||||
|
user_id=invited_user.id,
|
||||||
|
wallet_name=source_wallet.name,
|
||||||
|
wallet_type=WalletType.LIGHTNING_SHARED,
|
||||||
|
shared_wallet_id=source_wallet.id,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
existing_request.approve(shared_with_wallet_id=mirror_wallet.id)
|
||||||
|
await update_wallet(source_wallet, conn=conn)
|
||||||
|
mirror_wallet.mirror_shared_wallet(source_wallet)
|
||||||
|
return mirror_wallet
|
||||||
@@ -35,7 +35,7 @@ async def run_by_the_minute_tasks() -> None:
|
|||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
status_minutes = settings.lnbits_notification_server_status_hours * 60
|
status_minutes = settings.lnbits_notification_server_status_hours * 60
|
||||||
|
|
||||||
if settings.notification_balance_delta_changed:
|
if settings.notification_balance_delta_threshold_sats > 0:
|
||||||
try:
|
try:
|
||||||
# runs by default every minute, the delta should not change that often
|
# runs by default every minute, the delta should not change that often
|
||||||
await check_balance_delta_changed()
|
await check_balance_delta_changed()
|
||||||
@@ -57,7 +57,9 @@ async def run_by_the_minute_tasks() -> None:
|
|||||||
if minute_counter % 60 == 0:
|
if minute_counter % 60 == 0:
|
||||||
try:
|
try:
|
||||||
# initialize the list of all extensions
|
# initialize the list of all extensions
|
||||||
await InstallableExtension.get_installable_extensions()
|
await InstallableExtension.get_installable_extensions(
|
||||||
|
post_refresh_cache=True
|
||||||
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error(ex)
|
logger.error(ex)
|
||||||
|
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
<q-tab-panel name="exchange_providers">
|
|
||||||
<h6 class="q-my-none q-mb-sm">
|
|
||||||
<span v-text="$t('exchange_providers')"></span>
|
|
||||||
</h6>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8 col-sm-12">
|
|
||||||
<div class="q-pa-sm">
|
|
||||||
<canvas
|
|
||||||
style="
|
|
||||||
width: 100% !important;
|
|
||||||
height: auto !important;
|
|
||||||
min-height: 350px;
|
|
||||||
max-height: 50vh;
|
|
||||||
"
|
|
||||||
ref="exchangeRatesChart"
|
|
||||||
></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
|
|
||||||
type="number"
|
|
||||||
label="Refresh Interval (seconds)"
|
|
||||||
hint="How often should the exchange rates be fetched. Set to zero to disable."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_exchange_history_size"
|
|
||||||
type="number"
|
|
||||||
label="History Size"
|
|
||||||
hint="How many data points should be kept in memory."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<code>Refresh Interval</code> and <code> History Size </code>are for
|
|
||||||
historical purposes only.
|
|
||||||
</li>
|
|
||||||
<li>These two settings do not affect the live price computation.</li>
|
|
||||||
<li>
|
|
||||||
Chart currency:
|
|
||||||
<strong
|
|
||||||
><span
|
|
||||||
v-text="formData.lnbits_default_accounting_currency || 'USD'"
|
|
||||||
></span
|
|
||||||
></strong>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-mt-md">
|
|
||||||
<div class="col-6">
|
|
||||||
<q-btn
|
|
||||||
@click="addExchangeProvider()"
|
|
||||||
label="Add Exchange Provider"
|
|
||||||
color="primary"
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<q-btn
|
|
||||||
@click="getDefaultSetting('lnbits_exchange_rate_providers')"
|
|
||||||
flat
|
|
||||||
:label="$t('reset_defaults')"
|
|
||||||
color="primary"
|
|
||||||
class="float-right"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-table
|
|
||||||
row-key="name"
|
|
||||||
:rows="formData.lnbits_exchange_rate_providers"
|
|
||||||
:columns="exchangesTable.columns"
|
|
||||||
v-model:pagination="exchangesTable.pagination"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
<span v-text="col.label"></span>
|
|
||||||
</q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td>
|
|
||||||
<q-btn
|
|
||||||
@click="removeExchangeProvider(props.row)"
|
|
||||||
round
|
|
||||||
icon="delete"
|
|
||||||
size="sm"
|
|
||||||
color="negative"
|
|
||||||
class="q-ml-xs"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
|
||||||
<q-td>
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
v-model="props.row.name"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
type="text"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</q-td>
|
|
||||||
<q-td full-width>
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
v-model="props.row.api_url"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
type="text"
|
|
||||||
>
|
|
||||||
</q-input
|
|
||||||
></q-td>
|
|
||||||
<q-td>
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
v-model="props.row.path"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
type="text"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</q-td>
|
|
||||||
<q-td>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="props.row.exclude_to"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
multiple
|
|
||||||
:options="{{ currencies | safe }}"
|
|
||||||
></q-select>
|
|
||||||
</q-td>
|
|
||||||
<q-td>
|
|
||||||
<q-btn
|
|
||||||
@click="showTickerConversionDialog(props.row)"
|
|
||||||
round
|
|
||||||
icon="add"
|
|
||||||
size="sm"
|
|
||||||
color="gray"
|
|
||||||
class="q-ml-xs"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
<q-chip
|
|
||||||
v-for="ticker, index in props.row.ticker_conversion"
|
|
||||||
:key="ticker"
|
|
||||||
removable
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
@remove="removeExchangeTickerConversion(props.row, ticker)"
|
|
||||||
color="primary"
|
|
||||||
text-color="white"
|
|
||||||
:label="ticker"
|
|
||||||
class="ellipsis"
|
|
||||||
>
|
|
||||||
</q-chip>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<code>API URL</code> and <code>JSON Path</code> fields can use the
|
|
||||||
<code>{to}</code> and <code>{TO}</code> placeholders for the code of the
|
|
||||||
currency
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>{TO}</code> is the uppercase code and <code>{to}</code> is the
|
|
||||||
lowercase code
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<q-separator class="q-ma-md"></q-separator>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_exchange_rate_cache_seconds"
|
|
||||||
type="number"
|
|
||||||
label="Exchange rate cache (seconds)"
|
|
||||||
hint="For how many seconds should the exchange rate be cached."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8 col-sm-12"></div>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
<q-tab-panel name="fiat_providers">
|
|
||||||
<h6 class="q-my-none q-mb-sm">
|
|
||||||
<span v-text="$t('fiat_providers')"></span>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
flat
|
|
||||||
@click="hideInputsToggle()"
|
|
||||||
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
|
|
||||||
></q-btn>
|
|
||||||
</h6>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<q-list bordered class="rounded-borders">
|
|
||||||
<q-expansion-item header-class="text-primary text-bold">
|
|
||||||
<template v-slot:header>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar>
|
|
||||||
<img
|
|
||||||
:src="'{{ static_url_for('static', 'images/stripe_logo.ico') }}'"
|
|
||||||
/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section> Stripe </q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
:label="$t('enabled')"
|
|
||||||
v-model="formData.stripe_enabled"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<q-card class="q-pb-xl">
|
|
||||||
<q-expansion-item :label="$t('api')" default-opened>
|
|
||||||
<q-card-section class="q-pa-md">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="text"
|
|
||||||
v-model="formData.stripe_api_endpoint"
|
|
||||||
:label="$t('endpoint')"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-mt-md"
|
|
||||||
:type="hideInputToggle ? 'password' : 'text'"
|
|
||||||
v-model="formData.stripe_api_secret_key"
|
|
||||||
:label="$t('secret_key')"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-mt-md"
|
|
||||||
type="text"
|
|
||||||
v-model="formData.stripe_payment_success_url"
|
|
||||||
:label="$t('callback_success_url')"
|
|
||||||
:hint="$t('callback_success_url_hint')"
|
|
||||||
></q-input>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="q-pa-md">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
class="float-right"
|
|
||||||
:label="$t('check_connection')"
|
|
||||||
@click="checkFiatProvider('stripe')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item :label="$t('webhook')" default-opened>
|
|
||||||
<q-card-section>
|
|
||||||
<span v-text="$t('webhook_stripe_description')"></span>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-mt-md"
|
|
||||||
type="text"
|
|
||||||
disable
|
|
||||||
v-model="formData.stripe_payment_webhook_url"
|
|
||||||
:label="$t('webhook_url')"
|
|
||||||
:hint="$t('webhook_url_hint')"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-mt-md"
|
|
||||||
:type="hideInputToggle ? 'password' : 'text'"
|
|
||||||
v-model="formData.stripe_webhook_signing_secret"
|
|
||||||
:label="$t('signing_secret')"
|
|
||||||
:hint="$t('signing_secret_hint')"
|
|
||||||
></q-input>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<span v-text="$t('webhook_events_list')"></span>
|
|
||||||
<ul>
|
|
||||||
<li><code>checkout.session.completed</code></li>
|
|
||||||
- the user completed the checkout process
|
|
||||||
<li><code>invoice.paid</code></li>
|
|
||||||
- the invoice was successfully paid (for subscriptions)
|
|
||||||
</ul>
|
|
||||||
</q-card-section>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item :label="$t('service_fee')">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-ma-sm"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
v-model="formData.stripe_limits.service_fee_percent"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
:label="$t('service_fee_label')"
|
|
||||||
:hint="$t('service_fee_hint')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-ma-sm"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
v-model="formData.stripe_limits.service_max_fee_sats"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
:label="$t('service_fee_max')"
|
|
||||||
:hint="$t('service_fee_max_hint')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-ma-sm"
|
|
||||||
type="text"
|
|
||||||
v-model="formData.stripe_limits.service_fee_wallet_id"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
:label="$t('fee_wallet_label')"
|
|
||||||
:hint="$t('fee_wallet_hint')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item :label="$t('amount_limits')">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-ma-sm"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
v-model="formData.stripe_limits.service_min_amount_sats"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
:label="$t('min_incoming_payment_amount')"
|
|
||||||
:hint="$t('min_incoming_payment_amount_desc')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-ma-sm"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
v-model="formData.stripe_limits.service_max_amount_sats"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
:label="$t('max_incoming_payment_amount')"
|
|
||||||
:hint="$t('max_incoming_payment_amount_desc')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-ma-sm"
|
|
||||||
v-model="formData.stripe_limits.service_faucet_wallet_id"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
:label="$t('faucest_wallet_id')"
|
|
||||||
:hint="$t('faucest_wallet_id_hint')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('faucest_wallet')"></q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<span
|
|
||||||
v-text="$t('faucest_wallet_desc_1', {provider: 'stripe'})"
|
|
||||||
></span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span
|
|
||||||
v-text="$t('faucest_wallet_desc_2', {provider: 'stripe'})"
|
|
||||||
></span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span v-text="$t('faucest_wallet_desc_3')"></span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span
|
|
||||||
v-text="$t('faucest_wallet_desc_4', {provider: 'stripe'})"
|
|
||||||
></span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span v-text="$t('faucest_wallet_desc_5')"></span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<br />
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-card-section>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item :label="$t('allowed_users')">
|
|
||||||
<q-card-section>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formAddStripeUser"
|
|
||||||
@keydown.enter="addAllowedUser"
|
|
||||||
type="text"
|
|
||||||
:label="$t('allowed_users_label')"
|
|
||||||
:hint="$t('allowed_users_hint_feature', {feature: 'Stripe'})"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="addStripeAllowedUser"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
icon="add"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
<div>
|
|
||||||
<q-chip
|
|
||||||
v-for="user in formData.stripe_limits.allowed_users"
|
|
||||||
@update:model-value="touchSettings()"
|
|
||||||
:key="user"
|
|
||||||
removable
|
|
||||||
@remove="removeStripeAllowedUser(user)"
|
|
||||||
color="primary"
|
|
||||||
text-color="white"
|
|
||||||
:label="user"
|
|
||||||
class="ellipsis"
|
|
||||||
>
|
|
||||||
</q-chip>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-separator />
|
|
||||||
|
|
||||||
<q-expansion-item header-class="text-primary text-bold">
|
|
||||||
<template v-slot:header>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar>
|
|
||||||
<img
|
|
||||||
:src="'{{ static_url_for('static', 'images/square_logo.png') }}'"
|
|
||||||
/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section> Square </q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<div class="row items-center">Disabled</div>
|
|
||||||
</q-item-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<q-card>
|
|
||||||
<q-card-section> Coming Soon </q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-list>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<q-tab-panel name="library">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<h6 class="q-my-none q-mb-sm">
|
|
||||||
<span v-text="$t('image_library')"></span>
|
|
||||||
</h6>
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
label="Add Image"
|
|
||||||
@click="$refs.imageInput.click()"
|
|
||||||
class="q-mb-md"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref="imageInput"
|
|
||||||
accept="image/png, image/jpeg, image/gif"
|
|
||||||
style="display: none"
|
|
||||||
@change="onImageInput"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
<div class="row q-col-gutter-sm q-pa-sm">
|
|
||||||
<div
|
|
||||||
v-for="image in library_images"
|
|
||||||
:key="image.filename"
|
|
||||||
class="col-6 col-sm-4 col-md-3 col-lg-2"
|
|
||||||
style="max-width: 200px"
|
|
||||||
>
|
|
||||||
<q-card class="q-mb-sm">
|
|
||||||
<q-img :src="image.url" style="height: 150px" />
|
|
||||||
|
|
||||||
<q-card-section
|
|
||||||
class="q-pt-md q-pb-md row items-center justify-between"
|
|
||||||
>
|
|
||||||
<small
|
|
||||||
><div
|
|
||||||
class="text-caption ellipsis"
|
|
||||||
style="max-width: 100px"
|
|
||||||
:title="image.filename"
|
|
||||||
v-text="image.filename"
|
|
||||||
></div
|
|
||||||
></small>
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
size="sm"
|
|
||||||
icon="content_copy"
|
|
||||||
@click="copyText(image.url)"
|
|
||||||
:title="$t('copy')"
|
|
||||||
><q-tooltip>Copy image link</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
size="sm"
|
|
||||||
icon="delete"
|
|
||||||
color="negative"
|
|
||||||
@click="deleteImage(image.filename)"
|
|
||||||
:title="$t('delete')"
|
|
||||||
><q-tooltip>Delete image</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="library_images.length === 0" class="q-pa-xl">
|
|
||||||
<div class="text-subtitle2 text-grey">No images uploaded yet.</div>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %} {% from "macros.jinja"
|
|
||||||
import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|
||||||
{% endblock %} {% block page %}
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-card>
|
|
||||||
<div class="q-pa-sm">
|
|
||||||
<div class="row items-center justify-between q-gutter-xs">
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
|
||||||
:label="$t('save')"
|
|
||||||
color="primary"
|
|
||||||
@click="updateSettings"
|
|
||||||
:disabled="!checkChanges"
|
|
||||||
>
|
|
||||||
<q-tooltip v-if="checkChanges">
|
|
||||||
<span v-text="$t('save_tooltip')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
|
|
||||||
<q-badge
|
|
||||||
v-if="checkChanges"
|
|
||||||
color="red"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
style="padding: 6px; border-radius: 6px"
|
|
||||||
/>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-if="isSuperUser"
|
|
||||||
:label="$t('restart')"
|
|
||||||
color="primary"
|
|
||||||
@click="restartServer"
|
|
||||||
class="q-ml-md"
|
|
||||||
>
|
|
||||||
<q-tooltip v-if="needsRestart">
|
|
||||||
<span v-text="$t('restart_tooltip')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
|
|
||||||
<q-badge
|
|
||||||
v-if="needsRestart"
|
|
||||||
color="red"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
style="padding: 6px; border-radius: 6px"
|
|
||||||
/>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
:label="$t('download_backup')"
|
|
||||||
flat
|
|
||||||
@click="downloadBackup"
|
|
||||||
></q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
v-if="isSuperUser"
|
|
||||||
:label="$t('reset_defaults')"
|
|
||||||
color="primary"
|
|
||||||
@click="deleteSettings"
|
|
||||||
class="float-right"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="$t('reset_defaults_tooltip')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
|
||||||
<div class="col q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-splitter>
|
|
||||||
<template v-slot:before>
|
|
||||||
<q-tabs
|
|
||||||
@update:model-value="showExchangeProvidersTab"
|
|
||||||
v-model="tab"
|
|
||||||
vertical
|
|
||||||
active-color="primary"
|
|
||||||
>
|
|
||||||
<q-tab
|
|
||||||
name="funding"
|
|
||||||
icon="account_balance_wallet"
|
|
||||||
:label="$q.screen.gt.sm ? $t('funding') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('funding')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="security"
|
|
||||||
icon="security"
|
|
||||||
:label="$q.screen.gt.sm ? $t('security') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('security')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="server"
|
|
||||||
icon="price_change"
|
|
||||||
:label="$q.screen.gt.sm ? $t('payments') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('payments')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="exchange_providers"
|
|
||||||
icon="show_chart"
|
|
||||||
:label="$q.screen.gt.sm ? $t('exchanges') : null"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('exchanges')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="fiat_providers"
|
|
||||||
icon="credit_score"
|
|
||||||
:label="$q.screen.gt.sm ? $t('fiat_providers') : null"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('fiat_providers')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="users"
|
|
||||||
icon="group"
|
|
||||||
:label="$q.screen.gt.sm ? $t('users') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('users')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
|
|
||||||
<q-tab
|
|
||||||
name="extensions"
|
|
||||||
icon="extension"
|
|
||||||
:label="$q.screen.gt.sm ? $t('extensions') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('extensions')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
|
|
||||||
<q-tab
|
|
||||||
name="notifications"
|
|
||||||
icon="notifications"
|
|
||||||
:label="$q.screen.gt.sm ? $t('notifications') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('notifications')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="audit"
|
|
||||||
icon="playlist_add_check_circle"
|
|
||||||
:label="$q.screen.gt.sm ? $t('audit') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('audit')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="library"
|
|
||||||
icon="image"
|
|
||||||
:label="$q.screen.gt.sm ? $t('library') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('library')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
style="word-break: break-all"
|
|
||||||
name="site_customisation"
|
|
||||||
icon="language"
|
|
||||||
:label="$q.screen.gt.sm ? $t('site_customisation') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('site_customisation')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
</q-tabs>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:after>
|
|
||||||
<q-form name="settings_form" id="settings_form">
|
|
||||||
<q-scroll-area style="height: 100vh">
|
|
||||||
<q-tab-panels
|
|
||||||
v-model="tab"
|
|
||||||
animated
|
|
||||||
swipeable
|
|
||||||
vertical
|
|
||||||
scroll
|
|
||||||
transition-prev="jump-up"
|
|
||||||
transition-next="jump-up"
|
|
||||||
>
|
|
||||||
{% include "admin/_tab_funding.html" %} {% include
|
|
||||||
"admin/_tab_users.html" %} {% include "admin/_tab_server.html"
|
|
||||||
%} {% include "admin/_tab_exchange_providers.html" %}{% include
|
|
||||||
"admin/_tab_fiat_providers.html" %} {% include
|
|
||||||
"admin/_tab_extensions.html" %} {% include
|
|
||||||
"admin/_tab_notifications.html" %} {% include
|
|
||||||
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
|
||||||
%}{% include "admin/_tab_audit.html"%}{% include
|
|
||||||
"admin/_tab_library.html"%}
|
|
||||||
</q-tab-panels>
|
|
||||||
</q-scroll-area>
|
|
||||||
</q-form>
|
|
||||||
</template>
|
|
||||||
</q-splitter>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
|
|
||||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
|
||||||
<div class="q-mb-md">
|
|
||||||
<strong v-text="$t('create_ticker_converter')"></strong>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 q-mb-md">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="exchangeData.convertFromTicker"
|
|
||||||
label="From Currency"
|
|
||||||
:options="{{ currencies | safe }}"
|
|
||||||
></q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
v-model="exchangeData.convertToTicker"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
label="New Ticker"
|
|
||||||
hint="This ticker will be used for the exchange API calls."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
@click="addExchangeTickerConversion()"
|
|
||||||
label="Add Ticker Conversion"
|
|
||||||
color="primary"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
v-text="$t('close')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
|
||||||
<!---->
|
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
|
||||||
<!---->
|
|
||||||
{% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-card>
|
|
||||||
<div class="q-pa-sm q-pl-lg">
|
|
||||||
<div class="row items-center justify-between q-gutter-xs">
|
|
||||||
<div class="col">
|
|
||||||
<!-- Optional: Add content here if needed -->
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
v-if="g.user.admin"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
icon="settings"
|
|
||||||
to="/admin#audit"
|
|
||||||
>
|
|
||||||
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center q-mb-lg">
|
|
||||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('components')"></strong>
|
|
||||||
<div style="width: 250px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="componentUseChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('long_running_endpoints')"></strong>
|
|
||||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="longDurationChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('http_request_methods')"></strong>
|
|
||||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="requestMethodChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('http_response_codes')"></strong>
|
|
||||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="responseCodeChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
|
||||||
<div class="col">
|
|
||||||
<q-card class="q-pa-md">
|
|
||||||
<q-table
|
|
||||||
row-key="id"
|
|
||||||
:rows="auditEntries"
|
|
||||||
:columns="auditTable.columns"
|
|
||||||
v-model:pagination="auditTable.pagination"
|
|
||||||
:filter="auditTable.search"
|
|
||||||
:loading="auditTable.loading"
|
|
||||||
@request="fetchAudit"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
<q-input
|
|
||||||
v-if="['ip_address', 'user_id', 'path',].includes(col.name)"
|
|
||||||
v-model="searchData[col.name]"
|
|
||||||
@keydown.enter="searchAuditBy()"
|
|
||||||
@update:model-value="searchAuditBy()"
|
|
||||||
dense
|
|
||||||
type="text"
|
|
||||||
filled
|
|
||||||
clearable
|
|
||||||
:label="col.label"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
name="search"
|
|
||||||
@click="searchAuditBy()"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-else-if="['component', 'response_code','request_method'].includes(col.name)"
|
|
||||||
v-model="searchData[col.name]"
|
|
||||||
:options="searchOptions[col.name]"
|
|
||||||
@update:model-value="searchAuditBy()"
|
|
||||||
:label="col.label"
|
|
||||||
clearable
|
|
||||||
style="width: 100px"
|
|
||||||
></q-select>
|
|
||||||
|
|
||||||
<span v-else v-text="col.label"></span>
|
|
||||||
</q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr auto-width :props="props">
|
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
<div v-if="col.name == 'created_at'">
|
|
||||||
<q-btn
|
|
||||||
icon="description"
|
|
||||||
:disable="!props.row.request_details"
|
|
||||||
size="sm"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-mr-xs"
|
|
||||||
@click="showDetailsDialog(props.row)"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
><span v-text="$t('request_details')"></span
|
|
||||||
></q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<span v-text="formatDate(props.row.created_at)"></span>
|
|
||||||
<q-tooltip v-if="props.row.delete_at">
|
|
||||||
<span
|
|
||||||
v-text="'Will be deleted at: ' + formatDate(props.row.delete_at)"
|
|
||||||
></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="['user_id', 'request_details'].includes(col.name)"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-if="props.row[col.name]"
|
|
||||||
icon="content_copy"
|
|
||||||
size="sm"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-mr-xs"
|
|
||||||
@click="copyText(props.row[col.name])"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<span v-text="shortify(props.row[col.name])"> </span>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="props.row[col.name]"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
v-text="props.row[col.name]"
|
|
||||||
@click="searchAuditBy(col.name, props.row[col.name])"
|
|
||||||
class="cursor-pointer"
|
|
||||||
></span>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-dialog v-model="auditDetailsDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
|
||||||
<strong v-text="$t('http_request_details')"></strong>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="auditDetailsDialog.data"
|
|
||||||
type="textarea"
|
|
||||||
rows="25"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
@click="copyText(auditDetailsDialog.data)"
|
|
||||||
icon="copy_content"
|
|
||||||
color="grey"
|
|
||||||
flat
|
|
||||||
v-text="$t('copy')"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
v-text="$t('close')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
<q-expansion-item
|
|
||||||
group="extras"
|
|
||||||
icon="vpn_key"
|
|
||||||
:label="$t('api_keys_api_docs')"
|
|
||||||
:content-inset-level="0.5"
|
|
||||||
>
|
|
||||||
<q-card-section>
|
|
||||||
<q-list>
|
|
||||||
<q-item dense class="q-pa-none">
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>
|
|
||||||
<strong>Node URL: </strong><em v-text="origin"></em>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item dense class="q-pa-none">
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>
|
|
||||||
<strong>Wallet ID: </strong><em v-text="wallet.id"></em>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section side>
|
|
||||||
<q-icon
|
|
||||||
name="content_copy"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="copyText(wallet.id)"
|
|
||||||
></q-icon>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item dense class="q-pa-none">
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>
|
|
||||||
<strong>Admin key: </strong
|
|
||||||
><em
|
|
||||||
v-text="adminkeyHidden ? '****************' : wallet.adminkey"
|
|
||||||
></em>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section side>
|
|
||||||
<div>
|
|
||||||
<q-icon
|
|
||||||
:name="adminkeyHidden ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="adminkeyHidden = !adminkeyHidden"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon
|
|
||||||
name="content_copy"
|
|
||||||
class="cursor-pointer q-ml-sm"
|
|
||||||
@click="copyText(wallet.adminkey)"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
|
|
||||||
<q-popup-proxy>
|
|
||||||
<div class="q-pa-md">
|
|
||||||
<lnbits-qrcode
|
|
||||||
:value="wallet.adminkey"
|
|
||||||
:show-buttons="false"
|
|
||||||
></lnbits-qrcode>
|
|
||||||
</div>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item dense class="q-pa-none">
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>
|
|
||||||
<strong>Invoice/read key: </strong
|
|
||||||
><em v-text="inkeyHidden ? '****************' : wallet.inkey"></em>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section side>
|
|
||||||
<div>
|
|
||||||
<q-icon
|
|
||||||
:name="inkeyHidden ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="inkeyHidden = !inkeyHidden"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon
|
|
||||||
name="content_copy"
|
|
||||||
class="cursor-pointer q-ml-sm"
|
|
||||||
@click="copyText(wallet.inkey)"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
|
|
||||||
<q-popup-proxy>
|
|
||||||
<div class="q-pa-md">
|
|
||||||
<lnbits-qrcode
|
|
||||||
:value="wallet.inkey"
|
|
||||||
:show-buttons="false"
|
|
||||||
></lnbits-qrcode>
|
|
||||||
</div>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Get wallet details"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code
|
|
||||||
>{"X-Api-Key": "<i
|
|
||||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
|
||||||
></i
|
|
||||||
>"}</code
|
|
||||||
><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code
|
|
||||||
>{"id": <string>, "name": <string>, "balance":
|
|
||||||
<int>}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
|
|
||||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i
|
|
||||||
>"</code
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Create an invoice (incoming)"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code
|
|
||||||
>{"X-Api-Key": "<i
|
|
||||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
|
||||||
></i
|
|
||||||
>"}</code
|
|
||||||
><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<code
|
|
||||||
>{"out": false, "amount": <int>, "memo": <string>,
|
|
||||||
"expiry": <int>, "unit": <string>, "webhook":
|
|
||||||
<url:string>, "internal": <bool>}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 201 CREATED (application/json)
|
|
||||||
</h5>
|
|
||||||
<code
|
|
||||||
>{"payment_hash": <string>, "payment_request":
|
|
||||||
<string>}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
|
||||||
'{"out": false, "amount": <int>, "memo": <string>}' -H
|
|
||||||
"X-Api-Key:
|
|
||||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
|
||||||
"Content-type: application/json"</code
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Pay an invoice (outgoing)"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-light-green">POST</span> /api/v1/payments (reveal
|
|
||||||
admin keys
|
|
||||||
<q-icon
|
|
||||||
:name="adminkeyHidden ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="adminkeyHidden = !adminkeyHidden"
|
|
||||||
></q-icon
|
|
||||||
>)</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code
|
|
||||||
>{"X-Api-Key": "<i
|
|
||||||
v-text="adminkeyHidden ? '****************' : wallet.adminkey"
|
|
||||||
></i
|
|
||||||
>"}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<code>{"out": true, "bolt11": <string>}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 201 CREATED (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>{"payment_hash": <string>}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
|
||||||
'{"out": true, "bolt11": <string>}' -H "X-Api-Key:
|
|
||||||
<i v-text="adminkeyHidden ? '****************' : wallet.adminkey"></i
|
|
||||||
>" -H "Content-type: application/json"</code
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Decode an invoice"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-light-green">POST</span>
|
|
||||||
/api/v1/payments/decode</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<code>{"data": <string>}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 (application/json)
|
|
||||||
</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments/decode -d
|
|
||||||
'{"data": <bolt11/lnurl, string>}' -H "Content-type:
|
|
||||||
application/json"</code
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Check an invoice (incoming or outgoing)"
|
|
||||||
class="q-pb-md"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-light-blue">GET</span>
|
|
||||||
/api/v1/payments/<payment_hash></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code
|
|
||||||
>{"X-Api-Key": "<i
|
|
||||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
|
||||||
></i
|
|
||||||
>"}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>{"paid": <bool>}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET
|
|
||||||
<span v-text="baseUrl"></span>api/v1/payments/<payment_hash> -H
|
|
||||||
"X-Api-Key:
|
|
||||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
|
||||||
"Content-type: application/json"</code
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-pink">WS</span>
|
|
||||||
/api/v1/ws/<invoice_key></code
|
|
||||||
>
|
|
||||||
<h5
|
|
||||||
class="text-caption q-mt-sm q-mb-none"
|
|
||||||
v-text="$t('websocket_example')"
|
|
||||||
></h5>
|
|
||||||
<code
|
|
||||||
>wscat -c <span v-text="websocketUrl"></span>/<span
|
|
||||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
|
||||||
></span
|
|
||||||
></code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)/payments
|
|
||||||
</h5>
|
|
||||||
<code>{"balance": <int>, "payment": <object>}</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-card-section>
|
|
||||||
<p v-text="$t('reset_wallet_keys_desc')"></p>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="red-10"
|
|
||||||
@click="resetKeys()"
|
|
||||||
:label="$t('reset_wallet_keys')"
|
|
||||||
></q-btn>
|
|
||||||
</q-card-section>
|
|
||||||
</q-expansion-item>
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,830 +0,0 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
|
||||||
<!---->
|
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
|
||||||
<!---->
|
|
||||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-stepper
|
|
||||||
v-model="step"
|
|
||||||
ref="stepper"
|
|
||||||
color="primary"
|
|
||||||
animated
|
|
||||||
header-nav
|
|
||||||
class="q-pt-sm"
|
|
||||||
@update:model-value="onStepChange"
|
|
||||||
>
|
|
||||||
<q-step
|
|
||||||
:name="1"
|
|
||||||
title="Describe"
|
|
||||||
icon="info"
|
|
||||||
:done="step > 1"
|
|
||||||
style="min-height: 100px"
|
|
||||||
>
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<span class="text-h6">
|
|
||||||
Tell us something about your extension:
|
|
||||||
</span>
|
|
||||||
<ul>
|
|
||||||
<li>This is the first step, you can return and change it.</li>
|
|
||||||
<li>
|
|
||||||
The <code>`name`</code> and
|
|
||||||
<code>`sort description`</code> fields are what the users will
|
|
||||||
see when browsing the list of extensions.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
The <code>`id`</code> field is used internally and in the URL of
|
|
||||||
your extension.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- todo: add icon -->
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
label="Upload Existing config"
|
|
||||||
@click="$refs.extensionDataInput.click()"
|
|
||||||
class="q-mb-md"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref="extensionDataInput"
|
|
||||||
accept="application/json"
|
|
||||||
style="display: none"
|
|
||||||
@change="onJsonDataInput"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator class="q-mt-sm"></q-separator>
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="extensionData.name"
|
|
||||||
label="Extension Name"
|
|
||||||
hint="The name of your extension"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="extensionData.id"
|
|
||||||
label="Extension Id"
|
|
||||||
hint="Lowercase letters, numbers, and underscores only (snake_case). This will be used in the URL."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="extensionData.short_description"
|
|
||||||
label="Short Description"
|
|
||||||
hint="A short description that is shown in the extension list."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="extensionData.description"
|
|
||||||
label="Description"
|
|
||||||
hint="A detailed description of your extension."
|
|
||||||
type="textarea"
|
|
||||||
rows="3"
|
|
||||||
maxlength="1000"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<q-step
|
|
||||||
:name="2"
|
|
||||||
title="Settings"
|
|
||||||
icon="settings"
|
|
||||||
:done="step > 2"
|
|
||||||
style="min-height: 100px"
|
|
||||||
>
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
|
||||||
<div class="col-md-8 col-sm-12">
|
|
||||||
<iframe
|
|
||||||
ref="iframeStep2"
|
|
||||||
class="full-width"
|
|
||||||
height="400px"
|
|
||||||
sandbox="allow-scripts allow-same-origin"
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-btn
|
|
||||||
@click="previewExtension('settings')"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
label="Refresh Preview"
|
|
||||||
class="full-width q-mb-md"
|
|
||||||
></q-btn>
|
|
||||||
<q-toggle
|
|
||||||
v-model="extensionData.settings_data.enabled"
|
|
||||||
label="Generate Settings Fields"
|
|
||||||
size="md"
|
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Define what settings your extension will have.</li>
|
|
||||||
<li>
|
|
||||||
You can choose if each user has its own settings or if the
|
|
||||||
settings are global (set by the admin).
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator
|
|
||||||
v-if="extensionData.settings_data.enabled"
|
|
||||||
class="q-mt-sm"
|
|
||||||
></q-separator>
|
|
||||||
<div v-if="extensionData.settings_data.enabled" class="row q-mt-lg">
|
|
||||||
<div class="col-md-2 col-sm-12">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
v-model="extensionData.settings_data.type"
|
|
||||||
:options="settingsTypes"
|
|
||||||
></q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-10 col-sm-12 q-pt-sm">
|
|
||||||
<q-badge
|
|
||||||
v-if="extensionData.settings_data.type === 'user'"
|
|
||||||
outline
|
|
||||||
class="text-caption q-ml-md"
|
|
||||||
>Each user can set its own settings for this extension.</q-badge
|
|
||||||
>
|
|
||||||
<q-badge v-else outline class="text-caption q-ml-md"
|
|
||||||
>Settings are set by the admin and apply to all users of the
|
|
||||||
extension</q-badge
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="extensionData.settings_data.enabled" class="row q-mt-lg">
|
|
||||||
<div class="col-12">
|
|
||||||
<lnbits-data-fields
|
|
||||||
:fields="extensionData.settings_data.fields"
|
|
||||||
:hide-advanced="true"
|
|
||||||
></lnbits-data-fields>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<q-step
|
|
||||||
:name="3"
|
|
||||||
:done="step > 3"
|
|
||||||
title="Owner Data"
|
|
||||||
icon="list"
|
|
||||||
style="min-height: 100px"
|
|
||||||
>
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
|
||||||
<div class="col-md-8 col-sm-12">
|
|
||||||
<iframe
|
|
||||||
ref="iframeStep3"
|
|
||||||
class="full-width"
|
|
||||||
height="400px"
|
|
||||||
sandbox="allow-scripts allow-same-origin"
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-btn
|
|
||||||
@click="previewExtension('owner_data')"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
label="Refresh Preview"
|
|
||||||
class="full-width q-mb-md"
|
|
||||||
></q-btn>
|
|
||||||
<q-input
|
|
||||||
v-model="extensionData.owner_data.name"
|
|
||||||
filled
|
|
||||||
label="Owner Table Name"
|
|
||||||
hint="CamelCase name for the owner data table (e.g. Campaign, PoS, etc.)"
|
|
||||||
class="q-mb-xl"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
The owner of the extension manages this data. It can add, remove
|
|
||||||
and update instances of it.
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
Some fileds are present by default, like
|
|
||||||
<code>created_at</code>, <code>updated_at</code> and
|
|
||||||
<code>extra</code>.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<div class="col-12">
|
|
||||||
<lnbits-data-fields
|
|
||||||
:fields="extensionData.owner_data.fields"
|
|
||||||
></lnbits-data-fields>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<q-step
|
|
||||||
:name="4"
|
|
||||||
:done="step > 4"
|
|
||||||
title="Client Data"
|
|
||||||
icon="blur_linear"
|
|
||||||
style="min-height: 100px"
|
|
||||||
>
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
|
||||||
<div class="col-md-8 col-sm-12">
|
|
||||||
<iframe
|
|
||||||
ref="iframeStep4"
|
|
||||||
class="full-width"
|
|
||||||
height="400px"
|
|
||||||
sandbox="allow-scripts allow-same-origin"
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-btn
|
|
||||||
@click="previewExtension('client_data')"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
label="Refresh Preview"
|
|
||||||
class="full-width q-mb-md"
|
|
||||||
></q-btn>
|
|
||||||
<!-- <q-toggle
|
|
||||||
v-model="extensionData.client_data.enabled"
|
|
||||||
label="Generate Client Table"
|
|
||||||
disable
|
|
||||||
size="md"
|
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<br /> -->
|
|
||||||
<q-input
|
|
||||||
v-if="extensionData.client_data.enabled"
|
|
||||||
v-model="extensionData.client_data.name"
|
|
||||||
filled
|
|
||||||
label="Client Table Name"
|
|
||||||
hint="CamelCase name for the client data table (e.g. Donation, Payment, etc.)"
|
|
||||||
class="q-mb-xl"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
This data is created by users of the extension. Usually when
|
|
||||||
they submit a form or make a payment.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
The owner of the extension can view this data, but should not
|
|
||||||
modify it.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator
|
|
||||||
v-if="extensionData.client_data.enabled"
|
|
||||||
class="q-mt-sm"
|
|
||||||
></q-separator>
|
|
||||||
<div v-if="extensionData.client_data.enabled" class="row q-mt-lg">
|
|
||||||
<div class="col-12">
|
|
||||||
<lnbits-data-fields
|
|
||||||
:fields="extensionData.client_data.fields"
|
|
||||||
></lnbits-data-fields>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<q-step
|
|
||||||
:name="5"
|
|
||||||
:done="step > 5"
|
|
||||||
title="Public Pages"
|
|
||||||
icon="link"
|
|
||||||
style="min-height: 100px"
|
|
||||||
>
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
|
||||||
<div class="col-md-8 col-sm-12">
|
|
||||||
<iframe
|
|
||||||
ref="iframeStep5"
|
|
||||||
class="full-width"
|
|
||||||
height="400px"
|
|
||||||
sandbox="allow-scripts allow-same-origin"
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-12">
|
|
||||||
<q-btn
|
|
||||||
@click="previewExtension('public_page')"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
label="Refresh Preview"
|
|
||||||
class="full-width q-mb-md"
|
|
||||||
></q-btn>
|
|
||||||
<q-toggle
|
|
||||||
v-model="extensionData.public_page.has_public_page"
|
|
||||||
label="Generate Public Page"
|
|
||||||
size="md"
|
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Most extensions have a public page that can be shared (this page
|
|
||||||
will still be accessible even if you have restricted access to
|
|
||||||
your LNbits install).
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="extensionData.public_page.has_public_page">
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Public page title</q-item-label>
|
|
||||||
<q-item-label caption
|
|
||||||
>Select the field from the
|
|
||||||
<code v-text="extensionData.owner_data.name"></code>
|
|
||||||
(Owner Data) that will be used as a title for the public
|
|
||||||
page.</q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
v-model="extensionData.public_page.owner_data_fields.name"
|
|
||||||
:options="[''].concat(extensionData.owner_data.fields.map(f => f.name))"
|
|
||||||
></q-select>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Public page description</q-item-label>
|
|
||||||
<q-item-label caption
|
|
||||||
>Select the field from the
|
|
||||||
<code v-text="extensionData.owner_data.name"></code>
|
|
||||||
(Owner Data) that will be used as a description for the
|
|
||||||
public page.</q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
v-model="extensionData.public_page.owner_data_fields.description"
|
|
||||||
:options="[''].concat(extensionData.owner_data.fields.map(f => f.name))"
|
|
||||||
></q-select>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Public page inputs</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<ul class="q-pa-none q-ma-none">
|
|
||||||
<li>
|
|
||||||
Select the fields from the
|
|
||||||
<code v-text="extensionData.client_data.name"></code
|
|
||||||
> (Client Data) that will be shown as inputs in
|
|
||||||
the public page form.
|
|
||||||
</li>
|
|
||||||
<li>You can select multiple fields.</li>
|
|
||||||
<li>
|
|
||||||
A corresponding input field will be created for each
|
|
||||||
selected field.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
multiple
|
|
||||||
use-chips
|
|
||||||
v-model="extensionData.public_page.client_data_fields.public_inputs"
|
|
||||||
:options="extensionData.client_data.fields.map(f => f.name)"
|
|
||||||
></q-select>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Generate Action Button</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<ul class="q-pa-none q-ma-none">
|
|
||||||
<li>
|
|
||||||
If enabled, the public page will have a button to
|
|
||||||
perform an action (e.g. generate a payment request).
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
The action will use the selected input fields from
|
|
||||||
<code v-text="extensionData.client_data.name"></code
|
|
||||||
> (Client Data) as parameters.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
A corresponding REST API endpoint will be created.
|
|
||||||
</li>
|
|
||||||
</ul></q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
v-model="extensionData.public_page.action_fields.generate_action"
|
|
||||||
size="md"
|
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator
|
|
||||||
v-if="extensionData.public_page.action_fields.generate_action"
|
|
||||||
class="q-mt-sm"
|
|
||||||
></q-separator>
|
|
||||||
|
|
||||||
<div v-if="extensionData.public_page.action_fields.generate_action">
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Generate Payment Logic</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<ul class="q-pa-none q-ma-none">
|
|
||||||
<li>
|
|
||||||
If enabled, the endpoint will create an invoice from
|
|
||||||
the submitted data and the UI will show the QR code
|
|
||||||
with the invoice.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
A listener will be created to check for the pay event.
|
|
||||||
</li>
|
|
||||||
<li>You must map the fieds.</li>
|
|
||||||
</ul></q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
v-model="extensionData.public_page.action_fields.generate_payment_logic"
|
|
||||||
size="md"
|
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-6"></div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="extensionData.public_page.action_fields.generate_action && extensionData.public_page.action_fields.generate_payment_logic"
|
|
||||||
class="row q-col-gutter-md q-mt-md"
|
|
||||||
>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Wallet</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<ul class="q-pa-none q-ma-none">
|
|
||||||
<li>
|
|
||||||
Select the field from the
|
|
||||||
<code v-text="extensionData.owner_data.name"></code
|
|
||||||
> (Owner Data) that represents the wallet which
|
|
||||||
will generate the invoice and receive the payments.
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
Only fields with the type <code>Wallet</code> will be
|
|
||||||
shown.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
v-model="extensionData.public_page.action_fields.wallet_id"
|
|
||||||
:options="[''].concat(extensionData.owner_data.fields.filter(f => f.type === 'wallet').map(f => f.name))"
|
|
||||||
></q-select>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Currency</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<ul class="q-pa-none q-ma-none">
|
|
||||||
<li>
|
|
||||||
Select the field from the
|
|
||||||
<code v-text="extensionData.owner_data.name"></code
|
|
||||||
> (Owner Data) that represents the currency
|
|
||||||
which will be used to for the amount.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Only fields with the type <code>Currency</code> will
|
|
||||||
be shown.
|
|
||||||
</li>
|
|
||||||
<li>Empty if you want to use sats.</li>
|
|
||||||
</ul>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
v-model="extensionData.public_page.action_fields.currency"
|
|
||||||
:options="[''].concat(extensionData.owner_data.fields.filter(f => f.type === 'currency').map(f => f.name))"
|
|
||||||
></q-select>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Amount</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<ul class="q-pa-none q-ma-none">
|
|
||||||
<li>
|
|
||||||
Select the field from the
|
|
||||||
<code v-text="extensionData.owner_data.name"></code
|
|
||||||
> (Owner Data) or
|
|
||||||
<code v-text="extensionData.client_data.name"></code
|
|
||||||
> (Client Data) that represents the amount (in
|
|
||||||
the selected currency).
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Only fields with the type <code>Integer</code> and
|
|
||||||
<code>Float</code> will be shown.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-6">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
v-model="extensionData.public_page.action_fields.amount_source"
|
|
||||||
:options="amountSource"
|
|
||||||
class="q-mr-sm"
|
|
||||||
></q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
v-model="extensionData.public_page.action_fields.amount"
|
|
||||||
:options="paymentActionAmountFields"
|
|
||||||
></q-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Paid Flag</q-item-label>
|
|
||||||
<q-item-label caption>
|
|
||||||
<ul class="q-pa-none q-ma-none">
|
|
||||||
<li>
|
|
||||||
Select the field from the
|
|
||||||
<code v-text="extensionData.client_data.name"></code
|
|
||||||
> (Client Data) that will be set to true when
|
|
||||||
the invoice is paid.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Only fields with the type <code>Boolean</code> will be
|
|
||||||
shown.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
v-model="extensionData.public_page.action_fields.paid_flag"
|
|
||||||
:options="[''].concat(extensionData.client_data.fields.filter(f => f.type === 'bool').map(f => f.name))"
|
|
||||||
></q-select>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<q-step
|
|
||||||
:name="6"
|
|
||||||
:done="step > 6"
|
|
||||||
title="Publish"
|
|
||||||
icon="publish"
|
|
||||||
style="min-height: 100px"
|
|
||||||
>
|
|
||||||
<div v-if="g.user.admin" class="row">
|
|
||||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
v-model="extensionData.stub_version"
|
|
||||||
hint="The version of the extension stub. Make sure it is compatible with your LNbits install."
|
|
||||||
:options="extensionStubVersions.map(f => f.version)"
|
|
||||||
></q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
|
||||||
<q-btn
|
|
||||||
@click="cleanCacheData()"
|
|
||||||
color="grey"
|
|
||||||
outline
|
|
||||||
label="Clean Cache"
|
|
||||||
class="q-ml-md"
|
|
||||||
/>
|
|
||||||
<q-icon
|
|
||||||
name="info"
|
|
||||||
size="md"
|
|
||||||
color="primary"
|
|
||||||
class="cursor-pointer q-ml-xs q-mb-xs"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<ul class="q-pl-sm">
|
|
||||||
<li>
|
|
||||||
The extension builder uses caching to speed up the build
|
|
||||||
process.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
This action clears old data and redownloads the Extension
|
|
||||||
Builder Stub release.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="g.user.admin" class="row q-mt-md">
|
|
||||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
|
||||||
<div class="row">
|
|
||||||
<q-btn
|
|
||||||
@click="buildExtensionAndDeploy()"
|
|
||||||
color="primary"
|
|
||||||
label="Build and Deploy (Admin Only)"
|
|
||||||
class="col"
|
|
||||||
/>
|
|
||||||
<q-icon
|
|
||||||
name="info"
|
|
||||||
size="md"
|
|
||||||
color="primary"
|
|
||||||
class="cursor-pointer q-ml-sm self-center"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<ul class="q-pl-sm">
|
|
||||||
<li>
|
|
||||||
Installs the extension directly to this LNbits instance.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
The extension will be enabled by default, and available to
|
|
||||||
all users.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-mt-md">
|
|
||||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
|
||||||
<div class="row">
|
|
||||||
<q-btn
|
|
||||||
@click="buildExtension()"
|
|
||||||
outline
|
|
||||||
color="gray"
|
|
||||||
label="Download Extension Zip"
|
|
||||||
icon="download"
|
|
||||||
class="col"
|
|
||||||
/>
|
|
||||||
<q-icon
|
|
||||||
name="info"
|
|
||||||
size="md"
|
|
||||||
color="primary"
|
|
||||||
class="cursor-pointer q-ml-sm self-center"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
Builds the extension and downloads a zip file with the code.
|
|
||||||
You can then install it manually in your LNbits instance.
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<template v-slot:navigation>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 col-sm-12 q-pl-md q-pt-md">
|
|
||||||
<q-btn
|
|
||||||
v-if="step == 1"
|
|
||||||
label="Clear All Data"
|
|
||||||
color="negative"
|
|
||||||
@click="clearAllData"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
flat
|
|
||||||
color="grey-8"
|
|
||||||
class="q-mr-sm"
|
|
||||||
@click="previousStep()"
|
|
||||||
label="Back"
|
|
||||||
icon="chevron_left"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 col-sm-12 q-pr-md q-pb-md">
|
|
||||||
<q-stepper-navigation class="float-right">
|
|
||||||
<q-btn
|
|
||||||
v-if="step < 6"
|
|
||||||
@click="nextStep()"
|
|
||||||
color="primary"
|
|
||||||
label="Next"
|
|
||||||
></q-btn>
|
|
||||||
<template v-else>
|
|
||||||
<q-btn
|
|
||||||
@click="exportJsonData()"
|
|
||||||
color="primary"
|
|
||||||
label="Export JSON Data"
|
|
||||||
></q-btn>
|
|
||||||
<q-icon
|
|
||||||
name="info"
|
|
||||||
size="md"
|
|
||||||
color="primary"
|
|
||||||
class="cursor-pointer q-ml-sm self-center"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<ul class="q-pl-sm">
|
|
||||||
<li>
|
|
||||||
Exports the config JSON so it can be later imported or
|
|
||||||
shared.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
This JSON is also added to the zip in a file called
|
|
||||||
`builder.json`.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
</q-stepper-navigation>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-stepper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-col-gutter-md"></div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
{% extends "public.html" %} {% block page_container %}
|
|
||||||
<q-page-container>
|
|
||||||
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
|
|
||||||
{% block page %}
|
|
||||||
<div class="row q-col-gutter-md justify-center main">
|
|
||||||
<div class="col-10 col-md-8 col-lg-6 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section class="grid">
|
|
||||||
<div>
|
|
||||||
<h6 class="q-my-none text-center">
|
|
||||||
<strong v-text="$t('welcome_lnbits')"></strong>
|
|
||||||
<p><span v-text="$t('setup_su_account')"></span></p>
|
|
||||||
</h6>
|
|
||||||
<br />
|
|
||||||
<q-form class="q-gutter-md">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="loginData.username"
|
|
||||||
:label="$t('username')"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model.trim="loginData.password"
|
|
||||||
:type="loginData.isPwd ? 'password' : 'text'"
|
|
||||||
autocomplete="off"
|
|
||||||
:label="$t('password')"
|
|
||||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
|
||||||
><template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="loginData.isPwd = !loginData.isPwd"
|
|
||||||
/> </template
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model.trim="loginData.passwordRepeat"
|
|
||||||
:type="loginData.isPwdRepeat ? 'password' : 'text'"
|
|
||||||
autocomplete="off"
|
|
||||||
:label="$t('password_repeat')"
|
|
||||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || $t('invalid_password_repeat')]"
|
|
||||||
><template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
|
|
||||||
/> </template
|
|
||||||
></q-input>
|
|
||||||
<q-btn
|
|
||||||
@click="setPassword()"
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:label="$t('login')"
|
|
||||||
:disable="checkPasswordsMatch || !loginData.username || !loginData.password || !loginData.passwordRepeat"
|
|
||||||
></q-btn>
|
|
||||||
</q-form>
|
|
||||||
</div>
|
|
||||||
<div class="hero-wrapper">
|
|
||||||
<div class="hero q-mx-auto"></div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<style>
|
|
||||||
main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.grid {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.hero-wrapper {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.hero {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 250px;
|
|
||||||
background-image: url("{{ static_url_for('static', 'images/logos/lnbits.svg') }}");
|
|
||||||
background-position: center;
|
|
||||||
background-size: contain;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
grid-gap: 1rem;
|
|
||||||
}
|
|
||||||
.hero-wrapper {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
window.app = Vue.createApp({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
loginData: {
|
|
||||||
isPwd: true,
|
|
||||||
isPwdRepeat: true,
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
passwordRepeat: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.hasAdminUI = '{{ LNBITS_ADMIN_UI | tojson}}'
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
checkPasswordsMatch() {
|
|
||||||
return this.loginData.password !== this.loginData.passwordRepeat
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async setPassword() {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
'/api/v1/auth/first_install',
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
username: this.loginData.username,
|
|
||||||
password: this.loginData.password,
|
|
||||||
password_repeat: this.loginData.passwordRepeat
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
window.location.href = '/admin'
|
|
||||||
} catch (e) {
|
|
||||||
LNbits.utils.notifyApiError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
</q-page>
|
|
||||||
</q-page-container>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,710 +0,0 @@
|
|||||||
{% extends "public.html" %} {% block scripts %}
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--size: 100px;
|
|
||||||
--gap: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-fixed-width {
|
|
||||||
/* width: 45%; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap);
|
|
||||||
margin: auto;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee {
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
user-select: none;
|
|
||||||
gap: var(--gap);
|
|
||||||
height: max-content;
|
|
||||||
mask-image: linear-gradient(
|
|
||||||
to right,
|
|
||||||
hsl(0 0% 0% / 0),
|
|
||||||
hsl(0 0% 0% / 1) 20%,
|
|
||||||
hsl(0 0% 0% / 1) 80%,
|
|
||||||
hsl(0 0% 0% / 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee__group {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
gap: var(--gap);
|
|
||||||
min-width: 100%;
|
|
||||||
animation: scroll-x 60s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee:hover .marquee__group {
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee__group div {
|
|
||||||
width: var(--size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scroll-x {
|
|
||||||
from {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(calc(-100% - var(--gap)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
|
|
||||||
{% endblock %} {% block page_container %}
|
|
||||||
<q-page-container>
|
|
||||||
<q-page
|
|
||||||
class="q-px-md q-py-lg content-center"
|
|
||||||
:class="{'q-px-lg': $q.screen.gt.xs}"
|
|
||||||
>
|
|
||||||
{% block page %}
|
|
||||||
<div
|
|
||||||
class="row justify-center items-center"
|
|
||||||
style="min-height: calc(100vh / 1.618)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="full-width"
|
|
||||||
:style="`max-width: ${'{{ LNBITS_CUSTOM_IMAGE }}' ? '850' : '600'}px`"
|
|
||||||
>
|
|
||||||
<div class="row q-mb-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<div>
|
|
||||||
<h5
|
|
||||||
class="q-my-none"
|
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
|
||||||
>
|
|
||||||
{{SITE_TITLE}}
|
|
||||||
</h5>
|
|
||||||
<template v-if="$q.screen.gt.sm">
|
|
||||||
<h6
|
|
||||||
class="q-my-sm"
|
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
|
||||||
>
|
|
||||||
{{SITE_TAGLINE}}
|
|
||||||
</h6>
|
|
||||||
<p
|
|
||||||
class="q-my-sm"
|
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
|
||||||
>
|
|
||||||
{{SITE_DESCRIPTION}}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<!-- <div
|
|
||||||
class="gt-sm"
|
|
||||||
v-html="formatDescription"
|
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
|
||||||
></div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<q-badge
|
|
||||||
v-if="isAccessTokenExpired"
|
|
||||||
class="q-mx-auto q-mb-md"
|
|
||||||
color="primary"
|
|
||||||
rounded
|
|
||||||
>
|
|
||||||
<div class="text-h6">
|
|
||||||
<span v-text="$t('session_has_expired')"></span>
|
|
||||||
</div>
|
|
||||||
</q-badge>
|
|
||||||
<q-card bordered class="full-width q-py-md">
|
|
||||||
<div class="row">
|
|
||||||
<div
|
|
||||||
class="col-12"
|
|
||||||
:class="{'col-sm-7' : '{{ LNBITS_CUSTOM_IMAGE }}', 'col-lg-6' : '{{ LNBITS_CUSTOM_IMAGE }}'}"
|
|
||||||
>
|
|
||||||
{% if lnurl and LNBITS_NEW_ACCOUNTS_ALLOWED and ("user-id-only"
|
|
||||||
in LNBITS_AUTH_METHODS)%}
|
|
||||||
<div class="full-height content-center">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-body1">
|
|
||||||
<span v-text="$t('claim_desc')"></span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
@click="processing"
|
|
||||||
type="a"
|
|
||||||
href="/lnurlwallet?lightning={{ lnurl }}"
|
|
||||||
v-text="$t('press_to_claim')"
|
|
||||||
class="full-width"
|
|
||||||
></q-btn>
|
|
||||||
</q-card-section>
|
|
||||||
</div>
|
|
||||||
{%else%}
|
|
||||||
<username-password
|
|
||||||
v-if="authMethod != 'user-id-only'"
|
|
||||||
:allowed_new_users="allowedRegister"
|
|
||||||
:auth-methods="LNBITS_AUTH_METHODS"
|
|
||||||
:auth-action="authAction"
|
|
||||||
v-model:user-name="username"
|
|
||||||
v-model:password_1="password"
|
|
||||||
v-model:password_2="passwordRepeat"
|
|
||||||
v-model:reset-key="reset_key"
|
|
||||||
@login="login"
|
|
||||||
@register="register"
|
|
||||||
@reset="reset"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="text-center text-grey-6"
|
|
||||||
v-if="authAction !== 'reset'"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
v-if="authAction === 'login' && allowedRegister"
|
|
||||||
class="q-mb-none"
|
|
||||||
>
|
|
||||||
Not registered?
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="text-secondary cursor-pointer"
|
|
||||||
@click.prevent="showRegister('username-password')"
|
|
||||||
>Create an Account</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
v-else-if="authAction === 'login' && !allowedRegister"
|
|
||||||
class="q-mb-none"
|
|
||||||
>
|
|
||||||
<span v-text="$t('new_user_not_allowed')"></span>
|
|
||||||
</p>
|
|
||||||
<p v-else-if="authAction === 'register'" class="q-mb-none">
|
|
||||||
<span v-text="$t('existing_account_question')"></span>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="text-secondary cursor-pointer q-ml-sm"
|
|
||||||
@click.prevent="showLogin('username-password')"
|
|
||||||
v-text="$t('login')"
|
|
||||||
></a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</username-password>
|
|
||||||
{% if "user-id-only" in LNBITS_AUTH_METHODS %}
|
|
||||||
<user-id-only
|
|
||||||
:allowed_new_users="allowedRegister"
|
|
||||||
v-model:usr="usr"
|
|
||||||
v-model:wallet="walletName"
|
|
||||||
:auth-action="authAction"
|
|
||||||
:auth-method="authMethod"
|
|
||||||
@show-login="showLogin"
|
|
||||||
@show-register="showRegister"
|
|
||||||
@login-usr="loginUsr"
|
|
||||||
@create-wallet="createWallet"
|
|
||||||
>
|
|
||||||
</user-id-only>
|
|
||||||
{%endif%} {% endif %}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col-sm-5 col-lg-6 gt-xs"
|
|
||||||
v-if="'{{ LNBITS_CUSTOM_IMAGE }}'"
|
|
||||||
>
|
|
||||||
<div class="full-height flex flex-center q-pa-lg">
|
|
||||||
<q-img
|
|
||||||
:src="'{{ LNBITS_CUSTOM_IMAGE }}'"
|
|
||||||
:ratio="1"
|
|
||||||
width="250px"
|
|
||||||
></q-img>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats' && '{{ SITE_TITLE }}' == 'LNbits' && '{{ LNBITS_SHOW_HOME_PAGE_ELEMENTS }}' == 'True'"
|
|
||||||
class="full-width q-mb-lg q-mt-sm"
|
|
||||||
>
|
|
||||||
<div class="flex flex-center q-gutter-md q-py-md">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
type="a"
|
|
||||||
href="https://github.com/lnbits/lnbits"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
:label="$t('view_github')"
|
|
||||||
class=""
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
type="a"
|
|
||||||
href="https://demo.lnbits.com/lnurlp/link/fH59GD"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
:label="$t('donate')"
|
|
||||||
class=""
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if AD_SPACE_ENABLED and AD_SPACE %}
|
|
||||||
<div class="q-pt-md full-width">
|
|
||||||
<div class="row justify-center q-mb-xl">
|
|
||||||
<div class="full-width text-center">
|
|
||||||
<span class="text-uppercase text-grey">{{ AD_SPACE_TITLE }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-center columm">
|
|
||||||
{% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
|
|
||||||
<div class="flex flex-center column q-pr-sm">
|
|
||||||
<a href="{{ AD[0] }}">
|
|
||||||
<img
|
|
||||||
v-if="($q.dark.isActive)"
|
|
||||||
src="{{ AD[1] }}"
|
|
||||||
style="max-width: 420px"
|
|
||||||
class="full-width"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
src="{{ AD[2] }}"
|
|
||||||
style="max-width: 420px"
|
|
||||||
class="full-width"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div
|
|
||||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats' && '{{ SITE_TITLE }}' == 'LNbits' && '{{ LNBITS_SHOW_HOME_PAGE_ELEMENTS }}' == 'True'"
|
|
||||||
class="full-width"
|
|
||||||
>
|
|
||||||
<div class="wrapper">
|
|
||||||
<div class="marquee">
|
|
||||||
<div class="marquee__group">
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://github.com/ElementsProject/lightning"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/cln.png') }}' : '{{ static_url_for('static', 'images/clnl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://github.com/lightningnetwork/lnd"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnd.png') }}' : '{{ static_url_for('static', 'images/lnd.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://opennode.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/opennode.png') }}' : '{{ static_url_for('static', 'images/opennodel.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://lnpay.co/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnpay.png') }}' : '{{ static_url_for('static', 'images/lnpayl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://github.com/rootzoll/raspiblitz"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/blitz.png') }}' : '{{ static_url_for('static', 'images/blitzl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://start9.com/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/start9.png') }}' : '{{ static_url_for('static', 'images/start9l.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://getumbrel.com/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/umbrel.png') }}' : '{{ static_url_for('static', 'images/umbrell.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://mynodebtc.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/mynode.png') }}' : '{{ static_url_for('static', 'images/mynodel.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://github.com/shesek/spark-wallet"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/spark.png') }}' : '{{ static_url_for('static', 'images/sparkl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://voltage.cloud"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/voltage.png') }}' : '{{ static_url_for('static', 'images/voltagel.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://breez.technology/sdk/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/breez.png') }}' : '{{ static_url_for('static', 'images/breezl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://blockstream.com/lightning/greenlight/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/greenlight.png') }}' : '{{ static_url_for('static', 'images/greenlightl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://getalby.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/alby.png') }}' : '{{ static_url_for('static', 'images/albyl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://zbd.gg"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/zbd.png') }}' : '{{ static_url_for('static', 'images/zbdl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://phoenix.acinq.co/server"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/phoenixd.png') }}' : '{{ static_url_for('static', 'images/phoenixdl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://boltz.exchange/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/boltz.svg') }}' : '{{ static_url_for('static', 'images/boltz.svg') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://www.blink.sv/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/blink_logo.png') }}' : '{{ static_url_for('static', 'images/blink_logol.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<!-- # -->
|
|
||||||
</div>
|
|
||||||
<div class="marquee__group">
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://github.com/ElementsProject/lightning"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/cln.png') }}' : '{{ static_url_for('static', 'images/clnl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://github.com/lightningnetwork/lnd"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnd.png') }}' : '{{ static_url_for('static', 'images/lnd.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://opennode.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/opennode.png') }}' : '{{ static_url_for('static', 'images/opennodel.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://lnpay.co/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnpay.png') }}' : '{{ static_url_for('static', 'images/lnpayl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://github.com/rootzoll/raspiblitz"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/blitz.png') }}' : '{{ static_url_for('static', 'images/blitzl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://start9.com/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/start9.png') }}' : '{{ static_url_for('static', 'images/start9l.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://getumbrel.com/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/umbrel.png') }}' : '{{ static_url_for('static', 'images/umbrell.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://mynodebtc.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/mynode.png') }}' : '{{ static_url_for('static', 'images/mynodel.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://github.com/shesek/spark-wallet"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/spark.png') }}' : '{{ static_url_for('static', 'images/sparkl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://voltage.cloud"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/voltage.png') }}' : '{{ static_url_for('static', 'images/voltagel.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://breez.technology/sdk/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/breez.png') }}' : '{{ static_url_for('static', 'images/breezl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://blockstream.com/lightning/greenlight/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/greenlight.png') }}' : '{{ static_url_for('static', 'images/greenlightl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://getalby.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/alby.png') }}' : '{{ static_url_for('static', 'images/albyl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://zbd.gg"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/zbd.png') }}' : '{{ static_url_for('static', 'images/zbdl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://phoenix.acinq.co/server"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/phoenixd.png') }}' : '{{ static_url_for('static', 'images/phoenixdl.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://boltz.exchange/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/boltz.svg') }}' : '{{ static_url_for('static', 'images/boltz.svg') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://www.blink.sv/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
contain
|
|
||||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/blink_logo.png') }}' : '{{ static_url_for('static', 'images/blink_logol.png') }}'"
|
|
||||||
></q-img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<!-- # -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
</q-page>
|
|
||||||
</q-page-container>
|
|
||||||
{% endblock %}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,165 +0,0 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
|
||||||
<!---->
|
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
|
||||||
<!---->
|
|
||||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-card>
|
|
||||||
<div class="q-pa-sm q-pl-lg">
|
|
||||||
<div class="row items-center justify-between q-gutter-xs">
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
|
||||||
@click="showAddWalletDialog.show = true"
|
|
||||||
:label="$t('add_wallet')"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-input
|
|
||||||
:label="$t('search_wallets')"
|
|
||||||
dense
|
|
||||||
class="float-right q-pr-xl"
|
|
||||||
v-model="walletsTable.search"
|
|
||||||
>
|
|
||||||
<template v-slot:before>
|
|
||||||
<q-icon name="search"> </q-icon>
|
|
||||||
</template>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
v-if="walletsTable.search !== ''"
|
|
||||||
name="close"
|
|
||||||
@click="walletsTable.search = ''"
|
|
||||||
class="cursor-pointer"
|
|
||||||
>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<q-table
|
|
||||||
grid
|
|
||||||
grid-header
|
|
||||||
flat
|
|
||||||
bordered
|
|
||||||
:rows="wallets"
|
|
||||||
:columns="walletsTable.columns"
|
|
||||||
v-model:pagination="walletsTable.pagination"
|
|
||||||
:loading="walletsTable.loading"
|
|
||||||
@request="getUserWallets"
|
|
||||||
row-key="id"
|
|
||||||
:filter="filter"
|
|
||||||
hide-header
|
|
||||||
>
|
|
||||||
<template v-slot:item="props">
|
|
||||||
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
|
|
||||||
<q-card
|
|
||||||
class="q-ma-sm cursor-pointer wallet-list-card"
|
|
||||||
style="text-decoration: none"
|
|
||||||
@click="goToWallet(props.row.id)"
|
|
||||||
>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-avatar
|
|
||||||
size="lg"
|
|
||||||
:text-color="$q.dark.isActive ? 'black' : 'grey-3'"
|
|
||||||
:color="props.row.extra.color"
|
|
||||||
:icon="props.row.extra.icon"
|
|
||||||
>
|
|
||||||
</q-avatar>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="text-h6 q-pl-md ellipsis"
|
|
||||||
class="text-bold"
|
|
||||||
v-text="props.row.name"
|
|
||||||
></div>
|
|
||||||
<q-space> </q-space>
|
|
||||||
<q-btn
|
|
||||||
v-if="props.row.extra.pinned"
|
|
||||||
round
|
|
||||||
color="primary"
|
|
||||||
text-color="black"
|
|
||||||
size="xs"
|
|
||||||
icon="push_pin"
|
|
||||||
class="float-right"
|
|
||||||
style="transform: rotate(30deg)"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row items-center q-pt-sm">
|
|
||||||
<h6 class="q-my-none ellipsis full-width">
|
|
||||||
<strong
|
|
||||||
v-text="formatBalance(props.row.balance_msat / 1000)"
|
|
||||||
></strong>
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator />
|
|
||||||
|
|
||||||
<q-card-section class="text-left">
|
|
||||||
<small>
|
|
||||||
<strong>
|
|
||||||
<span v-text="$t('currency')"></span>
|
|
||||||
</strong>
|
|
||||||
<span v-text="props.row.currency || 'sat'"></span>
|
|
||||||
</small>
|
|
||||||
<br />
|
|
||||||
<small>
|
|
||||||
<strong>
|
|
||||||
<span v-text="$t('id')"></span>
|
|
||||||
:
|
|
||||||
</strong>
|
|
||||||
<span v-text="props.row.id"></span>
|
|
||||||
</small>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-dialog
|
|
||||||
v-model="showAddWalletDialog.show"
|
|
||||||
persistent
|
|
||||||
@hide="showAddWalletDialog = {show: false}"
|
|
||||||
>
|
|
||||||
<q-card style="min-width: 350px">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">
|
|
||||||
<span v-text="$t('wallet_name')"></span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section class="q-pt-none">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
v-model="showAddWalletDialog.name"
|
|
||||||
autofocus
|
|
||||||
@keyup.enter="submitAddWallet()"
|
|
||||||
></q-input>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="right" class="text-primary">
|
|
||||||
<q-btn flat :label="$t('cancel')" v-close-popup></q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:label="$t('add_wallet')"
|
|
||||||
v-close-popup
|
|
||||||
@click="submitAddWallet()"
|
|
||||||
></q-btn>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
|
%} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}{%
|
||||||
|
endblock %}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
|
||||||
|
context %} {% block scripts %} {{ window_vars() }} {% endblock %} {% block page
|
||||||
|
%} {% endblock %}
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
<q-tab-panel name="channels">
|
|
||||||
<q-dialog v-model="connectPeerDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<q-form class="q-gutter-md">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="text"
|
|
||||||
filled
|
|
||||||
v-model="connectPeerDialog.data.uri"
|
|
||||||
label="Node URI"
|
|
||||||
hint="pubkey@host:port"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
:label="$t('connect')"
|
|
||||||
color="primary"
|
|
||||||
@click="connectPeer"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
:label="$t('cancel')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog v-model="setFeeDialog.show">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<label class="text-h6">Set Channel Fee</label>
|
|
||||||
<p class="text-caption" v-text="setFeeDialog.channel_id"></p>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-form class="q-gutter-md">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
v-model.number="setFeeDialog.data.fee_ppm"
|
|
||||||
label="Fee Rate PPM"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
v-model.number="setFeeDialog.data.fee_base_msat"
|
|
||||||
label="Fee Base msat"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
:label="$t('set')"
|
|
||||||
color="primary"
|
|
||||||
@click="setChannelFee(setFeeDialog.channel_id)"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
:label="$t('cancel')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog v-model="openChannelDialog.show">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<q-form class="q-gutter-md">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="text"
|
|
||||||
filled
|
|
||||||
v-model="openChannelDialog.data.peer_id"
|
|
||||||
label="Peer ID"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
v-model.number="openChannelDialog.data.funding_amount"
|
|
||||||
label="Funding Amount"
|
|
||||||
></q-input>
|
|
||||||
<q-expansion-item icon="warning" label="Advanced">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="column q-gutter-md">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
v-model.number="openChannelDialog.data.push_amount"
|
|
||||||
label="Push Amount"
|
|
||||||
hint="This gifts sats to the other side!"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
v-model.number="openChannelDialog.data.fee_rate"
|
|
||||||
label="Fee Rate"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
:label="$t('open')"
|
|
||||||
color="primary"
|
|
||||||
@click="openChannel"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
:label="$t('cancel')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog v-model="closeChannelDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<q-form class="q-gutter-md">
|
|
||||||
<div>
|
|
||||||
<q-checkbox v-model="closeChannelDialog.data.force" label="Force" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
:label="$t('close')"
|
|
||||||
color="primary"
|
|
||||||
@click="closeChannel"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
:label="$t('cancel')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<div class="row q-col-gutter-lg">
|
|
||||||
<div class="col-12 col-xl-6">
|
|
||||||
<q-card class="full-height">
|
|
||||||
<q-card-section class="q-gutter-y-sm">
|
|
||||||
<div class="row items-center q-mt-none q-gutter-x-sm no-wrap">
|
|
||||||
<div class="col-grow text-h6 q-my-none col-grow">Channels</div>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
clearable
|
|
||||||
v-model="channels.filter"
|
|
||||||
placeholder="Search..."
|
|
||||||
class="col-auto"
|
|
||||||
></q-input>
|
|
||||||
<q-select
|
|
||||||
dense
|
|
||||||
size="sm"
|
|
||||||
style="min-width: 200px"
|
|
||||||
filled
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
v-model="stateFilters"
|
|
||||||
:options="this.states"
|
|
||||||
class="col-auto"
|
|
||||||
></q-select>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
size="md"
|
|
||||||
class="col-auto"
|
|
||||||
@click="showOpenChannelDialog()"
|
|
||||||
>
|
|
||||||
Open channel
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-subtitle1 col-grow">Total</div>
|
|
||||||
<lnbits-channel-balance
|
|
||||||
:balance="this.totalBalance"
|
|
||||||
></lnbits-channel-balance>
|
|
||||||
</div>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:rows="this.filteredChannels"
|
|
||||||
:filter="channels.filter"
|
|
||||||
no-data-label="No channels opened"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props" style="height: 0"> </q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<div class="q-pb-sm">
|
|
||||||
<div class="row items-center q-gutter-sm">
|
|
||||||
<div class="text-subtitle1" v-text="props.row.name"></div>
|
|
||||||
<div class="text-caption" v-if="props.row.peer_id">
|
|
||||||
<span>Peer ID</span>
|
|
||||||
<q-btn
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="content_paste"
|
|
||||||
@click="copyText(props.row.peer_id)"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="text-caption col-grow">
|
|
||||||
<span>Fees</span>
|
|
||||||
<q-btn
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="settings"
|
|
||||||
@click="showSetFeeDialog(props.row.id)"
|
|
||||||
></q-btn>
|
|
||||||
<span v-if="props.row.fee_ppm">
|
|
||||||
<span v-text="props.row.fee_ppm"></span> ppm,
|
|
||||||
<span v-text="props.row.fee_base_msat"></span> msat
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-caption" v-if="props.row.id">
|
|
||||||
<span>Channel ID</span>
|
|
||||||
<q-btn
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="content_paste"
|
|
||||||
@click="copyText(props.row.id)"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="text-caption" v-if="props.row.short_id">
|
|
||||||
<span v-text="props.row.short_id"></span>
|
|
||||||
<q-btn
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="content_paste"
|
|
||||||
@click="copyText(props.row.short_id)"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<q-badge
|
|
||||||
rounded
|
|
||||||
:color="states.find(s => s.value == props.row.state)?.color"
|
|
||||||
v-text="states.find(s => s.value == props.row.state)?.label"
|
|
||||||
>
|
|
||||||
</q-badge>
|
|
||||||
<q-btn
|
|
||||||
:disable='props.row.state !== "active"'
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="md"
|
|
||||||
@click="showCloseChannelDialog(props.row)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<lnbits-channel-balance
|
|
||||||
:balance="props.row.balance"
|
|
||||||
:color="props.row.color"
|
|
||||||
></lnbits-channel-balance>
|
|
||||||
</div>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-xl-6">
|
|
||||||
<q-card class="full-height">
|
|
||||||
<q-card-section class="column q-gutter-y-sm">
|
|
||||||
<div
|
|
||||||
class="row items-center q-mt-none justify-between q-gutter-x-md no-wrap"
|
|
||||||
>
|
|
||||||
<div class="col-grow text-h6 q-my-none">Peers</div>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
clearable
|
|
||||||
v-model="peers.filter"
|
|
||||||
placeholder="Search..."
|
|
||||||
class="col-auto"
|
|
||||||
></q-input>
|
|
||||||
<q-btn
|
|
||||||
class="col-auto"
|
|
||||||
color="primary"
|
|
||||||
@click="connectPeerDialog.show = true"
|
|
||||||
>
|
|
||||||
Connect Peer
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:rows="peers.data"
|
|
||||||
:filter="peers.filter"
|
|
||||||
no-data-label="No transactions made yet"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props" style="height: 0"> </q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<div class="row no-wrap items-center q-gutter-sm">
|
|
||||||
<div class="q-my-sm col-grow">
|
|
||||||
<div
|
|
||||||
class="text-subtitle1 text-bold"
|
|
||||||
v-text="props.row.alias"
|
|
||||||
></div>
|
|
||||||
<div class="row items-center q-gutter-sm">
|
|
||||||
<q-badge
|
|
||||||
:style="`background-color: #${props.row.color}`"
|
|
||||||
class="text-bold"
|
|
||||||
v-text="'#'+props.row.color"
|
|
||||||
>
|
|
||||||
</q-badge>
|
|
||||||
<div
|
|
||||||
class="text-bold"
|
|
||||||
v-text="shortenNodeId(props.row.id)"
|
|
||||||
></div>
|
|
||||||
<q-btn
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="content_paste"
|
|
||||||
@click="copyText(props.row.id)"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="qr_code"
|
|
||||||
@click="showNodeInfoDialog(props.row)"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
@click="showOpenChannelDialog(props.row.id)"
|
|
||||||
>
|
|
||||||
Open channel
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="md"
|
|
||||||
@click="disconnectPeer(props.row.id)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-tab-panel>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<q-tab-panel name="dashboard">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<lnbits-node-info :info="this.info"></lnbits-node-info>
|
|
||||||
<div class="row q-col-gutter-lg q-mt-sm">
|
|
||||||
<div class="col-12 col-md-8 q-gutter-y-md">
|
|
||||||
<div class="row q-col-gutter-md q-pb-lg">
|
|
||||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
:title="$t('total_capacity')"
|
|
||||||
:msat="this.channel_stats.total_capacity"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
|
||||||
<lnbits-stat title="Balance" :msat="this.info.balance_msat" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
title="Fees collected"
|
|
||||||
:msat="this.info.fees?.total_msat"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
title="Onchain Balance"
|
|
||||||
:btc="this.info.onchain_balance_sat / 100000000"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
title="Onchain Confirmed"
|
|
||||||
:btc="this.info.onchain_confirmed_sat / 100000000"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
|
||||||
<lnbits-stat title="Peers" :amount="this.info.num_peers" />
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
:title="$t('avg_channel_size')"
|
|
||||||
:msat="this.channel_stats.avg_size"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
:title="$t('biggest_channel_size')"
|
|
||||||
:msat="this.channel_stats.biggest_size"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
:title="$t('smallest_channel_size')"
|
|
||||||
:msat="this.channel_stats.smallest_size"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column col-12 col-md-4 q-gutter-y-md">
|
|
||||||
<lnbits-node-ranks :ranks="this.ranks"></lnbits-node-ranks>
|
|
||||||
<lnbits-channel-stats
|
|
||||||
:stats="this.channel_stats"
|
|
||||||
></lnbits-channel-stats>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-tab-panel>
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
<q-tab-panel name="transactions">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<q-dialog v-model="transactionDetailsDialog.show" position="top">
|
|
||||||
<q-card class="my-card">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-center q-mb-lg">
|
|
||||||
<div
|
|
||||||
v-if="transactionDetailsDialog.data.isIn && transactionDetailsDialog.data.pending"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
size="18px"
|
|
||||||
:name="'call_received'"
|
|
||||||
:color="'green'"
|
|
||||||
></q-icon>
|
|
||||||
<span v-text="$t('payment_received')"></span>
|
|
||||||
</div>
|
|
||||||
<div class="row q-my-md">
|
|
||||||
<div class="col-3"><b v-text="$t('payment_hash')"></b>:</div>
|
|
||||||
<div class="col-9 text-wrap mono">
|
|
||||||
<span
|
|
||||||
v-text="transactionDetailsDialog.data.payment_hash"
|
|
||||||
></span>
|
|
||||||
<q-icon
|
|
||||||
name="content_copy"
|
|
||||||
@click="copyText(transactionDetailsDialog.data.payment_hash)"
|
|
||||||
size="1em"
|
|
||||||
color="grey"
|
|
||||||
class="q-mb-xs cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="row"
|
|
||||||
v-if="transactionDetailsDialog.data.preimage && !transactionDetailsDialog.data.pending"
|
|
||||||
>
|
|
||||||
<div class="col-3"><b v-text="$t('payment_proof')"></b>:</div>
|
|
||||||
<div class="col-9 text-wrap mono">
|
|
||||||
<span v-text="transactionDetailsDialog.data.preimage"></span>
|
|
||||||
<q-icon
|
|
||||||
name="content_copy"
|
|
||||||
@click="copyText(transactionDetailsDialog.data.preimage)"
|
|
||||||
size="1em"
|
|
||||||
color="grey"
|
|
||||||
class="q-mb-xs cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="transactionDetailsDialog.data.bolt11"
|
|
||||||
class="text-center q-mb-lg"
|
|
||||||
>
|
|
||||||
<a :href="'lightning:' + transactionDetailsDialog.data.bolt11">
|
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
|
||||||
<qrcode-vue
|
|
||||||
:value="'lightning:' + transactionDetailsDialog.data.bolt11.toUpperCase()"
|
|
||||||
:options="{width: 340}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode-vue>
|
|
||||||
</q-responsive>
|
|
||||||
</a>
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText(transactionDetailsDialog.data.bolt11)"
|
|
||||||
:label="$t('copy_invoice')"
|
|
||||||
class="q-mt-sm"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md q-pb-lg"></div>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-lg">
|
|
||||||
<div class="col-12 col-lg-6 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap q-mb-sm">
|
|
||||||
<div class="col text-h6 q-my-none">Payments</div>
|
|
||||||
<q-input
|
|
||||||
v-if="payments.length > 10"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
clearable
|
|
||||||
v-model="paymentsTable.filter"
|
|
||||||
debounce="300"
|
|
||||||
placeholder="Search by tag, memo, amount"
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:rows="paymentsTable.data"
|
|
||||||
:columns="paymentsTable.columns"
|
|
||||||
v-model:pagination="paymentsTable.pagination"
|
|
||||||
row-key="payment_hash"
|
|
||||||
no-data-label="No transactions made yet"
|
|
||||||
:filter="paymentsTable.filter"
|
|
||||||
@request="getPayments"
|
|
||||||
>
|
|
||||||
<template v-slot:body-cell-pending="props">
|
|
||||||
<q-td auto-width class="text-center">
|
|
||||||
<q-icon
|
|
||||||
v-if="!props.row.pending"
|
|
||||||
size="xs"
|
|
||||||
name="call_made"
|
|
||||||
color="green"
|
|
||||||
@click="showTransactionDetailsDialog(props.row)"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon
|
|
||||||
v-else
|
|
||||||
size="xs"
|
|
||||||
name="settings_ethernet"
|
|
||||||
color="grey"
|
|
||||||
@click="showTransactionDetailsDialog(props.row)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Pending</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
<q-dialog
|
|
||||||
v-model="props.row.expand"
|
|
||||||
:props="props"
|
|
||||||
position="top"
|
|
||||||
>
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<div class="text-center q-mb-lg">
|
|
||||||
<div v-if="props.row.isIn && props.row.pending">
|
|
||||||
<q-icon
|
|
||||||
name="settings_ethernet"
|
|
||||||
color="grey"
|
|
||||||
></q-icon>
|
|
||||||
<span v-text="$t('invoice_waiting')"></span>
|
|
||||||
<lnbits-payment-details
|
|
||||||
:payment="props.row"
|
|
||||||
></lnbits-payment-details>
|
|
||||||
<div
|
|
||||||
v-if="props.row.bolt11"
|
|
||||||
class="text-center q-mb-lg"
|
|
||||||
>
|
|
||||||
<a :href="'lightning:' + props.row.bolt11">
|
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
|
||||||
<qrcode-vue
|
|
||||||
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
|
||||||
:options="{width: 340}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode-vue>
|
|
||||||
</q-responsive>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText(props.row.bolt11)"
|
|
||||||
:label="$t('copy_invoice')"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
:label="$t('close')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="props.row.isPaid && props.row.isIn">
|
|
||||||
<q-icon
|
|
||||||
size="18px"
|
|
||||||
:name="'call_received'"
|
|
||||||
:color="'green'"
|
|
||||||
></q-icon>
|
|
||||||
<span v-text="$t('payment_received')"></span>
|
|
||||||
<lnbits-payment-details
|
|
||||||
:payment="props.row"
|
|
||||||
></lnbits-payment-details>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="props.row.isPaid && props.row.isOut">
|
|
||||||
<q-icon
|
|
||||||
size="18px"
|
|
||||||
:name="'call_made'"
|
|
||||||
:color="'pink'"
|
|
||||||
></q-icon>
|
|
||||||
<span v-text="$t('payment_sent')"></span>
|
|
||||||
<lnbits-payment-details
|
|
||||||
:payment="props.row"
|
|
||||||
></lnbits-payment-details>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="props.row.isOut && props.row.pending">
|
|
||||||
<q-icon
|
|
||||||
name="settings_ethernet"
|
|
||||||
color="grey"
|
|
||||||
></q-icon>
|
|
||||||
<span v-text="$t('outgoing_payment_pending')"></span>
|
|
||||||
<lnbits-payment-details
|
|
||||||
:payment="props.row"
|
|
||||||
></lnbits-payment-details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-date="props">
|
|
||||||
<q-td auto-width key="date" :props="props">
|
|
||||||
<lnbits-date :ts="props.row.time"></lnbits-date>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-destination="props">
|
|
||||||
<q-td auto-width key="destination">
|
|
||||||
<div class="row items-center justify-between no-wrap">
|
|
||||||
<q-badge
|
|
||||||
:style="`background-color: #${props.row.destination?.color}`"
|
|
||||||
class="text-bold"
|
|
||||||
v-text="props.row.destination?.alias"
|
|
||||||
></q-badge>
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="content_paste"
|
|
||||||
@click="copyText(info.id)"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="qr_code"
|
|
||||||
@click="showNodeInfoDialog(props.row.destination)"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-lg-6 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap q-mb-sm">
|
|
||||||
<div class="col text-h6 q-my-none">Invoices</div>
|
|
||||||
<q-input
|
|
||||||
v-if="payments.length > 10"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
clearable
|
|
||||||
v-model="paymentsTable.filter"
|
|
||||||
debounce="300"
|
|
||||||
placeholder="Search by tag, memo, amount"
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:rows="invoiceTable.data"
|
|
||||||
:columns="invoiceTable.columns"
|
|
||||||
v-model:pagination="invoiceTable.pagination"
|
|
||||||
no-data-label="No transactions made yet"
|
|
||||||
:filter="invoiceTable.filter"
|
|
||||||
@request="getInvoices"
|
|
||||||
>
|
|
||||||
<template v-slot:body-cell-pending="props">
|
|
||||||
<q-td auto-width class="text-center">
|
|
||||||
<q-icon
|
|
||||||
v-if="!props.row.pending"
|
|
||||||
size="xs"
|
|
||||||
name="call_received"
|
|
||||||
color="green"
|
|
||||||
@click="showTransactionDetailsDialog(props.row)"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon
|
|
||||||
v-else
|
|
||||||
size="xs"
|
|
||||||
name="settings_ethernet"
|
|
||||||
color="grey"
|
|
||||||
@click="showTransactionDetailsDialog(props.row)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Pending</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:body-cell-paid_at="props">
|
|
||||||
<q-td auto-width :props="props">
|
|
||||||
<lnbits-date
|
|
||||||
v-if="props.row.paid_at"
|
|
||||||
:ts="props.row.paid_at"
|
|
||||||
></lnbits-date>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:body-cell-expiry="props">
|
|
||||||
<q-td auto-width :props="props">
|
|
||||||
<lnbits-date
|
|
||||||
v-if="props.row.expiry"
|
|
||||||
:ts="props.row.expiry"
|
|
||||||
></lnbits-date>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-tab-panel>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
|
||||||
<!---->
|
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
|
||||||
<!---->
|
|
||||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
|
||||||
|
|
||||||
<q-dialog v-model="nodeInfoDialog.show" position="top">
|
|
||||||
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
|
||||||
<div class="col q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<div class="q-pa-md">
|
|
||||||
<div class="q-gutter-y-md">
|
|
||||||
<q-tabs v-model="tab" active-color="primary" align="justify">
|
|
||||||
<q-tab
|
|
||||||
name="dashboard"
|
|
||||||
:label="$t('dashboard')"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="channels"
|
|
||||||
:label="$t('channels')"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="transactions"
|
|
||||||
:label="$t('transactions')"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
></q-tab>
|
|
||||||
</q-tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-form name="settings_form" id="settings_form">
|
|
||||||
<q-tab-panels v-model="tab" animated>
|
|
||||||
{% include "node/_tab_dashboard.html" %} {% include
|
|
||||||
"node/_tab_channels.html" %} {% include "node/_tab_transactions.html"
|
|
||||||
%}
|
|
||||||
</q-tab-panels>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
|
|
||||||
context %} {% block page %}
|
|
||||||
|
|
||||||
<div class="q-ma-lg-xl q-mx-auto q-ma-xl" style="max-width: 1048px">
|
|
||||||
<lnbits-node-info :info="this.info"></lnbits-node-info>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-lg q-mt-sm">
|
|
||||||
<div class="col-12 col-md-8 q-gutter-y-md">
|
|
||||||
<div class="row q-col-gutter-md q-pb-lg">
|
|
||||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
:title="$t('total_capacity')"
|
|
||||||
:msat="this.channel_stats.total_capacity"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
|
||||||
<lnbits-stat title="Peers" :amount="this.info.num_peers" />
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
:title="$t('avg_channel_size')"
|
|
||||||
:msat="this.channel_stats.avg_size"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
:title="$t('biggest_channel_size')"
|
|
||||||
:msat="this.channel_stats.biggest_size"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
:title="$t('smallest_channel_size')"
|
|
||||||
:msat="this.channel_stats.smallest_size"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
|
||||||
<lnbits-stat
|
|
||||||
:title="$t('smallest_channel_size')"
|
|
||||||
:msat="this.channel_stats.smallest_size"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column col-12 col-md-4 q-gutter-y-md">
|
|
||||||
<lnbits-node-ranks :ranks="this.ranks"></lnbits-node-ranks>
|
|
||||||
<lnbits-channel-stats :stats="this.channel_stats"></lnbits-channel-stats>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
|
||||||
<script src="{{ static_url_for('static', 'js/node.js') }}"></script>
|
|
||||||
<script>
|
|
||||||
window.app = Vue.createApp({
|
|
||||||
el: '#vue',
|
|
||||||
config: {
|
|
||||||
globalProperties: {
|
|
||||||
LNbits,
|
|
||||||
msg: 'hello'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
isSuperUser: false,
|
|
||||||
wallet: {},
|
|
||||||
tab: 'dashboard',
|
|
||||||
payments: 1000,
|
|
||||||
info: {},
|
|
||||||
channel_stats: {},
|
|
||||||
channels: [],
|
|
||||||
activeBalance: {},
|
|
||||||
ranks: {},
|
|
||||||
|
|
||||||
peers: [],
|
|
||||||
|
|
||||||
connectPeerDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
},
|
|
||||||
|
|
||||||
openChannelDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
},
|
|
||||||
|
|
||||||
closeChannelDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
},
|
|
||||||
|
|
||||||
nodeInfoDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
},
|
|
||||||
|
|
||||||
states: [
|
|
||||||
{label: 'Active', value: 'active', color: 'green'},
|
|
||||||
{label: 'Pending', value: 'pending', color: 'orange'},
|
|
||||||
{label: 'Inactive', value: 'inactive', color: 'grey'},
|
|
||||||
{label: 'Closed', value: 'closed', color: 'red'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created: function () {
|
|
||||||
this.getInfo()
|
|
||||||
this.get1MLStats()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
formatMsat: function (msat) {
|
|
||||||
return LNbits.utils.formatMsat(msat)
|
|
||||||
},
|
|
||||||
api: function (method, url, data) {
|
|
||||||
return LNbits.api.request(method, '/node/public/api/v1' + url, {}, data)
|
|
||||||
},
|
|
||||||
getInfo: function () {
|
|
||||||
this.api('GET', '/info', {}).then(response => {
|
|
||||||
this.info = response.data
|
|
||||||
this.channel_stats = response.data.channel_stats
|
|
||||||
})
|
|
||||||
},
|
|
||||||
get1MLStats: function () {
|
|
||||||
this.api('GET', '/rank', {}).then(response => {
|
|
||||||
this.ranks = response.data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,432 +0,0 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
|
||||||
<!---->
|
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
|
||||||
<!---->
|
|
||||||
{% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-card>
|
|
||||||
<div class="q-pa-sm q-pl-lg">
|
|
||||||
<div class="row items-center justify-between q-gutter-xs">
|
|
||||||
<div class="col"></div>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showPaymentStatus"
|
|
||||||
:label="$t('payments_status_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showPaymentTags"
|
|
||||||
:label="$t('payments_tag_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showBalance"
|
|
||||||
:label="$t('payments_balance_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showWalletsSize"
|
|
||||||
:label="$t('payments_wallets_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showBalanceInOut"
|
|
||||||
:label="$t('payments_balance_in_out_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div class="float-left">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
@click="saveChartsPreferences"
|
|
||||||
v-model="chartData.showPaymentCountInOut"
|
|
||||||
:label="$t('payments_count_in_out_chart')"
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<q-btn icon="event" outline flat>
|
|
||||||
<q-popup-proxy
|
|
||||||
cover
|
|
||||||
transition-show="scale"
|
|
||||||
transition-hide="scale"
|
|
||||||
>
|
|
||||||
<q-date v-model="searchDate" mask="YYYY-MM-DD" range />
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-6">
|
|
||||||
<q-btn
|
|
||||||
label="Search"
|
|
||||||
@click="searchByDate()"
|
|
||||||
color="primary"
|
|
||||||
flat
|
|
||||||
class="float-left"
|
|
||||||
v-close-popup
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
@click="clearDateSeach()"
|
|
||||||
label="Clear"
|
|
||||||
class="float-right"
|
|
||||||
color="grey"
|
|
||||||
flat
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-popup-proxy>
|
|
||||||
<q-badge
|
|
||||||
v-if="searchDate?.to || searchDate?.from"
|
|
||||||
class="q-mt-lg q-mr-md"
|
|
||||||
color="primary"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
style="border-radius: 6px"
|
|
||||||
/>
|
|
||||||
</q-btn>
|
|
||||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
v-if="g.user.admin"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
icon="settings"
|
|
||||||
to="/admin#server"
|
|
||||||
>
|
|
||||||
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-show="!showDetails">
|
|
||||||
<div class="row q-col-gutter-md justify-center q-mb-md">
|
|
||||||
<div
|
|
||||||
v-show="chartData.showPaymentStatus"
|
|
||||||
class="col-lg-3 col-md-6 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('payment_chart_status')"></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsStatusChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="chartData.showPaymentStatus"
|
|
||||||
class="col-lg-3 col-md-6 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('payment_chart_tags')"></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsTagsChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="chartData.showBalance"
|
|
||||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong
|
|
||||||
v-text="$t('lnbits_balance', {balance: (lnbitsBalance || 0).toLocaleString()})"
|
|
||||||
></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsDailyChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="chartData.showWalletsSize"
|
|
||||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('payment_chart_tx_per_wallet')"></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsWalletsChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-show="chartData.showBalanceInOut"
|
|
||||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('payments_balance_in_out')"></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsBalanceInOutChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="chartData.showPaymentCountInOut"
|
|
||||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
|
||||||
>
|
|
||||||
<q-card class="q-pt-sm">
|
|
||||||
<strong v-text="$t('payments_count_in_out')"></strong>
|
|
||||||
<div style="height: 300px" class="q-pa-sm">
|
|
||||||
<canvas v-if="chartsReady" ref="paymentsCountInOutChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
|
||||||
<div class="col">
|
|
||||||
<q-card class="q-pa-md">
|
|
||||||
<q-table
|
|
||||||
row-key="payment_hash"
|
|
||||||
:rows="payments"
|
|
||||||
:columns="paymentsTable.columns"
|
|
||||||
v-model:pagination="paymentsTable.pagination"
|
|
||||||
:filter="paymentsTable.search"
|
|
||||||
@request="fetchPayments"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
<q-input
|
|
||||||
v-if="['wallet_id', 'payment_hash', 'memo'].includes(col.name)"
|
|
||||||
v-model="searchData[col.name]"
|
|
||||||
@keydown.enter="searchPaymentsBy()"
|
|
||||||
@update:model-value="searchPaymentsBy()"
|
|
||||||
dense
|
|
||||||
type="text"
|
|
||||||
filled
|
|
||||||
clearable
|
|
||||||
:label="col.label"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
name="search"
|
|
||||||
@click="searchPaymentsBy()"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
<q-btn
|
|
||||||
v-else-if="['status'].includes(col.name)"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
:label="$q.screen.gt.md ? 'Status' : null"
|
|
||||||
icon="filter_alt"
|
|
||||||
color="grey"
|
|
||||||
class="text-capitalize"
|
|
||||||
>
|
|
||||||
<q-menu anchor="top right" self="top start">
|
|
||||||
<q-item dense>
|
|
||||||
<q-checkbox
|
|
||||||
v-model="statusFilters.success"
|
|
||||||
@click="handleFilterChanged"
|
|
||||||
label="Success Payments"
|
|
||||||
></q-checkbox>
|
|
||||||
</q-item>
|
|
||||||
<q-item dense>
|
|
||||||
<q-checkbox
|
|
||||||
v-model="statusFilters.pending"
|
|
||||||
@click="handleFilterChanged"
|
|
||||||
label="Pending Payments"
|
|
||||||
></q-checkbox>
|
|
||||||
</q-item>
|
|
||||||
<q-item dense>
|
|
||||||
<q-checkbox
|
|
||||||
v-model="statusFilters.failed"
|
|
||||||
@click="handleFilterChanged"
|
|
||||||
label="Failed Payments"
|
|
||||||
></q-checkbox>
|
|
||||||
</q-item>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-item dense>
|
|
||||||
<q-checkbox
|
|
||||||
v-model="statusFilters.incoming"
|
|
||||||
@click="handleFilterChanged"
|
|
||||||
label="Incoming Payments"
|
|
||||||
></q-checkbox>
|
|
||||||
</q-item>
|
|
||||||
<q-item dense>
|
|
||||||
<q-checkbox
|
|
||||||
v-model="statusFilters.outgoing"
|
|
||||||
@click="handleFilterChanged"
|
|
||||||
label="Outgoing Payments"
|
|
||||||
></q-checkbox>
|
|
||||||
</q-item>
|
|
||||||
</q-menu>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="$t('filter_payments')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-select
|
|
||||||
v-else-if="['tag'].includes(col.name)"
|
|
||||||
v-model="searchData[col.name]"
|
|
||||||
:options="searchOptions[col.name]"
|
|
||||||
@update:model-value="searchPaymentsBy()"
|
|
||||||
:label="col.label"
|
|
||||||
clearable
|
|
||||||
style="width: 100px"
|
|
||||||
></q-select>
|
|
||||||
<span v-else v-text="col.label"></span>
|
|
||||||
</q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr auto-width :props="props">
|
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
<div v-if="col.name == 'status'">
|
|
||||||
<q-tooltip
|
|
||||||
><span v-text="$t('payment_details')"></span
|
|
||||||
></q-tooltip>
|
|
||||||
<q-icon
|
|
||||||
@click="showDetailsToggle(props.row)"
|
|
||||||
v-if="props.row.status === 'success'"
|
|
||||||
size="14px"
|
|
||||||
:name="props.row.outgoing ? 'call_made' : 'call_received'"
|
|
||||||
:color="props.row.outgoing ? 'pink' : 'green'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon
|
|
||||||
v-else-if="props.row.status === 'pending'"
|
|
||||||
@click="showDetailsToggle(props.row)"
|
|
||||||
name="downloading"
|
|
||||||
:style="props.row.outgoing ? 'transform: rotate(225deg)' : 'transform: scaleX(-1) rotate(315deg)'"
|
|
||||||
color="grey"
|
|
||||||
class="cursor-pointer"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon
|
|
||||||
v-else
|
|
||||||
@click="showDetailsToggle(props.row)"
|
|
||||||
name="warning"
|
|
||||||
color="yellow"
|
|
||||||
class="cursor-pointer"
|
|
||||||
></q-icon>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="col.name == 'created_at'">
|
|
||||||
<div>
|
|
||||||
<q-tooltip anchor="top middle">
|
|
||||||
<span v-text="formatDate(props.row.created_at)"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
<span v-text="props.row.timeFrom"> </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="['wallet_id', 'payment_hash', 'memo'].includes(col.name)"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-if="props.row[col.name]"
|
|
||||||
icon="content_copy"
|
|
||||||
size="sm"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-mr-xs"
|
|
||||||
@click="copyText(props.row[col.name])"
|
|
||||||
>
|
|
||||||
<q-tooltip anchor="top middle">Copy</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<span v-text="shortify(props.row[col.name], col.max_length)">
|
|
||||||
</span>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="props.row[col.name]"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
v-text="props.row[col.name]"
|
|
||||||
class="cursor-pointer"
|
|
||||||
></span>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-show="showDetails">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section class="flex">
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
icon="arrow_back"
|
|
||||||
class="q-mr-md"
|
|
||||||
@click="showDetailsToggle(null)"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="self-center text-h6 text-weight-bolder text-grey-5">
|
|
||||||
<span v-text="$t('payment_details_back')"></span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-card-section class="text-h6">
|
|
||||||
<q-item>
|
|
||||||
<q-item-section avatar class="">
|
|
||||||
<q-icon color="primary" name="receipt" size="44px"></q-icon>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>
|
|
||||||
<div class="text-h6">
|
|
||||||
<span v-text="$t('payment_details')"></span>
|
|
||||||
</div>
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label caption v-text="$t('payment_details_desc')">
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-list separator>
|
|
||||||
<q-item v-for="(value, key) in paymentDetails" :key="key">
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="key"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="value"
|
|
||||||
style="word-wrap: break-word"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section side>
|
|
||||||
<q-btn
|
|
||||||
v-show="value"
|
|
||||||
icon="content_copy"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-ml-sm"
|
|
||||||
@click="copyText(value)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-item-section>
|
|
||||||
<!-- <q-separator></q-separator> -->
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<q-dialog v-model="createWalletDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
|
||||||
<strong>Create Wallet</strong>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<div class="col">
|
|
||||||
<q-input
|
|
||||||
v-model="createWalletDialog.data.name"
|
|
||||||
:label='$t("name_your_wallet")'
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<div class="col">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="createWalletDialog.data.currency"
|
|
||||||
:options="{{ currencies | safe }}"
|
|
||||||
></q-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
@click="createWallet()"
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
>Create</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
<div class="row q-mb-lg">
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
|
||||||
icon="arrow_back_ios"
|
|
||||||
@click="backToUsersPage()"
|
|
||||||
:label="$t('back')"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-if="activeUser.data.id"
|
|
||||||
@click="updateUser()"
|
|
||||||
color="primary"
|
|
||||||
:label="$t('update_account')"
|
|
||||||
class="q-ml-md"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
@click="createUser()"
|
|
||||||
:label="$t('create_account')"
|
|
||||||
color="primary"
|
|
||||||
class="float-right"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-card v-if="activeUser.show" class="q-pa-md">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">
|
|
||||||
<span v-if="activeUser.data.id" v-text="$t('update_account')"></span>
|
|
||||||
<span v-else v-text="$t('create_account')"></span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-input
|
|
||||||
v-if="activeUser.data.id"
|
|
||||||
v-model="activeUser.data.id"
|
|
||||||
:label="$t('user_id')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
readonly
|
|
||||||
:type="activeUser.data.showUserId ? 'text': 'password'"
|
|
||||||
class="q-mb-md"
|
|
||||||
><q-btn
|
|
||||||
@click="activeUser.data.showUserId = !activeUser.data.showUserId"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:icon="activeUser.data.showUserId ? 'visibility_off' : 'visibility'"
|
|
||||||
color="grey"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
|
||||||
v-model="activeUser.data.username"
|
|
||||||
:label="$t('username')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<q-toggle
|
|
||||||
size="xs"
|
|
||||||
v-if="!activeUser.data.id"
|
|
||||||
color="secondary"
|
|
||||||
:label="$t('set_password')"
|
|
||||||
v-model="activeUser.setPassword"
|
|
||||||
>
|
|
||||||
<q-tooltip v-text="$t('set_password_tooltip')"></q-tooltip>
|
|
||||||
</q-toggle>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-if="activeUser.setPassword"
|
|
||||||
v-model="activeUser.data.password"
|
|
||||||
:type="activeUser.data.showPassword ? 'text': 'password'"
|
|
||||||
autocomplete="off"
|
|
||||||
:label="$t('password')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="activeUser.data.showPassword = !activeUser.data.showPassword"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:icon="activeUser.data.showPassword ? 'visibility_off' : 'visibility'"
|
|
||||||
color="grey"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
|
||||||
v-if="activeUser.setPassword"
|
|
||||||
v-model="activeUser.data.password_repeat"
|
|
||||||
:type="activeUser.data.showPassword ? 'text': 'password'"
|
|
||||||
type="password"
|
|
||||||
autocomplete="off"
|
|
||||||
:label="$t('password_repeat')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="activeUser.data.showPassword = !activeUser.data.showPassword"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:icon="activeUser.data.showPassword ? 'visibility_off' : 'visibility'"
|
|
||||||
color="grey"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-model="activeUser.data.pubkey"
|
|
||||||
:label="'Nostr '+ $t('pubkey')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
<q-tooltip v-text="$t('nostr_pubkey_tooltip')"></q-tooltip>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
|
||||||
v-model="activeUser.data.email"
|
|
||||||
:label="$t('email')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section v-if="activeUser.data.extra">
|
|
||||||
<q-input
|
|
||||||
v-model="activeUser.data.extra.first_name"
|
|
||||||
:label="$t('first_name')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
|
||||||
v-model="activeUser.data.extra.last_name"
|
|
||||||
:label="$t('last_name')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
|
||||||
v-model="activeUser.data.extra.provider"
|
|
||||||
:label="$t('auth_provider')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
|
||||||
v-model="activeUser.data.external_id"
|
|
||||||
:label="$t('external_id')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
|
||||||
v-model="activeUser.data.extra.picture"
|
|
||||||
:label="$t('picture')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="activeUser.data.extensions"
|
|
||||||
multiple
|
|
||||||
label="User extensions"
|
|
||||||
:options="g.extensions"
|
|
||||||
></q-select>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section v-if="activeUser.data.id">
|
|
||||||
<q-btn
|
|
||||||
@click="resetPassword(activeUser.data.id)"
|
|
||||||
:disable="activeUser.data.is_super_user"
|
|
||||||
:label="$t('reset_password')"
|
|
||||||
icon="refresh"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
<q-tooltip>Generate and copy password reset url</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
@click="deleteUser(activeUser.data.id)"
|
|
||||||
:disable="activeUser.data.is_super_user"
|
|
||||||
:label="$t('delete')"
|
|
||||||
icon="delete"
|
|
||||||
color="negative"
|
|
||||||
class="float-right"
|
|
||||||
>
|
|
||||||
<q-tooltip>Delete User</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
<div v-if="paymentPage.show">
|
|
||||||
<div class="row q-mb-lg">
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
|
||||||
icon="arrow_back_ios"
|
|
||||||
@click="paymentPage.show = false"
|
|
||||||
:label="$t('back')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-card class="q-pa-md">
|
|
||||||
<q-card-section>
|
|
||||||
<payment-list :wallet="paymentsWallet" />
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="activeWallet.show">
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-card>
|
|
||||||
<div class="q-pa-sm">
|
|
||||||
<div class="row">
|
|
||||||
<div class="q-pa-xs">
|
|
||||||
<q-btn
|
|
||||||
icon="arrow_back_ios"
|
|
||||||
@click="backToUsersPage()"
|
|
||||||
:label="$t('back')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="q-pa-xs">
|
|
||||||
<q-btn
|
|
||||||
@click="createWalletDialog.show = true"
|
|
||||||
:label="$t('create_new_wallet')"
|
|
||||||
color="primary"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="q-pa-xs">
|
|
||||||
<q-btn
|
|
||||||
@click="deleteAllUserWallets(activeWallet.userId)"
|
|
||||||
:label="$t('delete_all_wallets')"
|
|
||||||
icon="delete"
|
|
||||||
color="negative"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-card class="q-pa-md">
|
|
||||||
<h2 class="text-h6 q-mb-md">Wallets</h2>
|
|
||||||
<q-table :rows="wallets" :columns="walletTable.columns">
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width v-if="g.user.super_user"></q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th
|
|
||||||
auto-width
|
|
||||||
v-for="col in props.cols"
|
|
||||||
v-text="col.label"
|
|
||||||
:key="col.name"
|
|
||||||
:props="props"
|
|
||||||
></q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width v-if="g.user.super_user">
|
|
||||||
<lnbits-update-balance
|
|
||||||
:wallet_id="props.row.id"
|
|
||||||
@credit-value="handleBalanceUpdate"
|
|
||||||
class="q-mr-md"
|
|
||||||
></lnbits-update-balance>
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="menu"
|
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
@click="showPayments(props.row.id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Show Payments</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
v-if="!props.row.deleted"
|
|
||||||
icon="vpn_key"
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
class="q-ml-xs"
|
|
||||||
@click="copyText(props.row.adminkey)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy Admin Key</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
v-if="!props.row.deleted"
|
|
||||||
icon="vpn_key"
|
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
class="q-ml-xs"
|
|
||||||
@click="copyText(props.row.inkey)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy Invoice Key</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="delete"
|
|
||||||
size="sm"
|
|
||||||
color="negative"
|
|
||||||
class="q-ml-xs"
|
|
||||||
@click="deleteUserWallet(props.row.user, props.row.id, props.row.deleted)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Delete Wallet</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
v-if="props.row.deleted"
|
|
||||||
icon="toggle_off"
|
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
class="q-ml-xs"
|
|
||||||
@click="undeleteUserWallet(props.row.user, props.row.id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Undelete Wallet</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
icon="link"
|
|
||||||
size="sm"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-mr-xs"
|
|
||||||
@click="copyWalletLink(props.row.id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy Wallet Link</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<span
|
|
||||||
v-text="props.row.name"
|
|
||||||
v-if="!props.row.editable"
|
|
||||||
:class="props.row.deleted ? 'text-strike' : 'cursor-pointer'"
|
|
||||||
@click="props.row.editable = true && !props.row.deleted"
|
|
||||||
></span>
|
|
||||||
<q-input
|
|
||||||
v-else
|
|
||||||
@keydown.enter="updateWallet(props.row)"
|
|
||||||
v-model="props.row.name"
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-btn
|
|
||||||
@click="updateWallet(props.row)"
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
flat
|
|
||||||
icon="send"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
icon="content_copy"
|
|
||||||
size="sm"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-mr-xs"
|
|
||||||
@click="copyText(props.row.id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy Wallet ID</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-text="props.row.id"
|
|
||||||
:class="props.row.deleted ? 'text-strike' : ''"
|
|
||||||
></span>
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td auto-width v-text="props.row.currency"></q-td>
|
|
||||||
<q-td auto-width v-text="formatSat(props.row.balance_msat)"></q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
|
||||||
<!---->
|
|
||||||
{% from "macros.jinja" import window_vars with context %}
|
|
||||||
<!---->
|
|
||||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
|
||||||
<div class="col">
|
|
||||||
{% include "users/_manageWallet.html" %}
|
|
||||||
<div v-if="activeUser.show" class="row">
|
|
||||||
<div class="col-12 col-md-6">{%include "users/_manageUser.html" %}</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="activeWallet.show">
|
|
||||||
{%include "users/_createWalletDialog.html" %}
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-card>
|
|
||||||
<div class="q-pa-sm">
|
|
||||||
<div class="row items-center justify-between q-gutter-xs">
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
|
||||||
@click="showAccountPage()"
|
|
||||||
:label="$t('create_account')"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
v-if="g.user.admin"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
icon="settings"
|
|
||||||
to="/admin#users"
|
|
||||||
>
|
|
||||||
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-card class="q-pa-md">
|
|
||||||
<q-table
|
|
||||||
row-key="id"
|
|
||||||
:rows="users"
|
|
||||||
:columns="usersTable.columns"
|
|
||||||
v-model:pagination="usersTable.pagination"
|
|
||||||
:no-data-label="$t('no_users')"
|
|
||||||
:filter="usersTable.search"
|
|
||||||
:loading="usersTable.loading"
|
|
||||||
@request="fetchUsers"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
<q-input
|
|
||||||
v-if="['user', 'username', 'email', 'pubkey', 'wallet_id'].includes(col.name)"
|
|
||||||
v-model="searchData[col.name]"
|
|
||||||
@keydown.enter="searchUserBy(col.name)"
|
|
||||||
dense
|
|
||||||
type="text"
|
|
||||||
filled
|
|
||||||
:label="col.label"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
name="search"
|
|
||||||
@click="searchUserBy(col.name)"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<span v-else v-text="col.label"></span>
|
|
||||||
</q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr auto-width :props="props">
|
|
||||||
<q-td>
|
|
||||||
<q-btn
|
|
||||||
@click="showAccountPage(props.row.id)"
|
|
||||||
round
|
|
||||||
icon="edit"
|
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
class="q-ml-xs"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="$t('update_account')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
|
||||||
<q-td>
|
|
||||||
<q-toggle
|
|
||||||
size="xs"
|
|
||||||
v-if="!props.row.is_super_user"
|
|
||||||
color="secondary"
|
|
||||||
v-model="props.row.is_admin"
|
|
||||||
@update:model-value="toggleAdmin(props.row.id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Toggle Admin</q-tooltip>
|
|
||||||
</q-toggle>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
v-if="props.row.is_super_user"
|
|
||||||
icon="verified"
|
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
class="q-ml-xs"
|
|
||||||
>
|
|
||||||
<q-tooltip>Super User</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td>
|
|
||||||
<q-btn
|
|
||||||
icon="list"
|
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
:label="props.row.wallet_count"
|
|
||||||
@click="fetchWallets(props.row.id)"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-if="(users.length == 1) && searchData.wallet_id"
|
|
||||||
round
|
|
||||||
icon="menu"
|
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
class="q-ml-sm"
|
|
||||||
@click="showWalletPayments(searchData.wallet_id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Show Payments</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td>
|
|
||||||
<q-btn
|
|
||||||
icon="content_copy"
|
|
||||||
size="sm"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-mr-xs"
|
|
||||||
@click="copyText(props.row.id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy User ID</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<span v-text="shortify(props.row.id)"></span>
|
|
||||||
</q-td>
|
|
||||||
<q-td v-text="props.row.username"></q-td>
|
|
||||||
|
|
||||||
<q-td v-text="props.row.email"></q-td>
|
|
||||||
|
|
||||||
<q-td>
|
|
||||||
<q-btn
|
|
||||||
v-if="props.row.pubkey"
|
|
||||||
icon="content_copy"
|
|
||||||
size="sm"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-mr-xs"
|
|
||||||
@click="copyText(props.row.pubkey)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy Public Key</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<span v-text="shortify(props.row.pubkey)"></span>
|
|
||||||
</q-td>
|
|
||||||
<q-td v-text="formatSat(props.row.balance_msat)"></q-td>
|
|
||||||
|
|
||||||
<q-td v-text="props.row.transaction_count"></q-td>
|
|
||||||
|
|
||||||
<q-td v-text="formatDate(props.row.last_payment)"></q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
+16
-108
@@ -1,20 +1,15 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
from shutil import make_archive
|
||||||
from shutil import make_archive, move
|
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
from typing import IO
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import filetype
|
from fastapi import APIRouter, Depends, File
|
||||||
from fastapi import APIRouter, Depends, File, Header, HTTPException, UploadFile
|
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from lnbits.core.models import User
|
|
||||||
from lnbits.core.models.misc import Image, SimpleStatus
|
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
|
from lnbits.core.models.users import Account
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
enqueue_admin_notification,
|
enqueue_admin_notification,
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
@@ -23,7 +18,6 @@ from lnbits.core.services import (
|
|||||||
from lnbits.core.services.notifications import send_email_notification
|
from lnbits.core.services.notifications import send_email_notification
|
||||||
from lnbits.core.services.settings import dict_to_settings
|
from lnbits.core.services.settings import dict_to_settings
|
||||||
from lnbits.decorators import check_admin, check_super_user
|
from lnbits.decorators import check_admin, check_super_user
|
||||||
from lnbits.helpers import safe_upload_file_path
|
|
||||||
from lnbits.server import server_restart
|
from lnbits.server import server_restart
|
||||||
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||||
from lnbits.tasks import invoice_listeners
|
from lnbits.tasks import invoice_listeners
|
||||||
@@ -73,9 +67,9 @@ async def api_test_email():
|
|||||||
|
|
||||||
@admin_router.get("/api/v1/settings")
|
@admin_router.get("/api/v1/settings")
|
||||||
async def api_get_settings(
|
async def api_get_settings(
|
||||||
user: User = Depends(check_admin),
|
account: Account = Depends(check_admin),
|
||||||
) -> AdminSettings | None:
|
) -> AdminSettings | None:
|
||||||
admin_settings = await get_admin_settings(user.super_user)
|
admin_settings = await get_admin_settings(account.is_super_user)
|
||||||
return admin_settings
|
return admin_settings
|
||||||
|
|
||||||
|
|
||||||
@@ -83,12 +77,14 @@ async def api_get_settings(
|
|||||||
"/api/v1/settings",
|
"/api/v1/settings",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
|
async def api_update_settings(
|
||||||
|
data: UpdateSettings, account: Account = Depends(check_admin)
|
||||||
|
):
|
||||||
enqueue_admin_notification(
|
enqueue_admin_notification(
|
||||||
NotificationType.settings_update, {"username": user.username}
|
NotificationType.settings_update, {"username": account.username}
|
||||||
)
|
)
|
||||||
await update_admin_settings(data)
|
await update_admin_settings(data)
|
||||||
admin_settings = await get_admin_settings(user.super_user)
|
admin_settings = await get_admin_settings(account.is_super_user)
|
||||||
if not admin_settings:
|
if not admin_settings:
|
||||||
raise ValueError("Updated admin settings not found.")
|
raise ValueError("Updated admin settings not found.")
|
||||||
update_cached_settings(admin_settings.dict())
|
update_cached_settings(admin_settings.dict())
|
||||||
@@ -100,9 +96,11 @@ async def api_update_settings(data: UpdateSettings, user: User = Depends(check_a
|
|||||||
"/api/v1/settings",
|
"/api/v1/settings",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
async def api_update_settings_partial(data: dict, user: User = Depends(check_admin)):
|
async def api_update_settings_partial(
|
||||||
|
data: dict, account: Account = Depends(check_admin)
|
||||||
|
):
|
||||||
updatable_settings = dict_to_settings({**settings.dict(), **data})
|
updatable_settings = dict_to_settings({**settings.dict(), **data})
|
||||||
return await api_update_settings(updatable_settings, user)
|
return await api_update_settings(updatable_settings, account)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get(
|
@admin_router.get(
|
||||||
@@ -116,9 +114,9 @@ async def api_reset_settings(field_name: str):
|
|||||||
|
|
||||||
|
|
||||||
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
||||||
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
async def api_delete_settings(account: Account = Depends(check_super_user)) -> None:
|
||||||
enqueue_admin_notification(
|
enqueue_admin_notification(
|
||||||
NotificationType.settings_update, {"username": user.username}
|
NotificationType.settings_update, {"username": account.username}
|
||||||
)
|
)
|
||||||
await reset_core_settings()
|
await reset_core_settings()
|
||||||
server_restart.set()
|
server_restart.set()
|
||||||
@@ -172,93 +170,3 @@ async def api_download_backup() -> FileResponse:
|
|||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.post(
|
|
||||||
"/api/v1/images",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def upload_image(
|
|
||||||
file: UploadFile = file_upload,
|
|
||||||
content_length: int = Header(..., le=settings.lnbits_upload_size_bytes),
|
|
||||||
) -> Image:
|
|
||||||
if not file.filename:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="No filename provided."
|
|
||||||
)
|
|
||||||
|
|
||||||
# validate file types
|
|
||||||
file_info = filetype.guess(file.file)
|
|
||||||
if file_info is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE,
|
|
||||||
detail="Unable to determine file type",
|
|
||||||
)
|
|
||||||
detected_content_type = file_info.extension.lower()
|
|
||||||
if (
|
|
||||||
file.content_type not in settings.lnbits_upload_allowed_types
|
|
||||||
or detected_content_type not in settings.lnbits_upload_allowed_types
|
|
||||||
):
|
|
||||||
raise HTTPException(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported file type")
|
|
||||||
|
|
||||||
# validate file name
|
|
||||||
try:
|
|
||||||
file_path = safe_upload_file_path(file.filename)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail=f"The requested filename '{file.filename}' is forbidden.",
|
|
||||||
) from e
|
|
||||||
|
|
||||||
# validate file size
|
|
||||||
real_file_size = 0
|
|
||||||
temp: IO = NamedTemporaryFile(delete=False)
|
|
||||||
for chunk in file.file:
|
|
||||||
real_file_size += len(chunk)
|
|
||||||
if real_file_size > content_length:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
|
|
||||||
detail=f"File too large ({content_length / 1000} KB max)",
|
|
||||||
)
|
|
||||||
temp.write(chunk)
|
|
||||||
temp.close()
|
|
||||||
|
|
||||||
move(temp.name, file_path)
|
|
||||||
return Image(filename=file.filename)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get(
|
|
||||||
"/api/v1/images",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def list_uploaded_images() -> list[Image]:
|
|
||||||
image_folder = Path(settings.lnbits_data_folder, "images")
|
|
||||||
files = image_folder.glob("*")
|
|
||||||
images = []
|
|
||||||
for file in files:
|
|
||||||
if file.is_file():
|
|
||||||
images.append(Image(filename=file.name))
|
|
||||||
return images
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.delete(
|
|
||||||
"/api/v1/images/{filename}",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def delete_uploaded_image(filename: str) -> SimpleStatus:
|
|
||||||
try:
|
|
||||||
file_path = safe_upload_file_path(filename)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail=f"The requested filename '{filename}' is forbidden.",
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if not file_path.exists():
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Image not found.")
|
|
||||||
|
|
||||||
file_path.unlink()
|
|
||||||
return SimpleStatus(success=True, message=f"{filename} deleted")
|
|
||||||
|
|||||||
@@ -8,13 +8,17 @@ from fastapi import APIRouter, Depends
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
BaseWallet,
|
|
||||||
ConversionData,
|
ConversionData,
|
||||||
CreateWallet,
|
CreateWallet,
|
||||||
User,
|
User,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.core.models.users import AccountId
|
||||||
|
from lnbits.decorators import (
|
||||||
|
check_account_exists,
|
||||||
|
check_account_id_exists,
|
||||||
|
check_user_exists,
|
||||||
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.exchange_rates import (
|
from lnbits.utils.exchange_rates import (
|
||||||
allowed_currencies,
|
allowed_currencies,
|
||||||
@@ -39,7 +43,9 @@ async def health() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
@api_router.get("/api/v1/status", status_code=HTTPStatus.OK)
|
@api_router.get("/api/v1/status", status_code=HTTPStatus.OK)
|
||||||
async def health_check(user: User = Depends(check_user_exists)) -> dict:
|
async def health_check(
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
|
) -> dict:
|
||||||
stat: dict[str, Any] = {
|
stat: dict[str, Any] = {
|
||||||
"server_time": int(time()),
|
"server_time": int(time()),
|
||||||
"up_time": settings.lnbits_server_up_time,
|
"up_time": settings.lnbits_server_up_time,
|
||||||
@@ -47,7 +53,7 @@ async def health_check(user: User = Depends(check_user_exists)) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
stat["version"] = settings.version
|
stat["version"] = settings.version
|
||||||
if not user.admin:
|
if not account_id.is_admin_id:
|
||||||
return stat
|
return stat
|
||||||
|
|
||||||
funding_source = get_funding_source()
|
funding_source = get_funding_source()
|
||||||
@@ -64,7 +70,6 @@ async def health_check(user: User = Depends(check_user_exists)) -> dict:
|
|||||||
"/api/v1/wallets",
|
"/api/v1/wallets",
|
||||||
name="Wallets",
|
name="Wallets",
|
||||||
description="Get basic info for all of user's wallets.",
|
description="Get basic info for all of user's wallets.",
|
||||||
response_model=list[BaseWallet],
|
|
||||||
)
|
)
|
||||||
async def api_wallets(user: User = Depends(check_user_exists)) -> list[Wallet]:
|
async def api_wallets(user: User = Depends(check_user_exists)) -> list[Wallet]:
|
||||||
return user.wallets
|
return user.wallets
|
||||||
@@ -78,7 +83,7 @@ async def api_create_account(data: CreateWallet) -> Wallet:
|
|||||||
|
|
||||||
@api_router.get(
|
@api_router.get(
|
||||||
"/api/v1/rate/history",
|
"/api/v1/rate/history",
|
||||||
dependencies=[Depends(check_user_exists)],
|
dependencies=[Depends(check_account_exists)],
|
||||||
)
|
)
|
||||||
async def api_exchange_rate_history() -> list[dict]:
|
async def api_exchange_rate_history() -> list[dict]:
|
||||||
return settings.lnbits_exchange_rate_history
|
return settings.lnbits_exchange_rate_history
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import base64
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile
|
||||||
|
|
||||||
|
from lnbits.core.crud.assets import (
|
||||||
|
delete_user_asset,
|
||||||
|
get_asset_info,
|
||||||
|
get_public_asset,
|
||||||
|
get_public_asset_info,
|
||||||
|
get_user_asset,
|
||||||
|
get_user_asset_info,
|
||||||
|
get_user_assets,
|
||||||
|
update_user_asset_info,
|
||||||
|
)
|
||||||
|
from lnbits.core.models.assets import AssetFilters, AssetInfo, AssetUpdate
|
||||||
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
|
from lnbits.core.models.users import AccountId
|
||||||
|
from lnbits.core.services.assets import create_user_asset
|
||||||
|
from lnbits.db import Filters, Page
|
||||||
|
from lnbits.decorators import (
|
||||||
|
check_account_id_exists,
|
||||||
|
optional_user_id,
|
||||||
|
parse_filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
asset_router = APIRouter(prefix="/api/v1/assets", tags=["Assets"])
|
||||||
|
|
||||||
|
upload_file_param = File(...)
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.get(
|
||||||
|
"/paginated",
|
||||||
|
name="Get user assets",
|
||||||
|
summary="Get paginated list user assets",
|
||||||
|
)
|
||||||
|
async def api_get_user_assets(
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
|
filters: Filters = Depends(parse_filters(AssetFilters)),
|
||||||
|
) -> Page[AssetInfo]:
|
||||||
|
return await get_user_assets(account_id.id, filters=filters)
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.get(
|
||||||
|
"/{asset_id}",
|
||||||
|
name="Get user asset",
|
||||||
|
summary="Get user asset by ID",
|
||||||
|
)
|
||||||
|
async def api_get_asset(
|
||||||
|
asset_id: str,
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
|
) -> AssetInfo:
|
||||||
|
asset_info = await get_user_asset_info(account_id.id, asset_id)
|
||||||
|
if not asset_info:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
return asset_info
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.get(
|
||||||
|
"/{asset_id}/binary",
|
||||||
|
name="Get user asset binary",
|
||||||
|
summary="Get user asset binary data by ID",
|
||||||
|
)
|
||||||
|
async def api_get_asset_binary(
|
||||||
|
asset_id: str,
|
||||||
|
user_id: str | None = Depends(optional_user_id),
|
||||||
|
) -> Response:
|
||||||
|
asset = None
|
||||||
|
if user_id:
|
||||||
|
asset = await get_user_asset(user_id, asset_id)
|
||||||
|
|
||||||
|
if not asset:
|
||||||
|
asset = await get_public_asset(asset_id)
|
||||||
|
|
||||||
|
if not asset:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
content=asset.data,
|
||||||
|
media_type=asset.mime_type,
|
||||||
|
headers={"Content-Disposition": f'inline; filename="{asset.name}"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.get(
|
||||||
|
"/{asset_id}/thumbnail",
|
||||||
|
name="Get user asset thumbnail",
|
||||||
|
summary="Get user asset thumbnail data by ID",
|
||||||
|
)
|
||||||
|
async def api_get_asset_thumbnail(
|
||||||
|
asset_id: str,
|
||||||
|
user_id: str | None = Depends(optional_user_id),
|
||||||
|
) -> Response:
|
||||||
|
asset_info = None
|
||||||
|
if user_id:
|
||||||
|
asset_info = await get_user_asset_info(user_id, asset_id)
|
||||||
|
|
||||||
|
if not asset_info:
|
||||||
|
asset_info = await get_public_asset_info(asset_id)
|
||||||
|
|
||||||
|
if not asset_info:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
content=(
|
||||||
|
base64.b64decode(asset_info.thumbnail_base64)
|
||||||
|
if asset_info.thumbnail_base64
|
||||||
|
else b""
|
||||||
|
),
|
||||||
|
media_type=asset_info.mime_type,
|
||||||
|
headers={"Content-Disposition": f'inline; filename="{asset_info.name}"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.put(
|
||||||
|
"/{asset_id}",
|
||||||
|
name="Update user asset",
|
||||||
|
summary="Update user asset by ID",
|
||||||
|
)
|
||||||
|
async def api_update_asset(
|
||||||
|
asset_id: str,
|
||||||
|
data: AssetUpdate,
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
|
) -> AssetInfo:
|
||||||
|
if account_id.is_admin_id:
|
||||||
|
asset_info = await get_asset_info(asset_id)
|
||||||
|
else:
|
||||||
|
asset_info = await get_user_asset_info(account_id.id, asset_id)
|
||||||
|
|
||||||
|
if not asset_info:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
|
||||||
|
asset_info.name = data.name or asset_info.name
|
||||||
|
asset_info.is_public = (
|
||||||
|
asset_info.is_public if data.is_public is None else data.is_public
|
||||||
|
)
|
||||||
|
await update_user_asset_info(asset_info)
|
||||||
|
return asset_info
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.post(
|
||||||
|
"",
|
||||||
|
name="Upload",
|
||||||
|
summary="Upload user assets",
|
||||||
|
)
|
||||||
|
async def api_upload_asset(
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
|
file: UploadFile = upload_file_param,
|
||||||
|
public_asset: bool = False,
|
||||||
|
) -> AssetInfo:
|
||||||
|
asset = await create_user_asset(account_id.id, file, public_asset)
|
||||||
|
|
||||||
|
asset_info = await get_user_asset_info(account_id.id, asset.id)
|
||||||
|
if not asset_info:
|
||||||
|
raise ValueError("Failed to retrieve asset info after upload.")
|
||||||
|
|
||||||
|
return asset_info
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.delete(
|
||||||
|
"/{asset_id}",
|
||||||
|
name="Delete user asset",
|
||||||
|
summary="Delete user asset by ID",
|
||||||
|
)
|
||||||
|
async def api_delete_asset(
|
||||||
|
asset_id: str,
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
|
) -> SimpleStatus:
|
||||||
|
asset = await get_user_asset(account_id.id, asset_id)
|
||||||
|
if not asset:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
|
||||||
|
await delete_user_asset(account_id.id, asset_id)
|
||||||
|
return SimpleStatus(success=True, message="Asset deleted successfully.")
|
||||||
@@ -25,7 +25,12 @@ from lnbits.core.models.users import (
|
|||||||
UpdateAccessControlList,
|
UpdateAccessControlList,
|
||||||
)
|
)
|
||||||
from lnbits.core.services import create_user_account
|
from lnbits.core.services import create_user_account
|
||||||
from lnbits.decorators import access_token_payload, check_user_exists
|
from lnbits.core.services.users import update_user_account
|
||||||
|
from lnbits.decorators import (
|
||||||
|
access_token_payload,
|
||||||
|
check_account_exists,
|
||||||
|
check_user_exists,
|
||||||
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
decrypt_internal_message,
|
decrypt_internal_message,
|
||||||
@@ -118,11 +123,11 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
|||||||
@auth_router.get("/acl")
|
@auth_router.get("/acl")
|
||||||
async def api_get_user_acls(
|
async def api_get_user_acls(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: User = Depends(check_user_exists),
|
account: Account = Depends(check_account_exists),
|
||||||
) -> UserAcls:
|
) -> UserAcls:
|
||||||
api_routes = get_api_routes(request.app.router.routes)
|
api_routes = get_api_routes(request.app.router.routes)
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
acls = await get_user_access_control_lists(account.id)
|
||||||
|
|
||||||
# Add missing/new endpoints to the ACLs
|
# Add missing/new endpoints to the ACLs
|
||||||
for acl in acls.access_control_list:
|
for acl in acls.access_control_list:
|
||||||
@@ -135,7 +140,7 @@ async def api_get_user_acls(
|
|||||||
acl.endpoints.append(EndpointAccess(path=path, name=name))
|
acl.endpoints.append(EndpointAccess(path=path, name=name))
|
||||||
acl.endpoints.sort(key=lambda e: e.name.lower())
|
acl.endpoints.sort(key=lambda e: e.name.lower())
|
||||||
|
|
||||||
return UserAcls(id=user.id, access_control_list=acls.access_control_list)
|
return UserAcls(id=account.id, access_control_list=acls.access_control_list)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/acl")
|
@auth_router.put("/acl")
|
||||||
@@ -143,13 +148,13 @@ async def api_get_user_acls(
|
|||||||
async def api_update_user_acl(
|
async def api_update_user_acl(
|
||||||
request: Request,
|
request: Request,
|
||||||
data: UpdateAccessControlList,
|
data: UpdateAccessControlList,
|
||||||
user: User = Depends(check_user_exists),
|
account: Account = Depends(check_account_exists),
|
||||||
) -> UserAcls:
|
) -> UserAcls:
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
user_acls = await get_user_access_control_lists(user.id)
|
user_acls = await get_user_access_control_lists(account.id)
|
||||||
acl = user_acls.get_acl_by_id(data.id)
|
acl = user_acls.get_acl_by_id(data.id)
|
||||||
if acl:
|
if acl:
|
||||||
user_acls.access_control_list.remove(acl)
|
user_acls.access_control_list.remove(acl)
|
||||||
@@ -174,33 +179,30 @@ async def api_update_user_acl(
|
|||||||
|
|
||||||
@auth_router.delete("/acl")
|
@auth_router.delete("/acl")
|
||||||
async def api_delete_user_acl(
|
async def api_delete_user_acl(
|
||||||
data: DeleteAccessControlList,
|
data: DeleteAccessControlList, account: Account = Depends(check_account_exists)
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
):
|
):
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
user_acls = await get_user_access_control_lists(user.id)
|
user_acls = await get_user_access_control_lists(account.id)
|
||||||
user_acls.delete_acl_by_id(data.id)
|
user_acls.delete_acl_by_id(data.id)
|
||||||
await update_user_access_control_list(user_acls)
|
await update_user_access_control_list(user_acls)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.post("/acl/token")
|
@auth_router.post("/acl/token")
|
||||||
async def api_create_user_api_token(
|
async def api_create_user_api_token(
|
||||||
data: ApiTokenRequest,
|
data: ApiTokenRequest, account: Account = Depends(check_account_exists)
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
) -> ApiTokenResponse:
|
) -> ApiTokenResponse:
|
||||||
if not data.expiration_time_minutes > 0:
|
if not data.expiration_time_minutes > 0:
|
||||||
raise ValueError("Expiration time must be in the future.")
|
raise ValueError("Expiration time must be in the future.")
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
if not account.username:
|
if not account.username:
|
||||||
raise ValueError("Username must be configured.")
|
raise ValueError("Username must be configured.")
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
acls = await get_user_access_control_lists(account.id)
|
||||||
acl = acls.get_acl_by_id(data.acl_id)
|
acl = acls.get_acl_by_id(data.acl_id)
|
||||||
if not acl:
|
if not acl:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
||||||
@@ -217,18 +219,16 @@ async def api_create_user_api_token(
|
|||||||
|
|
||||||
@auth_router.delete("/acl/token")
|
@auth_router.delete("/acl/token")
|
||||||
async def api_delete_user_api_token(
|
async def api_delete_user_api_token(
|
||||||
data: DeleteTokenRequest,
|
data: DeleteTokenRequest, account: Account = Depends(check_account_exists)
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
):
|
):
|
||||||
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
if not account.username:
|
if not account.username:
|
||||||
raise ValueError("Username must be configured.")
|
raise ValueError("Username must be configured.")
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
acls = await get_user_access_control_lists(account.id)
|
||||||
acl = acls.get_acl_by_id(data.acl_id)
|
acl = acls.get_acl_by_id(data.acl_id)
|
||||||
if not acl:
|
if not acl:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
||||||
@@ -317,24 +317,20 @@ async def register(data: RegisterUser) -> JSONResponse:
|
|||||||
@auth_router.put("/pubkey")
|
@auth_router.put("/pubkey")
|
||||||
async def update_pubkey(
|
async def update_pubkey(
|
||||||
data: UpdateUserPubkey,
|
data: UpdateUserPubkey,
|
||||||
user: User = Depends(check_user_exists),
|
account: Account = Depends(check_account_exists),
|
||||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
if data.user_id != user.id:
|
if data.user_id != account.id:
|
||||||
raise ValueError("Invalid user ID.")
|
raise ValueError("Invalid user ID.")
|
||||||
|
|
||||||
_validate_auth_timeout(payload.auth_time)
|
_validate_auth_timeout(payload.auth_time)
|
||||||
if (
|
if (
|
||||||
data.pubkey
|
data.pubkey
|
||||||
and data.pubkey != user.pubkey
|
and data.pubkey != account.pubkey
|
||||||
and await get_account_by_pubkey(data.pubkey)
|
and await get_account_by_pubkey(data.pubkey)
|
||||||
):
|
):
|
||||||
raise ValueError("Public key already in use.")
|
raise ValueError("Public key already in use.")
|
||||||
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account:
|
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Account not found.")
|
|
||||||
|
|
||||||
account.pubkey = normalize_public_key(data.pubkey)
|
account.pubkey = normalize_public_key(data.pubkey)
|
||||||
await update_account(account)
|
await update_account(account)
|
||||||
return await get_user_from_account(account)
|
return await get_user_from_account(account)
|
||||||
@@ -343,23 +339,19 @@ async def update_pubkey(
|
|||||||
@auth_router.put("/password")
|
@auth_router.put("/password")
|
||||||
async def update_password(
|
async def update_password(
|
||||||
data: UpdateUserPassword,
|
data: UpdateUserPassword,
|
||||||
user: User = Depends(check_user_exists),
|
account: Account = Depends(check_account_exists),
|
||||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
_validate_auth_timeout(payload.auth_time)
|
_validate_auth_timeout(payload.auth_time)
|
||||||
if data.user_id != user.id:
|
if data.user_id != account.id:
|
||||||
raise ValueError("Invalid user ID.")
|
raise ValueError("Invalid user ID.")
|
||||||
if (
|
if (
|
||||||
data.username
|
data.username
|
||||||
and user.username != data.username
|
and account.username != data.username
|
||||||
and await get_account_by_username(data.username)
|
and await get_account_by_username(data.username)
|
||||||
):
|
):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||||
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account:
|
|
||||||
raise ValueError("Account not found.")
|
|
||||||
|
|
||||||
# old accounts do not have a password
|
# old accounts do not have a password
|
||||||
if account.password_hash:
|
if account.password_hash:
|
||||||
if not data.password_old:
|
if not data.password_old:
|
||||||
@@ -418,30 +410,17 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
|||||||
|
|
||||||
@auth_router.put("/update")
|
@auth_router.put("/update")
|
||||||
async def update(
|
async def update(
|
||||||
data: UpdateUser, user: User = Depends(check_user_exists)
|
data: UpdateUser, account: Account = Depends(check_account_exists)
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
if data.user_id != user.id:
|
if data.user_id != account.id:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
||||||
if data.username and not is_valid_username(data.username):
|
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
|
||||||
|
|
||||||
if (
|
|
||||||
data.username
|
|
||||||
and user.username != data.username
|
|
||||||
and await get_account_by_username(data.username)
|
|
||||||
):
|
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
|
||||||
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account:
|
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Account not found.")
|
|
||||||
|
|
||||||
if data.username:
|
if data.username:
|
||||||
account.username = data.username
|
account.username = data.username
|
||||||
if data.extra:
|
if data.extra:
|
||||||
account.extra = data.extra
|
account.extra = data.extra
|
||||||
|
|
||||||
await update_account(account)
|
await update_user_account(account)
|
||||||
return await get_user_from_account(account)
|
return await get_user_from_account(account)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from lnbits.core.models.misc import SimpleStatus
|
|||||||
from lnbits.core.models.payments import CreateInvoice
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
from lnbits.core.services.fiat_providers import (
|
from lnbits.core.services.fiat_providers import (
|
||||||
check_stripe_signature,
|
check_stripe_signature,
|
||||||
|
verify_paypal_webhook,
|
||||||
)
|
)
|
||||||
from lnbits.core.services.payments import create_fiat_invoice
|
from lnbits.core.services.payments import create_fiat_invoice
|
||||||
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
|
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
|
||||||
@@ -37,6 +38,17 @@ async def api_generic_webhook_handler(
|
|||||||
message=f"Callback received successfully from '{provider_name}'.",
|
message=f"Callback received successfully from '{provider_name}'.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if provider_name.lower() == "paypal":
|
||||||
|
payload = await request.body()
|
||||||
|
await verify_paypal_webhook(request.headers, payload)
|
||||||
|
event = await request.json()
|
||||||
|
await handle_paypal_event(event)
|
||||||
|
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True,
|
||||||
|
message=f"Callback received successfully from '{provider_name}'.",
|
||||||
|
)
|
||||||
|
|
||||||
return SimpleStatus(
|
return SimpleStatus(
|
||||||
success=False,
|
success=False,
|
||||||
message=f"Unknown fiat provider '{provider_name}'.",
|
message=f"Unknown fiat provider '{provider_name}'.",
|
||||||
@@ -48,6 +60,8 @@ async def handle_stripe_event(event: dict):
|
|||||||
event_type = event.get("type")
|
event_type = event.get("type")
|
||||||
if event_type == "checkout.session.completed":
|
if event_type == "checkout.session.completed":
|
||||||
await _handle_stripe_checkout_session_completed(event)
|
await _handle_stripe_checkout_session_completed(event)
|
||||||
|
elif event_type == "payment_intent.succeeded":
|
||||||
|
await _handle_stripe_intent_session_completed(event)
|
||||||
elif event_type == "invoice.paid":
|
elif event_type == "invoice.paid":
|
||||||
await _handle_stripe_subscription_invoice_paid(event)
|
await _handle_stripe_subscription_invoice_paid(event)
|
||||||
else:
|
else:
|
||||||
@@ -56,18 +70,38 @@ async def handle_stripe_event(event: dict):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_stripe_intent_session_completed(event: dict):
|
||||||
|
event_id = event.get("id")
|
||||||
|
event_object = event.get("data", {}).get("object", {})
|
||||||
|
object_type = event_object.get("object")
|
||||||
|
payment_hash = event_object.get("metadata", {}).get("payment_hash")
|
||||||
|
logger.debug(
|
||||||
|
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
|
||||||
|
f" Payment hash: '{payment_hash}'."
|
||||||
|
)
|
||||||
|
if not payment_hash:
|
||||||
|
logger.warning("Stripe event does not contain a payment hash.")
|
||||||
|
return
|
||||||
|
|
||||||
|
payment = await get_standalone_payment(payment_hash)
|
||||||
|
if not payment:
|
||||||
|
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||||
|
return
|
||||||
|
await payment.check_fiat_status()
|
||||||
|
|
||||||
|
|
||||||
async def _handle_stripe_checkout_session_completed(event: dict):
|
async def _handle_stripe_checkout_session_completed(event: dict):
|
||||||
event_id = event.get("id")
|
event_id = event.get("id")
|
||||||
event_object = event.get("data", {}).get("object", {})
|
event_object = event.get("data", {}).get("object", {})
|
||||||
object_type = event_object.get("object")
|
object_type = event_object.get("object")
|
||||||
payment_hash = event_object.get("metadata", {}).get("payment_hash")
|
payment_hash = event_object.get("metadata", {}).get("payment_hash")
|
||||||
lnbits_action = event_object.get("metadata", {}).get("lnbits_action")
|
alan_action = event_object.get("metadata", {}).get("alan_action")
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
|
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
|
||||||
f" Payment hash: '{payment_hash}'."
|
f" Payment hash: '{payment_hash}'."
|
||||||
)
|
)
|
||||||
if lnbits_action != "invoice":
|
if alan_action != "invoice":
|
||||||
logger.warning(f"Stripe event is not an invoice: '{lnbits_action}'.")
|
logger.warning(f"Stripe event is not an invoice: '{alan_action}'.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not payment_hash:
|
if not payment_hash:
|
||||||
@@ -132,7 +166,7 @@ async def _get_stripe_subscription_payment_options(
|
|||||||
|
|
||||||
metadata = parent.get("subscription_details", {}).get("metadata", {})
|
metadata = parent.get("subscription_details", {}).get("metadata", {})
|
||||||
|
|
||||||
if metadata.get("lnbits_action") != "subscription":
|
if metadata.get("alan_action") != "subscription":
|
||||||
raise ValueError("Stripe invoice.paid metadata action is not 'subscription'.")
|
raise ValueError("Stripe invoice.paid metadata action is not 'subscription'.")
|
||||||
|
|
||||||
if "extra" in metadata:
|
if "extra" in metadata:
|
||||||
@@ -143,3 +177,84 @@ async def _get_stripe_subscription_payment_options(
|
|||||||
metadata["extra"] = {}
|
metadata["extra"] = {}
|
||||||
|
|
||||||
return FiatSubscriptionPaymentOptions(**metadata)
|
return FiatSubscriptionPaymentOptions(**metadata)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_paypal_event(event: dict):
|
||||||
|
event_type = event.get("event_type", "")
|
||||||
|
resource = event.get("resource", {})
|
||||||
|
|
||||||
|
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
|
||||||
|
payment_hash = _paypal_extract_payment_hash(resource)
|
||||||
|
if not payment_hash:
|
||||||
|
logger.warning("PayPal event missing payment hash.")
|
||||||
|
return
|
||||||
|
payment = await get_standalone_payment(payment_hash)
|
||||||
|
if not payment:
|
||||||
|
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||||
|
return
|
||||||
|
await payment.check_fiat_status()
|
||||||
|
return
|
||||||
|
|
||||||
|
if event_type in (
|
||||||
|
"PAYMENT.SALE.COMPLETED",
|
||||||
|
"BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED",
|
||||||
|
):
|
||||||
|
await _handle_paypal_subscription_payment(resource)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Unhandled PayPal event type: '{event_type}'.")
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_paypal_subscription_payment(resource: dict):
|
||||||
|
amount_info = resource.get("amount") or {}
|
||||||
|
currency = (amount_info.get("currency_code") or "").upper()
|
||||||
|
total = amount_info.get("value")
|
||||||
|
if not currency or total is None:
|
||||||
|
raise ValueError("PayPal subscription event missing amount.")
|
||||||
|
|
||||||
|
custom_id = resource.get("custom_id") or resource.get("custom")
|
||||||
|
if not custom_id:
|
||||||
|
raise ValueError("PayPal subscription event missing custom metadata.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata = json.loads(custom_id)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
payment_options = FiatSubscriptionPaymentOptions(**metadata)
|
||||||
|
if not payment_options.wallet_id:
|
||||||
|
raise ValueError("PayPal subscription event missing wallet_id.")
|
||||||
|
|
||||||
|
memo = payment_options.memo or ""
|
||||||
|
extra = {
|
||||||
|
**(payment_options.extra or {}),
|
||||||
|
"fiat_method": "subscription",
|
||||||
|
"tag": payment_options.tag,
|
||||||
|
"subscription": {
|
||||||
|
"checking_id": resource.get("id") or resource.get("billing_agreement_id"),
|
||||||
|
"payment_request": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
payment = await create_fiat_invoice(
|
||||||
|
wallet_id=payment_options.wallet_id,
|
||||||
|
invoice_data=CreateInvoice(
|
||||||
|
unit=currency,
|
||||||
|
amount=float(total),
|
||||||
|
memo=memo,
|
||||||
|
extra=extra,
|
||||||
|
fiat_provider="paypal",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await payment.check_fiat_status()
|
||||||
|
|
||||||
|
|
||||||
|
def _paypal_extract_payment_hash(resource: dict) -> str | None:
|
||||||
|
purchase_units = resource.get("purchase_units") or []
|
||||||
|
for pu in purchase_units:
|
||||||
|
if pu.get("invoice_id"):
|
||||||
|
return pu.get("invoice_id")
|
||||||
|
if pu.get("custom_id"):
|
||||||
|
return pu.get("custom_id")
|
||||||
|
return None
|
||||||
|
|||||||
@@ -2,27 +2,35 @@ import sys
|
|||||||
import traceback
|
import traceback
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
import httpx
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.requests import Request
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_extensions
|
from lnbits.core.crud.extensions import get_user_extensions
|
||||||
|
from lnbits.core.crud.wallets import get_wallets_ids
|
||||||
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
SimpleStatus,
|
SimpleStatus,
|
||||||
User,
|
|
||||||
)
|
)
|
||||||
from lnbits.core.models.extensions import (
|
from lnbits.core.models.extensions import (
|
||||||
CreateExtension,
|
CreateExtension,
|
||||||
|
CreateExtensionReview,
|
||||||
Extension,
|
Extension,
|
||||||
ExtensionConfig,
|
ExtensionConfig,
|
||||||
ExtensionMeta,
|
ExtensionMeta,
|
||||||
ExtensionRelease,
|
ExtensionRelease,
|
||||||
|
ExtensionReview,
|
||||||
|
ExtensionReviewPaymentRequest,
|
||||||
|
ExtensionReviewsStatus,
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
PayToEnableInfo,
|
PayToEnableInfo,
|
||||||
ReleasePaymentInfo,
|
ReleasePaymentInfo,
|
||||||
UserExtension,
|
UserExtension,
|
||||||
UserExtensionInfo,
|
UserExtensionInfo,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.models.users import Account, AccountId
|
||||||
from lnbits.core.services import check_transaction_status, create_invoice
|
from lnbits.core.services import check_transaction_status, create_invoice
|
||||||
from lnbits.core.services.extensions import (
|
from lnbits.core.services.extensions import (
|
||||||
activate_extension,
|
activate_extension,
|
||||||
@@ -32,16 +40,20 @@ from lnbits.core.services.extensions import (
|
|||||||
install_extension,
|
install_extension,
|
||||||
uninstall_extension,
|
uninstall_extension,
|
||||||
)
|
)
|
||||||
|
from lnbits.db import Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
|
check_account_exists,
|
||||||
|
check_account_id_exists,
|
||||||
check_admin,
|
check_admin,
|
||||||
check_user_exists,
|
|
||||||
)
|
)
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_user_extension,
|
create_user_extension,
|
||||||
delete_dbversion,
|
delete_dbversion,
|
||||||
drop_extension_db,
|
drop_extension_db,
|
||||||
get_db_version,
|
get_db_version,
|
||||||
|
get_db_versions,
|
||||||
get_installed_extension,
|
get_installed_extension,
|
||||||
get_installed_extensions,
|
get_installed_extensions,
|
||||||
get_user_extension,
|
get_user_extension,
|
||||||
@@ -140,9 +152,10 @@ async def api_extension_details(
|
|||||||
async def api_update_pay_to_enable(
|
async def api_update_pay_to_enable(
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
data: PayToEnableInfo,
|
data: PayToEnableInfo,
|
||||||
user: User = Depends(check_admin),
|
account: Account = Depends(check_admin),
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
if data.wallet not in user.wallet_ids:
|
user_wallet_ids = await get_wallets_ids(account.id, deleted=False)
|
||||||
|
if data.wallet not in user_wallet_ids:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.BAD_REQUEST, "Wallet does not belong to this admin user."
|
HTTPStatus.BAD_REQUEST, "Wallet does not belong to this admin user."
|
||||||
)
|
)
|
||||||
@@ -161,7 +174,7 @@ async def api_update_pay_to_enable(
|
|||||||
|
|
||||||
@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, user: User = Depends(check_user_exists)
|
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -175,12 +188,12 @@ async def api_enable_extension(
|
|||||||
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.")
|
||||||
|
|
||||||
user_ext = await get_user_extension(user.id, ext_id)
|
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||||
if not user_ext:
|
if not user_ext:
|
||||||
user_ext = UserExtension(user=user.id, extension=ext_id, active=False)
|
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||||
await create_user_extension(user_ext)
|
await create_user_extension(user_ext)
|
||||||
|
|
||||||
if user.admin or not ext.requires_payment:
|
if account_id.is_admin_id or not ext.requires_payment:
|
||||||
user_ext.active = True
|
user_ext.active = True
|
||||||
await update_user_extension(user_ext)
|
await update_user_extension(user_ext)
|
||||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
||||||
@@ -217,13 +230,13 @@ async def api_enable_extension(
|
|||||||
|
|
||||||
@extension_router.put("/{ext_id}/disable")
|
@extension_router.put("/{ext_id}/disable")
|
||||||
async def api_disable_extension(
|
async def api_disable_extension(
|
||||||
ext_id: str, user: User = Depends(check_user_exists)
|
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.BAD_REQUEST, f"Extension '{ext_id}' doesn't exist."
|
HTTPStatus.BAD_REQUEST, f"Extension '{ext_id}' doesn't exist."
|
||||||
)
|
)
|
||||||
user_ext = await get_user_extension(user.id, ext_id)
|
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||||
if not user_ext or not user_ext.active:
|
if not user_ext or not user_ext.active:
|
||||||
return SimpleStatus(
|
return SimpleStatus(
|
||||||
success=True, message=f"Extension '{ext_id}' already disabled."
|
success=True, message=f"Extension '{ext_id}' already disabled."
|
||||||
@@ -374,7 +387,9 @@ async def get_pay_to_install_invoice(
|
|||||||
|
|
||||||
@extension_router.put("/{ext_id}/invoice/enable")
|
@extension_router.put("/{ext_id}/invoice/enable")
|
||||||
async def get_pay_to_enable_invoice(
|
async def get_pay_to_enable_invoice(
|
||||||
ext_id: str, data: PayToEnableInfo, user: User = Depends(check_user_exists)
|
ext_id: str,
|
||||||
|
data: PayToEnableInfo,
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
):
|
):
|
||||||
if not data.amount or data.amount <= 0:
|
if not data.amount or data.amount <= 0:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -420,9 +435,9 @@ async def get_pay_to_enable_invoice(
|
|||||||
memo=f"Enable '{ext.name}' extension.",
|
memo=f"Enable '{ext.name}' extension.",
|
||||||
)
|
)
|
||||||
|
|
||||||
user_ext = await get_user_extension(user.id, ext_id)
|
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||||
if not user_ext:
|
if not user_ext:
|
||||||
user_ext = UserExtension(user=user.id, extension=ext_id, active=False)
|
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||||
await create_user_extension(user_ext)
|
await create_user_extension(user_ext)
|
||||||
user_ext_info = user_ext.extra if user_ext.extra else UserExtensionInfo()
|
user_ext_info = user_ext.extra if user_ext.extra else UserExtensionInfo()
|
||||||
user_ext_info.payment_hash_to_enable = payment.payment_hash
|
user_ext_info.payment_hash_to_enable = payment.payment_hash
|
||||||
@@ -433,7 +448,7 @@ async def get_pay_to_enable_invoice(
|
|||||||
|
|
||||||
@extension_router.get(
|
@extension_router.get(
|
||||||
"/release/{org}/{repo}/{tag_name}",
|
"/release/{org}/{repo}/{tag_name}",
|
||||||
dependencies=[Depends(check_user_exists)],
|
dependencies=[Depends(check_account_exists)],
|
||||||
)
|
)
|
||||||
async def get_extension_release(org: str, repo: str, tag_name: str):
|
async def get_extension_release(org: str, repo: str, tag_name: str):
|
||||||
try:
|
try:
|
||||||
@@ -454,15 +469,18 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
|
|||||||
|
|
||||||
@extension_router.get("")
|
@extension_router.get("")
|
||||||
async def api_get_user_extensions(
|
async def api_get_user_extensions(
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
) -> list[Extension]:
|
) -> list[Extension]:
|
||||||
|
async with db.connect() as conn:
|
||||||
user_extensions_ids = [ue.extension for ue in await get_user_extensions(user.id)]
|
user_extensions_ids = [
|
||||||
return [
|
ue.extension for ue in await get_user_extensions(account_id.id, conn=conn)
|
||||||
ext
|
]
|
||||||
for ext in await get_valid_extensions(False)
|
valid_extensions = [
|
||||||
if ext.code in user_extensions_ids
|
ext
|
||||||
]
|
for ext in await get_valid_extensions(False, conn=conn)
|
||||||
|
if ext.code in user_extensions_ids
|
||||||
|
]
|
||||||
|
return valid_extensions
|
||||||
|
|
||||||
|
|
||||||
@extension_router.delete(
|
@extension_router.delete(
|
||||||
@@ -492,3 +510,135 @@ async def delete_extension_db(ext_id: str):
|
|||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail=f"Cannot delete data for extension '{ext_id}'",
|
detail=f"Cannot delete data for extension '{ext_id}'",
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: create a response model for this
|
||||||
|
@extension_router.get("/all")
|
||||||
|
async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||||
|
async with db.connect() as conn:
|
||||||
|
installed_exts: list[InstallableExtension] = await get_installed_extensions(
|
||||||
|
conn=conn
|
||||||
|
)
|
||||||
|
all_ext_ids = [ext.code for ext in await get_valid_extensions(conn=conn)]
|
||||||
|
inactive_extensions = [
|
||||||
|
e.id for e in await get_installed_extensions(active=False, conn=conn)
|
||||||
|
]
|
||||||
|
db_versions = await get_db_versions(conn=conn)
|
||||||
|
|
||||||
|
installed_exts_ids = [e.id for e in installed_exts]
|
||||||
|
|
||||||
|
installable_exts = await InstallableExtension.get_installable_extensions(
|
||||||
|
post_refresh_cache=account_id.is_admin_id
|
||||||
|
)
|
||||||
|
installable_exts_ids = [e.id for e in installable_exts]
|
||||||
|
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
|
||||||
|
|
||||||
|
for e in installable_exts:
|
||||||
|
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
|
||||||
|
if installed_ext and installed_ext.meta:
|
||||||
|
installed_release = installed_ext.meta.installed_release
|
||||||
|
if installed_ext.meta.pay_to_enable and not account_id.is_admin_id:
|
||||||
|
# not a security leak, but better not to share the wallet id
|
||||||
|
installed_ext.meta.pay_to_enable.wallet = None
|
||||||
|
pay_to_enable = installed_ext.meta.pay_to_enable
|
||||||
|
|
||||||
|
if e.meta:
|
||||||
|
e.meta.installed_release = installed_release
|
||||||
|
e.meta.pay_to_enable = pay_to_enable
|
||||||
|
else:
|
||||||
|
e.meta = ExtensionMeta(
|
||||||
|
installed_release=installed_release,
|
||||||
|
pay_to_enable=pay_to_enable,
|
||||||
|
)
|
||||||
|
# use the installed extension values
|
||||||
|
e.name = installed_ext.name
|
||||||
|
e.short_description = installed_ext.short_description
|
||||||
|
e.icon = installed_ext.icon
|
||||||
|
|
||||||
|
extension_data = [
|
||||||
|
{
|
||||||
|
"id": ext.id,
|
||||||
|
"name": ext.name,
|
||||||
|
"icon": ext.icon,
|
||||||
|
"shortDescription": ext.short_description,
|
||||||
|
"stars": ext.stars,
|
||||||
|
"isFeatured": ext.meta.featured if ext.meta else False,
|
||||||
|
"dependencies": ext.meta.dependencies if ext.meta else "",
|
||||||
|
"isInstalled": ext.id in installed_exts_ids,
|
||||||
|
"hasDatabaseTables": next(
|
||||||
|
(True for version in db_versions if version.db == ext.id), False
|
||||||
|
),
|
||||||
|
"isAvailable": ext.id in all_ext_ids,
|
||||||
|
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||||
|
"isActive": ext.id not in inactive_extensions,
|
||||||
|
"latestRelease": (
|
||||||
|
dict(ext.meta.latest_release)
|
||||||
|
if ext.meta and ext.meta.latest_release
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"hasPaidRelease": ext.meta.has_paid_release if ext.meta else False,
|
||||||
|
"hasFreeRelease": ext.meta.has_free_release if ext.meta else False,
|
||||||
|
"paidFeatures": ext.meta.paid_features if ext.meta else False,
|
||||||
|
"installedRelease": (
|
||||||
|
dict(ext.meta.installed_release)
|
||||||
|
if ext.meta and ext.meta.installed_release
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"payToEnable": (
|
||||||
|
dict(ext.meta.pay_to_enable)
|
||||||
|
if ext.meta and ext.meta.pay_to_enable
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
"isPaymentRequired": ext.requires_payment,
|
||||||
|
"inProgress": False,
|
||||||
|
"selectedForUpdate": False,
|
||||||
|
}
|
||||||
|
for ext in installable_exts
|
||||||
|
]
|
||||||
|
return extension_data
|
||||||
|
|
||||||
|
|
||||||
|
@extension_router.get(
|
||||||
|
"/reviews/tags",
|
||||||
|
dependencies=[Depends(check_account_exists)],
|
||||||
|
)
|
||||||
|
async def get_extension_reviews_tags() -> list[ExtensionReviewsStatus]:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.get(settings.lnbits_extensions_reviews_url + "/tags")
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
return [ExtensionReviewsStatus(**item) for item in data]
|
||||||
|
|
||||||
|
|
||||||
|
@extension_router.get(
|
||||||
|
"/reviews/{ext_id}",
|
||||||
|
dependencies=[Depends(check_account_exists)],
|
||||||
|
)
|
||||||
|
async def get_extension_reviews(ext_id: str, request: Request) -> Page[ExtensionReview]:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
query_string = str(request.query_params)
|
||||||
|
resp = await client.get(
|
||||||
|
settings.lnbits_extensions_reviews_url + f"/reviews/{ext_id}?{query_string}"
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
reviews = resp.json()
|
||||||
|
return Page(
|
||||||
|
data=[ExtensionReview(**item) for item in reviews["data"]],
|
||||||
|
total=reviews["total"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@extension_router.put(
|
||||||
|
"/reviews",
|
||||||
|
dependencies=[Depends(check_account_exists)],
|
||||||
|
)
|
||||||
|
async def create_extension_review(
|
||||||
|
data: CreateExtensionReview,
|
||||||
|
) -> ExtensionReviewPaymentRequest:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.post(
|
||||||
|
settings.lnbits_extensions_reviews_url + "/reviews", json=data.dict()
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
payment_request = resp.json()
|
||||||
|
return ExtensionReviewPaymentRequest(**payment_request)
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
SimpleStatus,
|
SimpleStatus,
|
||||||
User,
|
|
||||||
)
|
)
|
||||||
from lnbits.core.models.extensions import (
|
from lnbits.core.models.extensions import (
|
||||||
Extension,
|
Extension,
|
||||||
@@ -17,6 +15,7 @@ from lnbits.core.models.extensions import (
|
|||||||
UserExtension,
|
UserExtension,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.extensions_builder import ExtensionData
|
from lnbits.core.models.extensions_builder import ExtensionData
|
||||||
|
from lnbits.core.models.users import Account, AccountId
|
||||||
from lnbits.core.services.extensions import (
|
from lnbits.core.services.extensions import (
|
||||||
activate_extension,
|
activate_extension,
|
||||||
install_extension,
|
install_extension,
|
||||||
@@ -27,10 +26,10 @@ from lnbits.core.services.extensions_builder import (
|
|||||||
zip_directory,
|
zip_directory,
|
||||||
)
|
)
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
|
check_account_id_exists,
|
||||||
check_admin,
|
check_admin,
|
||||||
check_user_exists,
|
check_extension_builder,
|
||||||
)
|
)
|
||||||
from lnbits.settings import settings
|
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_user_extension,
|
create_user_extension,
|
||||||
@@ -47,19 +46,12 @@ extension_builder_router = APIRouter(
|
|||||||
@extension_builder_router.post(
|
@extension_builder_router.post(
|
||||||
"/zip",
|
"/zip",
|
||||||
summary="Build and download extension zip.",
|
summary="Build and download extension zip.",
|
||||||
|
dependencies=[Depends(check_extension_builder)],
|
||||||
description="""
|
description="""
|
||||||
This endpoint generates a zip file for the extension based on the provided data.
|
This endpoint generates a zip file for the extension based on the provided data.
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
async def api_build_extension(
|
async def api_build_extension(data: ExtensionData) -> FileResponse:
|
||||||
data: ExtensionData,
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
) -> FileResponse:
|
|
||||||
if not settings.lnbits_extensions_builder_activate_non_admins and not user.admin:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.FORBIDDEN,
|
|
||||||
"Extension Builder is disabled for non admin users.",
|
|
||||||
)
|
|
||||||
stub_ext_id = "extension_builder_stub" # todo: do not hardcode, fetch from manifest
|
stub_ext_id = "extension_builder_stub" # todo: do not hardcode, fetch from manifest
|
||||||
release, build_dir = await build_extension_from_data(data, stub_ext_id)
|
release, build_dir = await build_extension_from_data(data, stub_ext_id)
|
||||||
|
|
||||||
@@ -92,9 +84,9 @@ async def api_build_extension(
|
|||||||
)
|
)
|
||||||
async def api_deploy_extension(
|
async def api_deploy_extension(
|
||||||
data: ExtensionData,
|
data: ExtensionData,
|
||||||
user: User = Depends(check_admin),
|
account: Account = Depends(check_admin),
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
working_dir_name = "deploy_" + sha256(user.id.encode("utf-8")).hexdigest()
|
working_dir_name = "deploy_" + sha256(account.id.encode("utf-8")).hexdigest()
|
||||||
stub_ext_id = "extension_builder_stub"
|
stub_ext_id = "extension_builder_stub"
|
||||||
release, build_dir = await build_extension_from_data(
|
release, build_dir = await build_extension_from_data(
|
||||||
data, stub_ext_id, working_dir_name
|
data, stub_ext_id, working_dir_name
|
||||||
@@ -118,9 +110,9 @@ async def api_deploy_extension(
|
|||||||
|
|
||||||
await activate_extension(Extension.from_installable_ext(ext_info))
|
await activate_extension(Extension.from_installable_ext(ext_info))
|
||||||
|
|
||||||
user_ext = await get_user_extension(user.id, data.id)
|
user_ext = await get_user_extension(account.id, data.id)
|
||||||
if not user_ext:
|
if not user_ext:
|
||||||
user_ext = UserExtension(user=user.id, extension=data.id, active=True)
|
user_ext = UserExtension(user=account.id, extension=data.id, active=True)
|
||||||
await create_user_extension(user_ext)
|
await create_user_extension(user_ext)
|
||||||
elif not user_ext.active:
|
elif not user_ext.active:
|
||||||
user_ext.active = True
|
user_ext.active = True
|
||||||
@@ -132,18 +124,14 @@ async def api_deploy_extension(
|
|||||||
@extension_builder_router.post(
|
@extension_builder_router.post(
|
||||||
"/preview",
|
"/preview",
|
||||||
summary="Build and preview the extension ui.",
|
summary="Build and preview the extension ui.",
|
||||||
|
dependencies=[Depends(check_extension_builder)],
|
||||||
)
|
)
|
||||||
async def api_preview_extension(
|
async def api_preview_extension(
|
||||||
data: ExtensionData,
|
data: ExtensionData,
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
if not settings.lnbits_extensions_builder_activate_non_admins and not user.admin:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.FORBIDDEN,
|
|
||||||
"Extension Builder is disabled for non admin users.",
|
|
||||||
)
|
|
||||||
stub_ext_id = "extension_builder_stub"
|
stub_ext_id = "extension_builder_stub"
|
||||||
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
working_dir_name = "preview_" + sha256(account_id.id.encode("utf-8")).hexdigest()
|
||||||
await build_extension_from_data(data, stub_ext_id, working_dir_name)
|
await build_extension_from_data(data, stub_ext_id, working_dir_name)
|
||||||
|
|
||||||
return SimpleStatus(success=True, message=f"Extension '{data.id}' preview ready.")
|
return SimpleStatus(success=True, message=f"Extension '{data.id}' preview ready.")
|
||||||
|
|||||||
@@ -75,20 +75,28 @@ async def cancel_subscription(
|
|||||||
)
|
)
|
||||||
async def connection_token(provider: str):
|
async def connection_token(provider: str):
|
||||||
fiat_provider = await get_fiat_provider(provider)
|
fiat_provider = await get_fiat_provider(provider)
|
||||||
if provider == "stripe":
|
if not fiat_provider:
|
||||||
if not isinstance(fiat_provider, StripeWallet):
|
raise HTTPException(status_code=404, detail="Fiat provider not found")
|
||||||
|
|
||||||
|
if provider != "stripe":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Connection tokens are not supported for provider '{provider}'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(fiat_provider, StripeWallet):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail="Stripe wallet/provider not configured"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
tok = await fiat_provider.create_terminal_connection_token()
|
||||||
|
secret = tok.get("secret")
|
||||||
|
if not secret:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500, detail="Stripe wallet/provider not configured"
|
status_code=502, detail="Stripe returned no connection token"
|
||||||
)
|
)
|
||||||
try:
|
return {"secret": secret}
|
||||||
tok = await fiat_provider.create_terminal_connection_token()
|
except Exception as e:
|
||||||
secret = tok.get("secret")
|
raise HTTPException(
|
||||||
if not secret:
|
status_code=500, detail="Failed to create connection token"
|
||||||
raise HTTPException(
|
) from e
|
||||||
status_code=502, detail="Stripe returned no connection token"
|
|
||||||
)
|
|
||||||
return {"secret": secret}
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500, detail="Failed to create connection token"
|
|
||||||
) from e
|
|
||||||
|
|||||||
+39
-342
@@ -1,35 +1,29 @@
|
|||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated
|
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import Cookie, Depends, Query, Request
|
from fastapi import Depends, Request
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from lnurl import url_decode
|
from lnurl import url_decode
|
||||||
from pydantic.types import UUID4
|
|
||||||
|
|
||||||
from lnbits.core.helpers import to_valid_user_id
|
from lnbits.core.helpers import to_valid_user_id
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.core.models.extensions import ExtensionMeta, InstallableExtension
|
|
||||||
from lnbits.core.services import create_invoice, create_user_account
|
from lnbits.core.services import create_invoice, create_user_account
|
||||||
from lnbits.core.services.extensions import get_valid_extensions
|
from lnbits.decorators import (
|
||||||
from lnbits.decorators import check_admin, check_user_exists
|
check_admin,
|
||||||
|
check_admin_ui,
|
||||||
|
check_extension_builder,
|
||||||
|
check_first_install,
|
||||||
|
check_user_exists,
|
||||||
|
)
|
||||||
from lnbits.helpers import check_callback_url, template_renderer
|
from lnbits.helpers import check_callback_url, template_renderer
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets import get_funding_source
|
|
||||||
|
|
||||||
from ...utils.exchange_rates import allowed_currencies, currencies
|
from ..crud import get_user
|
||||||
from ..crud import (
|
|
||||||
create_wallet,
|
|
||||||
get_db_versions,
|
|
||||||
get_installed_extensions,
|
|
||||||
get_user,
|
|
||||||
get_wallet,
|
|
||||||
)
|
|
||||||
|
|
||||||
generic_router = APIRouter(
|
generic_router = APIRouter(
|
||||||
tags=["Core NON-API Website Routes"], include_in_schema=False
|
tags=["Core NON-API Website Routes"], include_in_schema=False
|
||||||
@@ -41,251 +35,23 @@ async def favicon():
|
|||||||
return RedirectResponse(settings.lnbits_qr_logo)
|
return RedirectResponse(settings.lnbits_qr_logo)
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/", response_class=HTMLResponse)
|
|
||||||
async def home(request: Request, lightning: str = ""):
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
request, "core/index.html", {"lnurl": lightning}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get(
|
|
||||||
"/account",
|
|
||||||
response_class=HTMLResponse,
|
|
||||||
description="show account page",
|
|
||||||
)
|
|
||||||
async def account(
|
|
||||||
request: Request,
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
):
|
|
||||||
nostr_configured = settings.is_nostr_notifications_configured()
|
|
||||||
telegram_configured = settings.is_telegram_notifications_configured()
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
"core/account.html",
|
|
||||||
{
|
|
||||||
"user": user.json(),
|
|
||||||
"nostr_configured": nostr_configured,
|
|
||||||
"telegram_configured": telegram_configured,
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get(
|
|
||||||
"/wallet",
|
|
||||||
response_class=HTMLResponse,
|
|
||||||
description="show wallet page",
|
|
||||||
)
|
|
||||||
async def get_user_wallet(
|
|
||||||
request: Request,
|
|
||||||
lnbits_last_active_wallet: Annotated[str | None, Cookie()] = None,
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
wal: UUID4 | None = Query(None),
|
|
||||||
):
|
|
||||||
if wal:
|
|
||||||
wallet = await get_wallet(wal.hex)
|
|
||||||
elif len(user.wallets) == 0:
|
|
||||||
wallet = await create_wallet(user_id=user.id)
|
|
||||||
user.wallets.append(wallet)
|
|
||||||
elif lnbits_last_active_wallet and user.get_wallet(lnbits_last_active_wallet):
|
|
||||||
wallet = await get_wallet(lnbits_last_active_wallet)
|
|
||||||
else:
|
|
||||||
wallet = user.wallets[0]
|
|
||||||
|
|
||||||
if not wallet or wallet.deleted:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
detail="Wallet not found",
|
|
||||||
)
|
|
||||||
if wallet.user != user.id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Not your wallet.",
|
|
||||||
)
|
|
||||||
context = {
|
|
||||||
"user": user.json(),
|
|
||||||
"wallet": wallet.json(),
|
|
||||||
"wallet_name": wallet.name,
|
|
||||||
"currencies": allowed_currencies(),
|
|
||||||
"service_fee": settings.lnbits_service_fee,
|
|
||||||
"service_fee_max": settings.lnbits_service_fee_max,
|
|
||||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
|
||||||
}
|
|
||||||
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
"core/wallet.html",
|
|
||||||
{**context, "ajax": _is_ajax_request(request)},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get(
|
|
||||||
"/wallets",
|
|
||||||
response_class=HTMLResponse,
|
|
||||||
description="show wallets page",
|
|
||||||
)
|
|
||||||
async def wallets(
|
|
||||||
request: Request,
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
):
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
"core/wallets.html",
|
|
||||||
{
|
|
||||||
"user": user.json(),
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/first_install", response_class=HTMLResponse)
|
|
||||||
async def first_install(request: Request):
|
|
||||||
if not settings.first_install:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="Super user account has already been configured.",
|
|
||||||
)
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
"core/first_install.html",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/robots.txt", response_class=HTMLResponse)
|
@generic_router.get("/robots.txt", response_class=HTMLResponse)
|
||||||
async def robots():
|
async def robots():
|
||||||
data = """
|
data = "User-agent: *\nDisallow: /"
|
||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
||||||
"""
|
|
||||||
return HTMLResponse(content=data, media_type="text/plain")
|
return HTMLResponse(content=data, media_type="text/plain")
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
|
|
||||||
async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
|
||||||
installed_exts: list[InstallableExtension] = await get_installed_extensions()
|
|
||||||
installed_exts_ids = [e.id for e in installed_exts]
|
|
||||||
|
|
||||||
installable_exts = await InstallableExtension.get_installable_extensions()
|
|
||||||
installable_exts_ids = [e.id for e in installable_exts]
|
|
||||||
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
|
|
||||||
|
|
||||||
for e in installable_exts:
|
|
||||||
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
|
|
||||||
if installed_ext and installed_ext.meta:
|
|
||||||
installed_release = installed_ext.meta.installed_release
|
|
||||||
if installed_ext.meta.pay_to_enable and not user.admin:
|
|
||||||
# not a security leak, but better not to share the wallet id
|
|
||||||
installed_ext.meta.pay_to_enable.wallet = None
|
|
||||||
pay_to_enable = installed_ext.meta.pay_to_enable
|
|
||||||
|
|
||||||
if e.meta:
|
|
||||||
e.meta.installed_release = installed_release
|
|
||||||
e.meta.pay_to_enable = pay_to_enable
|
|
||||||
else:
|
|
||||||
e.meta = ExtensionMeta(
|
|
||||||
installed_release=installed_release,
|
|
||||||
pay_to_enable=pay_to_enable,
|
|
||||||
)
|
|
||||||
# use the installed extension values
|
|
||||||
e.name = installed_ext.name
|
|
||||||
e.short_description = installed_ext.short_description
|
|
||||||
e.icon = installed_ext.icon
|
|
||||||
|
|
||||||
all_ext_ids = [ext.code for ext in await get_valid_extensions()]
|
|
||||||
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
|
|
||||||
db_versions = await get_db_versions()
|
|
||||||
|
|
||||||
extension_data = [
|
|
||||||
{
|
|
||||||
"id": ext.id,
|
|
||||||
"name": ext.name,
|
|
||||||
"icon": ext.icon,
|
|
||||||
"shortDescription": ext.short_description,
|
|
||||||
"stars": ext.stars,
|
|
||||||
"isFeatured": ext.meta.featured if ext.meta else False,
|
|
||||||
"dependencies": ext.meta.dependencies if ext.meta else "",
|
|
||||||
"isInstalled": ext.id in installed_exts_ids,
|
|
||||||
"hasDatabaseTables": next(
|
|
||||||
(True for version in db_versions if version.db == ext.id), False
|
|
||||||
),
|
|
||||||
"isAvailable": ext.id in all_ext_ids,
|
|
||||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
|
||||||
"isActive": ext.id not in inactive_extensions,
|
|
||||||
"latestRelease": (
|
|
||||||
dict(ext.meta.latest_release)
|
|
||||||
if ext.meta and ext.meta.latest_release
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
"hasPaidRelease": ext.meta.has_paid_release if ext.meta else False,
|
|
||||||
"hasFreeRelease": ext.meta.has_free_release if ext.meta else False,
|
|
||||||
"paidFeatures": ext.meta.paid_features if ext.meta else False,
|
|
||||||
"installedRelease": (
|
|
||||||
dict(ext.meta.installed_release)
|
|
||||||
if ext.meta and ext.meta.installed_release
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
"payToEnable": (
|
|
||||||
dict(ext.meta.pay_to_enable)
|
|
||||||
if ext.meta and ext.meta.pay_to_enable
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
"isPaymentRequired": ext.requires_payment,
|
|
||||||
}
|
|
||||||
for ext in installable_exts
|
|
||||||
]
|
|
||||||
|
|
||||||
# refresh user state. Eg: enabled extensions.
|
|
||||||
# TODO: refactor
|
|
||||||
# user = await get_user(user.id) or user
|
|
||||||
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
"core/extensions.html",
|
|
||||||
{
|
|
||||||
"user": user.json(),
|
|
||||||
"extension_data": extension_data,
|
|
||||||
"extension_builder_enabled": user.admin
|
|
||||||
or settings.lnbits_extensions_builder_activate_non_admins,
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get(
|
|
||||||
"/extensions/builder", name="extensions builder", response_class=HTMLResponse
|
|
||||||
)
|
|
||||||
async def extensions_builder(request: Request, user: User = Depends(check_user_exists)):
|
|
||||||
if not settings.lnbits_extensions_builder_activate_non_admins and not user.admin:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.FORBIDDEN,
|
|
||||||
"Extension Builder is disabled for non admin users.",
|
|
||||||
)
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
"core/extensions_builder.html",
|
|
||||||
{
|
|
||||||
"user": user.json(),
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get(
|
@generic_router.get(
|
||||||
"/extensions/builder/preview/{ext_id}",
|
"/extensions/builder/preview/{ext_id}",
|
||||||
name="extensions builder",
|
name="extensions builder",
|
||||||
response_class=HTMLResponse,
|
dependencies=[Depends(check_extension_builder)],
|
||||||
)
|
)
|
||||||
async def extensions_builder_preview(
|
async def extensions_builder_preview(
|
||||||
request: Request,
|
request: Request,
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
page_name: str | None = None,
|
page_name: str | None = None,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
):
|
) -> HTMLResponse:
|
||||||
if not settings.lnbits_extensions_builder_activate_non_admins and not user.admin:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.FORBIDDEN,
|
|
||||||
"Extension Builder is disabled for non admin users.",
|
|
||||||
)
|
|
||||||
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
||||||
html_file_name = "index.html"
|
html_file_name = "index.html"
|
||||||
if page_name == "public_page":
|
if page_name == "public_page":
|
||||||
@@ -310,8 +76,8 @@ async def extensions_builder_preview(
|
|||||||
request,
|
request,
|
||||||
"error.html",
|
"error.html",
|
||||||
{
|
{
|
||||||
"err": f"Extension {ext_id} not found",
|
"status_code": 404,
|
||||||
"message": "Please 'Refresh Preview' first.",
|
"message": f"Extension {ext_id} not found, refresh Preview.",
|
||||||
},
|
},
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
)
|
)
|
||||||
@@ -321,7 +87,6 @@ async def extensions_builder_preview(
|
|||||||
html_file_path.as_posix(),
|
html_file_path.as_posix(),
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -330,6 +95,7 @@ async def extensions_builder_preview(
|
|||||||
"style-src 'self' 'unsafe-inline'; "
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -359,7 +125,7 @@ async def manifest(request: Request, usr: str):
|
|||||||
"src": (
|
"src": (
|
||||||
settings.lnbits_custom_logo
|
settings.lnbits_custom_logo
|
||||||
if settings.lnbits_custom_logo
|
if settings.lnbits_custom_logo
|
||||||
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@main/docs/logos/lnbits.png"
|
else "images/logos/lnbits.png"
|
||||||
),
|
),
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
@@ -426,104 +192,39 @@ async def manifest(request: Request, usr: str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/node", response_class=HTMLResponse)
|
admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
||||||
async def node(request: Request, user: User = Depends(check_admin)):
|
|
||||||
if not settings.lnbits_node_ui:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
|
|
||||||
|
|
||||||
funding_source = get_funding_source()
|
|
||||||
_, balance = await funding_source.status()
|
|
||||||
|
|
||||||
|
@generic_router.get("/payments")
|
||||||
|
@generic_router.get("/wallet")
|
||||||
|
@generic_router.get("/wallet/{wallet_id}")
|
||||||
|
@generic_router.get("/wallets")
|
||||||
|
@generic_router.get("/account")
|
||||||
|
@generic_router.get("/extensions")
|
||||||
|
@generic_router.get("/users", dependencies=admin_ui_checks)
|
||||||
|
@generic_router.get("/audit", dependencies=admin_ui_checks)
|
||||||
|
@generic_router.get("/node", dependencies=admin_ui_checks)
|
||||||
|
@generic_router.get("/admin", dependencies=admin_ui_checks)
|
||||||
|
@generic_router.get(
|
||||||
|
"/extensions/builder", dependencies=[Depends(check_extension_builder)]
|
||||||
|
)
|
||||||
|
async def index(
|
||||||
|
request: Request, user: User = Depends(check_user_exists)
|
||||||
|
) -> HTMLResponse:
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"node/index.html",
|
"index.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"balance": balance,
|
|
||||||
"wallets": user.wallets[0].json(),
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/node/public", response_class=HTMLResponse)
|
@generic_router.get("/")
|
||||||
async def node_public(request: Request):
|
@generic_router.get("/node/public")
|
||||||
if not settings.lnbits_public_node_ui:
|
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
||||||
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
|
async def index_public(request: Request) -> HTMLResponse:
|
||||||
|
return template_renderer().TemplateResponse(request, "index.html", {"public": True})
|
||||||
funding_source = get_funding_source()
|
|
||||||
_, balance = await funding_source.status()
|
|
||||||
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
"node/public.html",
|
|
||||||
{
|
|
||||||
"balance": balance,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/admin", response_class=HTMLResponse)
|
|
||||||
async def admin_index(request: Request, user: User = Depends(check_admin)):
|
|
||||||
if not settings.lnbits_admin_ui:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
|
||||||
|
|
||||||
funding_source = get_funding_source()
|
|
||||||
_, balance = await funding_source.status()
|
|
||||||
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
"admin/index.html",
|
|
||||||
{
|
|
||||||
"user": user.json(),
|
|
||||||
"balance": balance,
|
|
||||||
"currencies": list(currencies.keys()),
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/users", response_class=HTMLResponse)
|
|
||||||
async def users_index(request: Request, user: User = Depends(check_admin)):
|
|
||||||
if not settings.lnbits_admin_ui:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
|
||||||
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
"users/index.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user.json(),
|
|
||||||
"currencies": list(currencies.keys()),
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/audit", response_class=HTMLResponse)
|
|
||||||
async def audit_index(request: Request, user: User = Depends(check_admin)):
|
|
||||||
if not settings.lnbits_audit_enabled:
|
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Audit not enabled")
|
|
||||||
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
"audit/index.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user.json(),
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/payments", response_class=HTMLResponse)
|
|
||||||
async def payments_index(request: Request, user: User = Depends(check_user_exists)):
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
"payments/index.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user.json(),
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/uuidv4/{hex_value}")
|
@generic_router.get("/uuidv4/{hex_value}")
|
||||||
@@ -584,7 +285,3 @@ async def lnurlwallet(request: Request, lightning: str = ""):
|
|||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/wallet?usr={account.id}&wal={wallet.id}",
|
f"/wallet?usr={account.id}&wal={wallet.id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _is_ajax_request(request: Request):
|
|
||||||
return request.headers.get("X-Requested-With", None) == "XMLHttpRequest"
|
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ from fastapi import (
|
|||||||
Depends,
|
Depends,
|
||||||
HTTPException,
|
HTTPException,
|
||||||
)
|
)
|
||||||
from lnurl import LnurlResponseException
|
from lnurl import (
|
||||||
from lnurl import execute_login as lnurlauth
|
|
||||||
from lnurl import handle as lnurl_handle
|
|
||||||
from lnurl.models import (
|
|
||||||
LnurlAuthResponse,
|
LnurlAuthResponse,
|
||||||
LnurlErrorResponse,
|
LnurlErrorResponse,
|
||||||
|
LnurlException,
|
||||||
LnurlPayResponse,
|
LnurlPayResponse,
|
||||||
LnurlResponseModel,
|
LnurlResponseException,
|
||||||
LnurlWithdrawResponse,
|
LnurlWithdrawResponse,
|
||||||
)
|
)
|
||||||
|
from lnurl import execute_login as lnurlauth
|
||||||
|
from lnurl import handle as lnurl_handle
|
||||||
|
from lnurl.models import LnurlResponseModel
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
@@ -23,7 +24,7 @@ from lnbits.core.models.lnurl import CreateLnurlPayment, LnurlScan
|
|||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_base_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import check_callback_url
|
from lnbits.helpers import check_callback_url
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -38,7 +39,7 @@ async def _handle(lnurl: str) -> LnurlResponseModel:
|
|||||||
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
|
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
|
||||||
if isinstance(res, LnurlErrorResponse):
|
if isinstance(res, LnurlErrorResponse):
|
||||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason)
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason)
|
||||||
except LnurlResponseException as exc:
|
except LnurlException as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||||
) from exc
|
) from exc
|
||||||
@@ -47,7 +48,7 @@ async def _handle(lnurl: str) -> LnurlResponseModel:
|
|||||||
|
|
||||||
@lnurl_router.get(
|
@lnurl_router.get(
|
||||||
"/api/v1/lnurlscan/{code}",
|
"/api/v1/lnurlscan/{code}",
|
||||||
dependencies=[Depends(require_invoice_key)],
|
dependencies=[Depends(require_base_invoice_key)],
|
||||||
deprecated=True,
|
deprecated=True,
|
||||||
response_model=LnurlPayResponse
|
response_model=LnurlPayResponse
|
||||||
| LnurlWithdrawResponse
|
| LnurlWithdrawResponse
|
||||||
@@ -63,7 +64,7 @@ async def api_lnurlscan(code: str) -> LnurlResponseModel:
|
|||||||
|
|
||||||
@lnurl_router.post(
|
@lnurl_router.post(
|
||||||
"/api/v1/lnurlscan",
|
"/api/v1/lnurlscan",
|
||||||
dependencies=[Depends(require_invoice_key)],
|
dependencies=[Depends(require_base_invoice_key)],
|
||||||
response_model=LnurlPayResponse
|
response_model=LnurlPayResponse
|
||||||
| LnurlWithdrawResponse
|
| LnurlWithdrawResponse
|
||||||
| LnurlAuthResponse
|
| LnurlAuthResponse
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from http import HTTPStatus
|
|||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from starlette.status import HTTP_503_SERVICE_UNAVAILABLE
|
|
||||||
|
|
||||||
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -154,7 +153,7 @@ async def api_get_payments(
|
|||||||
) -> Page[NodePayment] | None:
|
) -> Page[NodePayment] | None:
|
||||||
if not settings.lnbits_node_ui_transactions:
|
if not settings.lnbits_node_ui_transactions:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTP_503_SERVICE_UNAVAILABLE,
|
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
detail="You can enable node transactions in the Admin UI",
|
detail="You can enable node transactions in the Admin UI",
|
||||||
)
|
)
|
||||||
return await node.get_payments(filters)
|
return await node.get_payments(filters)
|
||||||
@@ -167,7 +166,7 @@ async def api_get_invoices(
|
|||||||
) -> Page[NodeInvoice] | None:
|
) -> Page[NodeInvoice] | None:
|
||||||
if not settings.lnbits_node_ui_transactions:
|
if not settings.lnbits_node_ui_transactions:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTP_503_SERVICE_UNAVAILABLE,
|
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
detail="You can enable node transactions in the Admin UI",
|
detail="You can enable node transactions in the Admin UI",
|
||||||
)
|
)
|
||||||
return await node.get_invoices(filters)
|
return await node.get_invoices(filters)
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ from lnbits import bolt11
|
|||||||
from lnbits.core.crud.payments import (
|
from lnbits.core.crud.payments import (
|
||||||
get_payment_count_stats,
|
get_payment_count_stats,
|
||||||
get_wallets_stats,
|
get_wallets_stats,
|
||||||
|
update_payment,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.crud.users import get_account
|
||||||
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
CancelInvoice,
|
CancelInvoice,
|
||||||
CreateInvoice,
|
CreateInvoice,
|
||||||
@@ -32,14 +35,17 @@ from lnbits.core.models import (
|
|||||||
SettleInvoice,
|
SettleInvoice,
|
||||||
SimpleStatus,
|
SimpleStatus,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.users import User
|
from lnbits.core.models.payments import UpdatePaymentLabels
|
||||||
|
from lnbits.core.models.users import AccountId
|
||||||
|
from lnbits.core.models.wallets import BaseWalletTypeInfo
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
check_user_exists,
|
check_account_id_exists,
|
||||||
parse_filters,
|
parse_filters,
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_base_admin_key,
|
||||||
|
require_base_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
filter_dict_keys,
|
filter_dict_keys,
|
||||||
@@ -64,7 +70,6 @@ from ..services import (
|
|||||||
perform_withdraw,
|
perform_withdraw,
|
||||||
settle_hold_invoice,
|
settle_hold_invoice,
|
||||||
update_pending_payment,
|
update_pending_payment,
|
||||||
update_pending_payments,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
|
payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
|
||||||
@@ -79,10 +84,9 @@ payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
|
|||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments(
|
async def api_payments(
|
||||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key),
|
||||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||||
):
|
):
|
||||||
await update_pending_payments(key_info.wallet.id)
|
|
||||||
return await get_payments(
|
return await get_payments(
|
||||||
wallet_id=key_info.wallet.id,
|
wallet_id=key_info.wallet.id,
|
||||||
pending=True,
|
pending=True,
|
||||||
@@ -98,11 +102,10 @@ async def api_payments(
|
|||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments_history(
|
async def api_payments_history(
|
||||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key),
|
||||||
group: DateTrunc = Query("day"),
|
group: DateTrunc = Query("day"),
|
||||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||||
):
|
):
|
||||||
await update_pending_payments(key_info.wallet.id)
|
|
||||||
return await get_payments_history(key_info.wallet.id, group, filters)
|
return await get_payments_history(key_info.wallet.id, group, filters)
|
||||||
|
|
||||||
|
|
||||||
@@ -115,14 +118,14 @@ async def api_payments_history(
|
|||||||
async def api_payments_counting_stats(
|
async def api_payments_counting_stats(
|
||||||
count_by: PaymentCountField = Query("tag"),
|
count_by: PaymentCountField = Query("tag"),
|
||||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
):
|
):
|
||||||
if user.admin:
|
if account_id.is_admin_id:
|
||||||
# admin user can see payments from all wallets
|
# admin user can see payments from all wallets
|
||||||
for_user_id = None
|
for_user_id = None
|
||||||
else:
|
else:
|
||||||
# regular user can only see payments from their wallets
|
# regular user can only see payments from their wallets
|
||||||
for_user_id = user.id
|
for_user_id = account_id.id
|
||||||
|
|
||||||
return await get_payment_count_stats(count_by, filters=filters, user_id=for_user_id)
|
return await get_payment_count_stats(count_by, filters=filters, user_id=for_user_id)
|
||||||
|
|
||||||
@@ -135,14 +138,14 @@ async def api_payments_counting_stats(
|
|||||||
)
|
)
|
||||||
async def api_payments_wallets_stats(
|
async def api_payments_wallets_stats(
|
||||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
):
|
):
|
||||||
if user.admin:
|
if account_id.is_admin_id:
|
||||||
# admin user can see payments from all wallets
|
# admin user can see payments from all wallets
|
||||||
for_user_id = None
|
for_user_id = None
|
||||||
else:
|
else:
|
||||||
# regular user can only see payments from their wallets
|
# regular user can only see payments from their wallets
|
||||||
for_user_id = user.id
|
for_user_id = account_id.id
|
||||||
|
|
||||||
return await get_wallets_stats(filters, user_id=for_user_id)
|
return await get_wallets_stats(filters, user_id=for_user_id)
|
||||||
|
|
||||||
@@ -154,15 +157,15 @@ async def api_payments_wallets_stats(
|
|||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments_daily_stats(
|
async def api_payments_daily_stats(
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||||
):
|
):
|
||||||
if user.admin:
|
if account_id.is_admin_id:
|
||||||
# admin user can see payments from all wallets
|
# admin user can see payments from all wallets
|
||||||
for_user_id = None
|
for_user_id = None
|
||||||
else:
|
else:
|
||||||
# regular user can only see payments from their wallets
|
# regular user can only see payments from their wallets
|
||||||
for_user_id = user.id
|
for_user_id = account_id.id
|
||||||
return await get_payments_daily_stats(filters, user_id=for_user_id)
|
return await get_payments_daily_stats(filters, user_id=for_user_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -175,18 +178,30 @@ async def api_payments_daily_stats(
|
|||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments_paginated(
|
async def api_payments_paginated(
|
||||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key),
|
||||||
|
recheck_pending: bool = Query(
|
||||||
|
False, description="Force check and update of pending payments."
|
||||||
|
),
|
||||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||||
):
|
) -> Page[Payment]:
|
||||||
page = await get_payments_paginated(
|
async with db.connect() as conn:
|
||||||
wallet_id=key_info.wallet.id,
|
page = await get_payments_paginated(
|
||||||
filters=filters,
|
wallet_id=key_info.wallet.id,
|
||||||
)
|
filters=filters,
|
||||||
for payment in page.data:
|
conn=conn,
|
||||||
if payment.pending:
|
)
|
||||||
await update_pending_payment(payment)
|
if not recheck_pending:
|
||||||
|
return page
|
||||||
|
|
||||||
return page
|
payments = []
|
||||||
|
for payment in page.data:
|
||||||
|
if payment.pending:
|
||||||
|
refreshed_payment = await update_pending_payment(payment, conn=conn)
|
||||||
|
payments.append(refreshed_payment)
|
||||||
|
else:
|
||||||
|
payments.append(payment)
|
||||||
|
|
||||||
|
return Page(data=payments, total=page.total)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
@@ -199,19 +214,19 @@ async def api_payments_paginated(
|
|||||||
)
|
)
|
||||||
async def api_all_payments_paginated(
|
async def api_all_payments_paginated(
|
||||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
):
|
):
|
||||||
if user.admin:
|
if account_id.is_admin_id:
|
||||||
# admin user can see payments from all wallets
|
# admin user can see payments from all wallets
|
||||||
for_user_id = None
|
for_user_id = None
|
||||||
else:
|
else:
|
||||||
# regular user can only see payments from their wallets
|
# regular user can only see payments from their wallets
|
||||||
for_user_id = user.id
|
for_user_id = account_id.id
|
||||||
|
|
||||||
return await get_payments_paginated(
|
async with db.connect() as conn:
|
||||||
filters=filters,
|
return await get_payments_paginated(
|
||||||
user_id=for_user_id,
|
filters=filters, user_id=for_user_id, conn=conn
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.post(
|
@payment_router.post(
|
||||||
@@ -234,10 +249,10 @@ async def api_all_payments_paginated(
|
|||||||
)
|
)
|
||||||
async def api_payments_create(
|
async def api_payments_create(
|
||||||
invoice_data: CreateInvoice,
|
invoice_data: CreateInvoice,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key),
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
wallet_id = wallet.wallet.id
|
wallet_id = key_info.wallet.id
|
||||||
if invoice_data.out is True and wallet.key_type == KeyType.admin:
|
if invoice_data.out is True and key_info.key_type == KeyType.admin:
|
||||||
if not invoice_data.bolt11:
|
if not invoice_data.bolt11:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
@@ -247,6 +262,7 @@ async def api_payments_create(
|
|||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
payment_request=invoice_data.bolt11,
|
payment_request=invoice_data.bolt11,
|
||||||
extra=invoice_data.extra,
|
extra=invoice_data.extra,
|
||||||
|
labels=invoice_data.labels,
|
||||||
)
|
)
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
@@ -260,6 +276,26 @@ async def api_payments_create(
|
|||||||
return await create_payment_request(wallet_id, invoice_data)
|
return await create_payment_request(wallet_id, invoice_data)
|
||||||
|
|
||||||
|
|
||||||
|
@payment_router.put("/{payment_hash}/labels")
|
||||||
|
async def api_update_payment_labels(
|
||||||
|
payment_hash: str,
|
||||||
|
data: UpdatePaymentLabels,
|
||||||
|
key_type: BaseWalletTypeInfo = Depends(require_base_admin_key),
|
||||||
|
) -> SimpleStatus:
|
||||||
|
payment = await get_standalone_payment(payment_hash, wallet_id=key_type.wallet.id)
|
||||||
|
if payment is None:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Payment does not exist.")
|
||||||
|
account = await get_account(key_type.wallet.user)
|
||||||
|
if not account:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Account does not exist.")
|
||||||
|
|
||||||
|
# only keep labels that belong to the user
|
||||||
|
user_label_names = [label.name for label in account.extra.labels]
|
||||||
|
payment.labels = [label for label in data.labels if label in user_label_names]
|
||||||
|
await update_payment(payment)
|
||||||
|
return SimpleStatus(success=True, message="Payment labels updated.")
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get("/fee-reserve")
|
@payment_router.get("/fee-reserve")
|
||||||
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||||
invoice_obj = bolt11.decode(invoice)
|
invoice_obj = bolt11.decode(invoice)
|
||||||
@@ -301,7 +337,7 @@ async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
|
|||||||
return {"paid": False, "status": "failed"}
|
return {"paid": False, "status": "failed"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
status = await payment.check_status()
|
payment = await update_pending_payment(payment)
|
||||||
except Exception:
|
except Exception:
|
||||||
if wallet and wallet.id == payment.wallet_id:
|
if wallet and wallet.id == payment.wallet_id:
|
||||||
return {"paid": False, "details": payment}
|
return {"paid": False, "details": payment}
|
||||||
@@ -310,7 +346,7 @@ async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
|
|||||||
if wallet and wallet.id == payment.wallet_id:
|
if wallet and wallet.id == payment.wallet_id:
|
||||||
return {
|
return {
|
||||||
"paid": payment.success,
|
"paid": payment.success,
|
||||||
"status": f"{status!s}",
|
"status": f"{payment.status!s}",
|
||||||
"preimage": payment.preimage,
|
"preimage": payment.preimage,
|
||||||
"details": payment,
|
"details": payment,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ from fastapi import (
|
|||||||
)
|
)
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
|
|
||||||
|
from lnbits.core.models.wallets import BaseWalletTypeInfo
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_base_invoice_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
@@ -50,12 +51,12 @@ async def api_create_tinyurl(
|
|||||||
description="get a tinyurl by id",
|
description="get a tinyurl by id",
|
||||||
)
|
)
|
||||||
async def api_get_tinyurl(
|
async def api_get_tinyurl(
|
||||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
tinyurl_id: str, key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
tinyurl = await get_tinyurl(tinyurl_id)
|
tinyurl = await get_tinyurl(tinyurl_id)
|
||||||
if tinyurl:
|
if tinyurl:
|
||||||
if tinyurl.wallet == wallet.wallet.id:
|
if tinyurl.wallet == key_info.wallet.id:
|
||||||
return tinyurl
|
return tinyurl
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
|
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from lnbits.core.crud import (
|
|||||||
update_admin_settings,
|
update_admin_settings,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.crud.wallets import delete_wallet_by_id
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccountFilters,
|
AccountFilters,
|
||||||
AccountOverview,
|
AccountOverview,
|
||||||
@@ -115,12 +116,12 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
|||||||
|
|
||||||
@users_router.put("/user/{user_id}", name="Update user")
|
@users_router.put("/user/{user_id}", name="Update user")
|
||||||
async def api_update_user(
|
async def api_update_user(
|
||||||
user_id: str, data: CreateUser, user: User = Depends(check_admin)
|
user_id: str, data: CreateUser, account: Account = Depends(check_admin)
|
||||||
) -> CreateUser:
|
) -> CreateUser:
|
||||||
if user_id != data.id:
|
if user_id != data.id:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
|
||||||
|
|
||||||
if user_id == settings.super_user and user.id != settings.super_user:
|
if user_id == settings.super_user and account.id != settings.super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="Action only allowed for super user.",
|
detail="Action only allowed for super user.",
|
||||||
@@ -154,14 +155,11 @@ async def api_update_user(
|
|||||||
name="Delete user by Id",
|
name="Delete user by Id",
|
||||||
)
|
)
|
||||||
async def api_users_delete_user(
|
async def api_users_delete_user(
|
||||||
user_id: str, user: User = Depends(check_admin)
|
user_id: str, account: Account = Depends(check_admin)
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
wallets = await get_wallets(user_id, deleted=False)
|
wallets = await get_wallets(user_id, deleted=False)
|
||||||
if len(wallets) > 0:
|
for wallet in wallets:
|
||||||
raise HTTPException(
|
await delete_wallet_by_id(wallet.id)
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="Cannot delete user with wallets.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if user_id == settings.super_user:
|
if user_id == settings.super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -169,7 +167,7 @@ async def api_users_delete_user(
|
|||||||
detail="Cannot delete super user.",
|
detail="Cannot delete super user.",
|
||||||
)
|
)
|
||||||
|
|
||||||
if user_id in settings.lnbits_admin_users and not user.super_user:
|
if user_id in settings.lnbits_admin_users and not account.is_super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="Only super_user can delete admin user.",
|
detail="Only super_user can delete admin user.",
|
||||||
@@ -224,7 +222,7 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
|||||||
|
|
||||||
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
||||||
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
||||||
return await get_wallets(user_id)
|
return await get_wallets(user_id, deleted=None)
|
||||||
|
|
||||||
|
|
||||||
@users_router.post("/user/{user_id}/wallet", name="Create a new wallet for user")
|
@users_router.post("/user/{user_id}/wallet", name="Create a new wallet for user")
|
||||||
@@ -247,7 +245,7 @@ async def api_users_create_user_wallet(
|
|||||||
"/user/{user_id}/wallet/{wallet}/undelete", name="Reactivate deleted wallet"
|
"/user/{user_id}/wallet/{wallet}/undelete", name="Reactivate deleted wallet"
|
||||||
)
|
)
|
||||||
async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
|
async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
|
||||||
wal = await get_wallet(wallet)
|
wal = await get_wallet(wallet, deleted=True)
|
||||||
if not wal:
|
if not wal:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
@@ -295,7 +293,7 @@ async def api_users_delete_all_user_wallet(user_id: str) -> SimpleStatus:
|
|||||||
"The second time it is called will delete the entry from the DB",
|
"The second time it is called will delete the entry from the DB",
|
||||||
)
|
)
|
||||||
async def api_users_delete_user_wallet(
|
async def api_users_delete_user_wallet(
|
||||||
user_id: str, wallet: str, user: User = Depends(check_admin)
|
user_id: str, wallet: str, account: Account = Depends(check_admin)
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
wal = await get_wallet(wallet)
|
wal = await get_wallet(wallet)
|
||||||
if not wal:
|
if not wal:
|
||||||
@@ -304,7 +302,7 @@ async def api_users_delete_user_wallet(
|
|||||||
detail="Wallet does not exist.",
|
detail="Wallet does not exist.",
|
||||||
)
|
)
|
||||||
|
|
||||||
if user_id == settings.super_user and user.id != settings.super_user:
|
if user_id == settings.super_user and account.id != settings.super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="Action only allowed for super user.",
|
detail="Action only allowed for super user.",
|
||||||
|
|||||||
@@ -8,21 +8,38 @@ from fastapi import (
|
|||||||
HTTPException,
|
HTTPException,
|
||||||
)
|
)
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_wallets_paginated
|
from lnbits.core.crud.wallets import (
|
||||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
|
create_wallet,
|
||||||
|
get_wallets_paginated,
|
||||||
|
)
|
||||||
|
from lnbits.core.models import CreateWallet, KeyType, Wallet, WalletTypeInfo
|
||||||
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
|
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
|
||||||
from lnbits.core.models.wallets import WalletsFilters
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
|
from lnbits.core.models.users import Account, AccountId
|
||||||
|
from lnbits.core.models.wallets import (
|
||||||
|
WalletsFilters,
|
||||||
|
WalletSharePermission,
|
||||||
|
WalletType,
|
||||||
|
)
|
||||||
|
from lnbits.core.services.wallets import (
|
||||||
|
create_lightning_shared_wallet,
|
||||||
|
delete_wallet_share,
|
||||||
|
invite_to_wallet,
|
||||||
|
reject_wallet_invitation,
|
||||||
|
update_wallet_share_permissions,
|
||||||
|
)
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
check_user_exists,
|
check_account_exists,
|
||||||
|
check_account_id_exists,
|
||||||
parse_filters,
|
parse_filters,
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import generate_filter_params_openapi
|
from lnbits.helpers import generate_filter_params_openapi
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_wallet,
|
|
||||||
delete_wallet,
|
delete_wallet,
|
||||||
get_wallet,
|
get_wallet,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
@@ -51,17 +68,46 @@ async def api_wallet(key_info: WalletTypeInfo = Depends(require_invoice_key)):
|
|||||||
openapi_extra=generate_filter_params_openapi(WalletsFilters),
|
openapi_extra=generate_filter_params_openapi(WalletsFilters),
|
||||||
)
|
)
|
||||||
async def api_wallets_paginated(
|
async def api_wallets_paginated(
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
filters: Filters = Depends(parse_filters(WalletsFilters)),
|
filters: Filters = Depends(parse_filters(WalletsFilters)),
|
||||||
):
|
):
|
||||||
page = await get_wallets_paginated(
|
page = await get_wallets_paginated(
|
||||||
user_id=user.id,
|
user_id=account_id.id,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
||||||
|
@wallet_router.put("/share/invite")
|
||||||
|
async def api_invite_wallet_share(
|
||||||
|
data: WalletSharePermission, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> WalletSharePermission:
|
||||||
|
return await invite_to_wallet(key_info.wallet, data)
|
||||||
|
|
||||||
|
|
||||||
|
@wallet_router.delete("/share/invite/{share_request_id}")
|
||||||
|
async def api_reject_wallet_invitation(
|
||||||
|
share_request_id: str, invited_user: Account = Depends(check_account_exists)
|
||||||
|
) -> SimpleStatus:
|
||||||
|
await reject_wallet_invitation(invited_user.id, share_request_id)
|
||||||
|
return SimpleStatus(success=True, message="Invitation rejected.")
|
||||||
|
|
||||||
|
|
||||||
|
@wallet_router.put("/share")
|
||||||
|
async def api_accept_wallet_share_request(
|
||||||
|
data: WalletSharePermission, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> WalletSharePermission:
|
||||||
|
return await update_wallet_share_permissions(key_info.wallet, data)
|
||||||
|
|
||||||
|
|
||||||
|
@wallet_router.delete("/share/{share_request_id}")
|
||||||
|
async def api_delete_wallet_share_permissions(
|
||||||
|
share_request_id: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> SimpleStatus:
|
||||||
|
return await delete_wallet_share(key_info.wallet, share_request_id)
|
||||||
|
|
||||||
|
|
||||||
@wallet_router.put("/{new_name}")
|
@wallet_router.put("/{new_name}")
|
||||||
async def api_update_wallet_name(
|
async def api_update_wallet_name(
|
||||||
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
@@ -70,6 +116,7 @@ async def api_update_wallet_name(
|
|||||||
if not wallet:
|
if not wallet:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||||
wallet.name = new_name
|
wallet.name = new_name
|
||||||
|
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
return {
|
return {
|
||||||
"id": wallet.id,
|
"id": wallet.id,
|
||||||
@@ -80,12 +127,17 @@ async def api_update_wallet_name(
|
|||||||
|
|
||||||
@wallet_router.put("/reset/{wallet_id}")
|
@wallet_router.put("/reset/{wallet_id}")
|
||||||
async def api_reset_wallet_keys(
|
async def api_reset_wallet_keys(
|
||||||
wallet_id: str, user: User = Depends(check_user_exists)
|
wallet_id: str,
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
wallet = await get_wallet(wallet_id)
|
wallet = await get_wallet(wallet_id)
|
||||||
if not wallet or wallet.user != user.id:
|
if not wallet or wallet.user != account_id.id:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||||
|
|
||||||
|
cache.pop(f"auth:wallet:{wallet.id}")
|
||||||
|
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
||||||
|
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
||||||
|
|
||||||
wallet.adminkey = uuid4().hex
|
wallet.adminkey = uuid4().hex
|
||||||
wallet.inkey = uuid4().hex
|
wallet.inkey = uuid4().hex
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
@@ -124,16 +176,17 @@ async def api_update_wallet(
|
|||||||
wallet.extra.color = color or wallet.extra.color
|
wallet.extra.color = color or wallet.extra.color
|
||||||
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
|
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
|
||||||
wallet.currency = currency if currency is not None else wallet.currency
|
wallet.currency = currency if currency is not None else wallet.currency
|
||||||
|
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
@wallet_router.delete("/{wallet_id}")
|
@wallet_router.delete("/{wallet_id}")
|
||||||
async def api_delete_wallet(
|
async def api_delete_wallet(
|
||||||
wallet_id: str, user: User = Depends(check_user_exists)
|
wallet_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||||
) -> None:
|
) -> None:
|
||||||
wallet = await get_wallet(wallet_id)
|
wallet = await get_wallet(wallet_id)
|
||||||
if not wallet or wallet.user != user.id:
|
if not wallet or wallet.user != account_id.id:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||||
|
|
||||||
await delete_wallet(
|
await delete_wallet(
|
||||||
@@ -144,7 +197,25 @@ async def api_delete_wallet(
|
|||||||
|
|
||||||
@wallet_router.post("")
|
@wallet_router.post("")
|
||||||
async def api_create_wallet(
|
async def api_create_wallet(
|
||||||
data: CreateWallet,
|
data: CreateWallet, account_id: AccountId = Depends(check_account_id_exists)
|
||||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
return await create_wallet(user_id=key_info.wallet.user, wallet_name=data.name)
|
|
||||||
|
if data.wallet_type not in list(WalletType):
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
f"Wallet type {data.wallet_type} does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.wallet_type == WalletType.LIGHTNING_SHARED:
|
||||||
|
if not data.shared_wallet_id:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
"Shared wallet ID is required for shared wallets.",
|
||||||
|
)
|
||||||
|
return await create_lightning_shared_wallet(
|
||||||
|
user_id=account_id.id,
|
||||||
|
source_wallet_id=data.shared_wallet_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# default WalletType.LIGHTNING:
|
||||||
|
return await create_wallet(user_id=account_id.id, wallet_name=data.name)
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ from lnbits.core.models import (
|
|||||||
CreateWebPushSubscription,
|
CreateWebPushSubscription,
|
||||||
WebPushSubscription,
|
WebPushSubscription,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.users import User
|
from lnbits.core.models.users import AccountId
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
check_user_exists,
|
check_account_id_exists,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
@@ -33,20 +33,20 @@ webpush_router = APIRouter(prefix="/api/v1/webpush", tags=["Webpush"])
|
|||||||
async def api_create_webpush_subscription(
|
async def api_create_webpush_subscription(
|
||||||
request: Request,
|
request: Request,
|
||||||
data: CreateWebPushSubscription,
|
data: CreateWebPushSubscription,
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
) -> WebPushSubscription:
|
) -> WebPushSubscription:
|
||||||
try:
|
try:
|
||||||
subscription = json.loads(data.subscription)
|
subscription = json.loads(data.subscription)
|
||||||
endpoint = subscription["endpoint"]
|
endpoint = subscription["endpoint"]
|
||||||
host = urlparse(str(request.url)).netloc
|
host = urlparse(str(request.url)).netloc
|
||||||
|
|
||||||
subscription = await get_webpush_subscription(endpoint, user.id)
|
subscription = await get_webpush_subscription(endpoint, account_id.id)
|
||||||
if subscription:
|
if subscription:
|
||||||
return subscription
|
return subscription
|
||||||
else:
|
else:
|
||||||
return await create_webpush_subscription(
|
return await create_webpush_subscription(
|
||||||
endpoint,
|
endpoint,
|
||||||
user.id,
|
account_id.id,
|
||||||
data.subscription,
|
data.subscription,
|
||||||
host,
|
host,
|
||||||
)
|
)
|
||||||
@@ -61,13 +61,13 @@ async def api_create_webpush_subscription(
|
|||||||
@webpush_router.delete("", status_code=HTTPStatus.OK)
|
@webpush_router.delete("", status_code=HTTPStatus.OK)
|
||||||
async def api_delete_webpush_subscription(
|
async def api_delete_webpush_subscription(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
endpoint = unquote(
|
endpoint = unquote(
|
||||||
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
|
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
|
||||||
)
|
)
|
||||||
count = await delete_webpush_subscription(endpoint, user.id)
|
count = await delete_webpush_subscription(endpoint, account_id.id)
|
||||||
return {"count": count}
|
return {"count": count}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(exc)
|
logger.debug(exc)
|
||||||
|
|||||||
+92
-25
@@ -130,6 +130,12 @@ class Compat:
|
|||||||
return "BIGINT"
|
return "BIGINT"
|
||||||
return "INT"
|
return "INT"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blob(self) -> str:
|
||||||
|
if self.type in {POSTGRES}:
|
||||||
|
return "BYTEA"
|
||||||
|
return "BLOB"
|
||||||
|
|
||||||
def timestamp_placeholder(self, key: str) -> str:
|
def timestamp_placeholder(self, key: str) -> str:
|
||||||
return compat_timestamp_placeholder(key)
|
return compat_timestamp_placeholder(key)
|
||||||
|
|
||||||
@@ -216,18 +222,34 @@ class Connection(Compat):
|
|||||||
filters: Filters | None = None,
|
filters: Filters | None = None,
|
||||||
model: type[TModel] | None = None,
|
model: type[TModel] | None = None,
|
||||||
group_by: list[str] | None = None,
|
group_by: list[str] | None = None,
|
||||||
|
table_name: str | None = None,
|
||||||
) -> Page[TModel]:
|
) -> Page[TModel]:
|
||||||
|
"""
|
||||||
|
Parameters:
|
||||||
|
query: The main SQL query string to execute for data retrieval.
|
||||||
|
where: list of additional WHERE clause conditions to filter results.
|
||||||
|
values: dictionary of parameter values to be used in the SQL query.
|
||||||
|
filters: object for advanced filtering, sorting, and pagination logic.
|
||||||
|
model: pydantic model type to map query results into model instances.
|
||||||
|
group_by: list of column names to group results by in the SQL query.
|
||||||
|
table_name: if provided some optimisations can be applied.
|
||||||
|
"""
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|
||||||
|
if table_name:
|
||||||
|
if not _valid_sql_name(table_name):
|
||||||
|
raise ValueError(f"Invalid table name: '{table_name}'.")
|
||||||
|
filters.set_table_name(table_name)
|
||||||
|
|
||||||
clause = filters.where(where)
|
clause = filters.where(where)
|
||||||
parsed_values = filters.values(values)
|
parsed_values = filters.values(values)
|
||||||
|
|
||||||
group_by_string = ""
|
group_by_string = ""
|
||||||
if group_by:
|
if group_by:
|
||||||
for field in group_by:
|
for field in group_by:
|
||||||
if not re.fullmatch(
|
if not _valid_sql_name(field):
|
||||||
r"[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?", field
|
|
||||||
):
|
|
||||||
raise ValueError("Value for GROUP BY is invalid")
|
raise ValueError("Value for GROUP BY is invalid")
|
||||||
group_by_string = f"GROUP BY {', '.join(group_by)}"
|
group_by_string = f"GROUP BY {', '.join(group_by)}"
|
||||||
|
|
||||||
@@ -245,16 +267,17 @@ class Connection(Compat):
|
|||||||
if rows:
|
if rows:
|
||||||
# no need for extra query if no pagination is specified
|
# no need for extra query if no pagination is specified
|
||||||
if filters.offset or filters.limit:
|
if filters.offset or filters.limit:
|
||||||
result = await self.execute(
|
if table_name:
|
||||||
f"""
|
count_query = f"SELECT COUNT(*) as count FROM {table_name} {clause}" # noqa: S608
|
||||||
SELECT COUNT(*) as count FROM (
|
else:
|
||||||
|
count_query = f"""SELECT COUNT(*) as count
|
||||||
|
FROM (
|
||||||
{query}
|
{query}
|
||||||
{clause}
|
{clause}
|
||||||
{group_by_string}
|
{group_by_string}
|
||||||
) as count
|
) as count""" # noqa: S608
|
||||||
""", # noqa: S608
|
|
||||||
parsed_values,
|
result = await self.execute(count_query, parsed_values)
|
||||||
)
|
|
||||||
row = result.mappings().first()
|
row = result.mappings().first()
|
||||||
result.close()
|
result.close()
|
||||||
count = int(row.get("count", 0))
|
count = int(row.get("count", 0))
|
||||||
@@ -387,9 +410,12 @@ class Database(Compat):
|
|||||||
filters: Filters | None = None,
|
filters: Filters | None = None,
|
||||||
model: type[TModel] | None = None,
|
model: type[TModel] | None = None,
|
||||||
group_by: list[str] | None = None,
|
group_by: list[str] | None = None,
|
||||||
|
table_name: str | None = None,
|
||||||
) -> Page[TModel]:
|
) -> Page[TModel]:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
return await conn.fetch_page(
|
||||||
|
query, where, values, filters, model, group_by, table_name
|
||||||
|
)
|
||||||
|
|
||||||
async def execute(self, query: str, values: dict | None = None):
|
async def execute(self, query: str, values: dict | None = None):
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
@@ -425,6 +451,8 @@ class Operator(Enum):
|
|||||||
INCLUDE = "in"
|
INCLUDE = "in"
|
||||||
EXCLUDE = "ex"
|
EXCLUDE = "ex"
|
||||||
LIKE = "like"
|
LIKE = "like"
|
||||||
|
EVERY = "every"
|
||||||
|
ANY = "any"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_sql(self):
|
def as_sql(self):
|
||||||
@@ -444,7 +472,7 @@ class Operator(Enum):
|
|||||||
return ">="
|
return ">="
|
||||||
elif self == Operator.LE:
|
elif self == Operator.LE:
|
||||||
return "<="
|
return "<="
|
||||||
elif self == Operator.LIKE:
|
elif self in {Operator.LIKE, Operator.EVERY, Operator.ANY}:
|
||||||
return "LIKE"
|
return "LIKE"
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown SQL Operator")
|
raise ValueError("Unknown SQL Operator")
|
||||||
@@ -466,6 +494,7 @@ class Page(BaseModel, Generic[T]):
|
|||||||
|
|
||||||
|
|
||||||
class Filter(BaseModel, Generic[TFilterModel]):
|
class Filter(BaseModel, Generic[TFilterModel]):
|
||||||
|
table_name: str | None = None
|
||||||
field: str
|
field: str
|
||||||
op: Operator = Operator.EQ
|
op: Operator = Operator.EQ
|
||||||
model: type[TFilterModel] | None
|
model: type[TFilterModel] | None
|
||||||
@@ -475,6 +504,8 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
|||||||
def parse_query(
|
def parse_query(
|
||||||
cls, key: str, raw_values: list[Any], model: type[TFilterModel], i: int = 0
|
cls, key: str, raw_values: list[Any], model: type[TFilterModel], i: int = 0
|
||||||
):
|
):
|
||||||
|
if i > 1000 or len(raw_values) > 1000:
|
||||||
|
raise ValueError("Too many filter values")
|
||||||
# Key format:
|
# Key format:
|
||||||
# key[operator]
|
# key[operator]
|
||||||
# e.g. name[eq]
|
# e.g. name[eq]
|
||||||
@@ -491,11 +522,14 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
|||||||
if field in model.__fields__:
|
if field in model.__fields__:
|
||||||
compare_field = model.__fields__[field]
|
compare_field = model.__fields__[field]
|
||||||
values: dict = {}
|
values: dict = {}
|
||||||
for raw_value in raw_values:
|
if op in {Operator.EVERY, Operator.ANY, Operator.INCLUDE, Operator.EXCLUDE}:
|
||||||
|
raw_values = [v for rv in raw_values for v in rv.split(",")]
|
||||||
|
|
||||||
|
for index, raw_value in enumerate(raw_values):
|
||||||
validated, errors = compare_field.validate(raw_value, {}, loc="none")
|
validated, errors = compare_field.validate(raw_value, {}, loc="none")
|
||||||
if errors:
|
if errors:
|
||||||
raise ValidationError(errors=[errors], model=model)
|
raise ValidationError(errors=[errors], model=model)
|
||||||
values[f"{field}__{i}"] = validated
|
values[f"{field}__{index}"] = validated
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown filter field")
|
raise ValueError("Unknown filter field")
|
||||||
|
|
||||||
@@ -503,15 +537,24 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def statement(self) -> str:
|
def statement(self) -> str:
|
||||||
|
prefix = f"{self.table_name}." if self.table_name else ""
|
||||||
stmt = []
|
stmt = []
|
||||||
for key in self.values.keys() if self.values else []:
|
for key in self.values.keys() if self.values else []:
|
||||||
clean_key = key.split("__")[0]
|
if self.model and self.model.__fields__[self.field].type_ == datetime:
|
||||||
if self.model and self.model.__fields__[clean_key].type_ == datetime:
|
|
||||||
placeholder = compat_timestamp_placeholder(key)
|
placeholder = compat_timestamp_placeholder(key)
|
||||||
|
stmt.append(f"{prefix}{self.field} {self.op.as_sql} {placeholder}")
|
||||||
|
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
||||||
|
stmt.append(f":{key}")
|
||||||
else:
|
else:
|
||||||
placeholder = f":{key}"
|
stmt.append(f"{prefix}{self.field} {self.op.as_sql} :{key}")
|
||||||
stmt.append(f"{clean_key} {self.op.as_sql} {placeholder}")
|
|
||||||
return " OR ".join(stmt)
|
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
||||||
|
statement = f"{prefix}{self.field} {self.op.as_sql} ({', '.join(stmt)})"
|
||||||
|
elif self.op == Operator.EVERY:
|
||||||
|
statement = " AND ".join(stmt)
|
||||||
|
else:
|
||||||
|
statement = " OR ".join(stmt)
|
||||||
|
return f"({statement})"
|
||||||
|
|
||||||
|
|
||||||
class Filters(BaseModel, Generic[TFilterModel]):
|
class Filters(BaseModel, Generic[TFilterModel]):
|
||||||
@@ -527,13 +570,14 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
search: str | None = None
|
search: str | None = None
|
||||||
|
|
||||||
offset: int | None = None
|
offset: int | None = None
|
||||||
limit: int | None = None
|
limit: int | None = 10
|
||||||
|
|
||||||
sortby: str | None = None
|
sortby: str | None = None
|
||||||
direction: Literal["asc", "desc"] | None = None
|
direction: Literal["asc", "desc"] | None = None
|
||||||
|
|
||||||
model: type[TFilterModel] | None = None
|
model: type[TFilterModel] | None = None
|
||||||
|
|
||||||
|
table_name: str | None = None
|
||||||
|
|
||||||
@root_validator(pre=True)
|
@root_validator(pre=True)
|
||||||
def validate_sortby(cls, values):
|
def validate_sortby(cls, values):
|
||||||
sortby = values.get("sortby")
|
sortby = values.get("sortby")
|
||||||
@@ -548,8 +592,8 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
|
|
||||||
def pagination(self) -> str:
|
def pagination(self) -> str:
|
||||||
stmt = ""
|
stmt = ""
|
||||||
if self.limit:
|
self.limit = self.limit or 10
|
||||||
stmt += f"LIMIT {self.limit} "
|
stmt += f"LIMIT {min(1000, self.limit)} "
|
||||||
if self.offset:
|
if self.offset:
|
||||||
stmt += f"OFFSET {self.offset}"
|
stmt += f"OFFSET {self.offset}"
|
||||||
return stmt
|
return stmt
|
||||||
@@ -575,7 +619,8 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
|
|
||||||
def order_by(self) -> str:
|
def order_by(self) -> str:
|
||||||
if self.sortby:
|
if self.sortby:
|
||||||
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
prefix = f"{self.table_name}." if self.table_name else ""
|
||||||
|
return f"ORDER BY {prefix}{self.sortby} {self.direction or 'asc'}"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def values(self, values: dict | None = None) -> dict:
|
def values(self, values: dict | None = None) -> dict:
|
||||||
@@ -587,12 +632,26 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
for key, value in page_filter.values.items():
|
for key, value in page_filter.values.items():
|
||||||
if page_filter.op == Operator.LIKE:
|
if page_filter.op == Operator.LIKE:
|
||||||
values[key] = f"%{value}%"
|
values[key] = f"%{value}%"
|
||||||
|
elif page_filter.op in {Operator.EVERY, Operator.ANY}:
|
||||||
|
values[key] = f"""%"{value}"%"""
|
||||||
else:
|
else:
|
||||||
values[key] = value
|
values[key] = value
|
||||||
if self.search and self.model:
|
if self.search and self.model:
|
||||||
values["search"] = f"%{self.search.lower()}%"
|
values["search"] = f"%{self.search.lower()}%"
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
def set_table_name(self, table_name: str) -> None:
|
||||||
|
self.table_name = table_name
|
||||||
|
for page_filter in self.filters:
|
||||||
|
page_filter.table_name = table_name
|
||||||
|
|
||||||
|
|
||||||
|
class DbJsonEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, Enum):
|
||||||
|
return o.value
|
||||||
|
return super().default(o)
|
||||||
|
|
||||||
|
|
||||||
def insert_query(table_name: str, model: BaseModel) -> str:
|
def insert_query(table_name: str, model: BaseModel) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -648,7 +707,7 @@ def model_to_dict(model: BaseModel) -> dict:
|
|||||||
or type_ is dict
|
or type_ is dict
|
||||||
or get_origin(outertype_) is list
|
or get_origin(outertype_) is list
|
||||||
):
|
):
|
||||||
_dict[key] = json.dumps(value)
|
_dict[key] = json.dumps(value, cls=DbJsonEncoder)
|
||||||
continue
|
continue
|
||||||
_dict[key] = value
|
_dict[key] = value
|
||||||
|
|
||||||
@@ -722,3 +781,11 @@ def _safe_load_json(value: str) -> dict:
|
|||||||
# DB is corrupted if it gets here
|
# DB is corrupted if it gets here
|
||||||
logger.error(f"Failed to decode JSON: '{value}'")
|
logger.error(f"Failed to decode JSON: '{value}'")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_sql_name(name: str) -> bool:
|
||||||
|
"""Check if a SQL name is valid (alphanumeric and underscores only)"""
|
||||||
|
return (
|
||||||
|
re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?", name)
|
||||||
|
is not None
|
||||||
|
)
|
||||||
|
|||||||
+246
-53
@@ -19,6 +19,8 @@ from lnbits.core.crud import (
|
|||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import get_user_access_control_lists
|
from lnbits.core.crud.users import get_user_access_control_lists
|
||||||
|
from lnbits.core.crud.wallets import get_base_wallet_for_key
|
||||||
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccessTokenPayload,
|
AccessTokenPayload,
|
||||||
Account,
|
Account,
|
||||||
@@ -27,9 +29,12 @@ from lnbits.core.models import (
|
|||||||
User,
|
User,
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.models.users import AccountId
|
||||||
|
from lnbits.core.models.wallets import BaseWallet, BaseWalletTypeInfo
|
||||||
from lnbits.db import Connection, Filter, Filters, TFilterModel
|
from lnbits.db import Connection, Filter, Filters, TFilterModel
|
||||||
from lnbits.helpers import normalize_path, path_segments
|
from lnbits.helpers import normalize_path, path_segments, sha256s
|
||||||
from lnbits.settings import AuthMethods, settings
|
from lnbits.settings import AuthMethods, settings
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(
|
oauth2_scheme = OAuth2PasswordBearer(
|
||||||
tokenUrl="api/v1/auth",
|
tokenUrl="api/v1/auth",
|
||||||
@@ -52,7 +57,7 @@ api_key_query = APIKeyQuery(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class KeyChecker(SecurityBase):
|
class BaseKeyChecker(SecurityBase):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
@@ -77,8 +82,7 @@ class KeyChecker(SecurityBase):
|
|||||||
)
|
)
|
||||||
self.model: APIKey = openapi_model # type: ignore
|
self.model: APIKey = openapi_model # type: ignore
|
||||||
|
|
||||||
async def __call__(self, request: Request) -> WalletTypeInfo:
|
def _extract_key_value(self, request):
|
||||||
|
|
||||||
key_value = (
|
key_value = (
|
||||||
self._api_key
|
self._api_key
|
||||||
if self._api_key
|
if self._api_key
|
||||||
@@ -91,27 +95,88 @@ class KeyChecker(SecurityBase):
|
|||||||
detail="No Api Key provided.",
|
detail="No Api Key provided.",
|
||||||
)
|
)
|
||||||
|
|
||||||
wallet = await get_wallet_for_key(key_value)
|
return key_value
|
||||||
|
|
||||||
if not wallet:
|
async def _extract_key_type(self, key_value: str, wallet: BaseWallet) -> KeyType:
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
detail="Wallet not found.",
|
|
||||||
)
|
|
||||||
|
|
||||||
request.scope["user_id"] = wallet.user
|
|
||||||
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
detail="Invalid adminkey.",
|
detail="Invalid adminkey.",
|
||||||
)
|
)
|
||||||
|
|
||||||
await _check_user_extension_access(wallet.user, request["path"])
|
|
||||||
|
|
||||||
key_type = KeyType.admin if wallet.adminkey == key_value else KeyType.invoice
|
key_type = KeyType.admin if wallet.adminkey == key_value else KeyType.invoice
|
||||||
|
return key_type
|
||||||
|
|
||||||
|
|
||||||
|
class KeyChecker(BaseKeyChecker):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str | None = None,
|
||||||
|
expected_key_type: KeyType | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(api_key, expected_key_type)
|
||||||
|
|
||||||
|
async def __call__(self, request: Request) -> WalletTypeInfo:
|
||||||
|
key_value = self._extract_key_value(request)
|
||||||
|
|
||||||
|
async with db.connect() as conn:
|
||||||
|
wallet = await get_wallet_for_key(key_value, conn=conn)
|
||||||
|
|
||||||
|
if not wallet:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="Wallet not found.",
|
||||||
|
)
|
||||||
|
|
||||||
|
request.scope["user_id"] = wallet.user
|
||||||
|
await _check_user_access(request, wallet.user, conn=conn)
|
||||||
|
|
||||||
|
key_type = await self._extract_key_type(key_value, wallet)
|
||||||
return WalletTypeInfo(key_type, wallet)
|
return WalletTypeInfo(key_type, wallet)
|
||||||
|
|
||||||
|
|
||||||
|
class LightKeyChecker(BaseKeyChecker):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str | None = None,
|
||||||
|
expected_key_type: KeyType | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(api_key, expected_key_type)
|
||||||
|
|
||||||
|
async def __call__(self, request: Request) -> BaseWalletTypeInfo:
|
||||||
|
key_value = self._extract_key_value(request)
|
||||||
|
cache_key = f"auth:x-api-key:{key_value}"
|
||||||
|
cache_time = settings.auth_authentication_cache_minutes * 60
|
||||||
|
|
||||||
|
async with db.connect() as conn:
|
||||||
|
if cache_time > 0:
|
||||||
|
key_info: BaseWalletTypeInfo | None = cache.get(cache_key)
|
||||||
|
if key_info:
|
||||||
|
request.scope["user_id"] = key_info.wallet.user
|
||||||
|
await _check_user_access(request, key_info.wallet.user, conn=conn)
|
||||||
|
return key_info
|
||||||
|
|
||||||
|
wallet = await get_base_wallet_for_key(key_value, conn=conn)
|
||||||
|
|
||||||
|
if not wallet:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="Wallet not found.",
|
||||||
|
)
|
||||||
|
request.scope["user_id"] = wallet.user
|
||||||
|
await _check_user_access(request, wallet.user, conn=conn)
|
||||||
|
|
||||||
|
key_type = await self._extract_key_type(key_value, wallet)
|
||||||
|
key_info = BaseWalletTypeInfo(key_type, wallet)
|
||||||
|
|
||||||
|
if cache_time > 0:
|
||||||
|
cache.set(cache_key, key_info, expiry=cache_time)
|
||||||
|
cache.set(f"auth:wallet:{wallet.id}", wallet, expiry=cache_time)
|
||||||
|
return key_info
|
||||||
|
|
||||||
|
|
||||||
async def require_admin_key(
|
async def require_admin_key(
|
||||||
request: Request,
|
request: Request,
|
||||||
api_key_header: str = Security(api_key_header),
|
api_key_header: str = Security(api_key_header),
|
||||||
@@ -124,6 +189,18 @@ async def require_admin_key(
|
|||||||
return await check(request)
|
return await check(request)
|
||||||
|
|
||||||
|
|
||||||
|
async def require_base_admin_key(
|
||||||
|
request: Request,
|
||||||
|
api_key_header: str = Security(api_key_header),
|
||||||
|
api_key_query: str = Security(api_key_query),
|
||||||
|
) -> BaseWalletTypeInfo:
|
||||||
|
check: LightKeyChecker = LightKeyChecker(
|
||||||
|
api_key=api_key_header or api_key_query,
|
||||||
|
expected_key_type=KeyType.admin,
|
||||||
|
)
|
||||||
|
return await check(request)
|
||||||
|
|
||||||
|
|
||||||
async def require_invoice_key(
|
async def require_invoice_key(
|
||||||
request: Request,
|
request: Request,
|
||||||
api_key_header: str = Security(api_key_header),
|
api_key_header: str = Security(api_key_header),
|
||||||
@@ -136,6 +213,18 @@ async def require_invoice_key(
|
|||||||
return await check(request)
|
return await check(request)
|
||||||
|
|
||||||
|
|
||||||
|
async def require_base_invoice_key(
|
||||||
|
request: Request,
|
||||||
|
api_key_header: str = Security(api_key_header),
|
||||||
|
api_key_query: str = Security(api_key_query),
|
||||||
|
) -> BaseWalletTypeInfo:
|
||||||
|
check: LightKeyChecker = LightKeyChecker(
|
||||||
|
api_key=api_key_header or api_key_query,
|
||||||
|
expected_key_type=KeyType.invoice,
|
||||||
|
)
|
||||||
|
return await check(request)
|
||||||
|
|
||||||
|
|
||||||
async def check_access_token(
|
async def check_access_token(
|
||||||
header_access_token: Annotated[str | None, Depends(oauth2_scheme)],
|
header_access_token: Annotated[str | None, Depends(oauth2_scheme)],
|
||||||
cookie_access_token: Annotated[str | None, Cookie()] = None,
|
cookie_access_token: Annotated[str | None, Cookie()] = None,
|
||||||
@@ -144,33 +233,95 @@ async def check_access_token(
|
|||||||
return header_access_token or cookie_access_token or bearer_access_token
|
return header_access_token or cookie_access_token or bearer_access_token
|
||||||
|
|
||||||
|
|
||||||
|
async def check_account_id_exists(
|
||||||
|
r: Request,
|
||||||
|
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||||
|
usr: UUID4 | None = None,
|
||||||
|
) -> AccountId:
|
||||||
|
cache_key: str | None = None
|
||||||
|
if access_token:
|
||||||
|
cache_key = f"auth:access_token:{sha256s(access_token)}"
|
||||||
|
elif usr:
|
||||||
|
cache_key = f"auth:user_id:{sha256s(usr.hex)}"
|
||||||
|
|
||||||
|
async with db.connect() as conn:
|
||||||
|
if cache_key and settings.auth_authentication_cache_minutes > 0:
|
||||||
|
account_id = cache.get(cache_key)
|
||||||
|
if account_id:
|
||||||
|
r.scope["user_id"] = account_id.id
|
||||||
|
await _check_user_access(r, account_id.id, conn=conn)
|
||||||
|
return account_id
|
||||||
|
|
||||||
|
account = await _check_account_exists(r, access_token, usr, conn=conn)
|
||||||
|
account_id = AccountId(id=account.id)
|
||||||
|
|
||||||
|
if cache_key and settings.auth_authentication_cache_minutes > 0:
|
||||||
|
cache.set(
|
||||||
|
cache_key,
|
||||||
|
account_id,
|
||||||
|
expiry=settings.auth_authentication_cache_minutes * 60,
|
||||||
|
)
|
||||||
|
|
||||||
|
return account_id
|
||||||
|
|
||||||
|
|
||||||
|
async def check_account_exists(
|
||||||
|
r: Request,
|
||||||
|
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||||
|
usr: UUID4 | None = None,
|
||||||
|
) -> Account:
|
||||||
|
return await _check_account_exists(r, access_token, usr)
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_account_exists(
|
||||||
|
r: Request,
|
||||||
|
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||||
|
usr: UUID4 | None = None,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Account:
|
||||||
|
"""
|
||||||
|
Check that the account exists based on access token or user id.
|
||||||
|
More performant version of `check_user_exists`.
|
||||||
|
Unlike `check_user_exists`, this function:
|
||||||
|
- does not fetch the user wallets
|
||||||
|
- caches the account info based on settings cache time
|
||||||
|
"""
|
||||||
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
|
if access_token:
|
||||||
|
account = await _get_account_from_token(
|
||||||
|
access_token, r["path"], r["method"], conn=new_conn
|
||||||
|
)
|
||||||
|
elif usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||||
|
account = await get_account(usr.hex, conn=new_conn)
|
||||||
|
if account and account.is_admin:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.FORBIDDEN, "User id only access for admins is forbidden."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.UNAUTHORIZED, "Missing user ID or access token."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not found.")
|
||||||
|
|
||||||
|
r.scope["user_id"] = account.id
|
||||||
|
await _check_user_access(r, account.id, conn=new_conn)
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def check_user_exists(
|
async def check_user_exists(
|
||||||
r: Request,
|
r: Request,
|
||||||
access_token: Annotated[str | None, Depends(check_access_token)],
|
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||||
usr: UUID4 | None = None,
|
usr: UUID4 | None = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
if access_token:
|
async with db.connect() as conn:
|
||||||
account = await _get_account_from_token(access_token, r["path"], r["method"])
|
account = await _check_account_exists(r, access_token, usr, conn=conn)
|
||||||
elif usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
user = await get_user_from_account(account, conn=conn)
|
||||||
account = await get_account(usr.hex)
|
|
||||||
if account and account.is_admin:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.FORBIDDEN, "User id only access for admins is forbidden."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing user ID or access token.")
|
|
||||||
|
|
||||||
if not account:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not found.")
|
|
||||||
|
|
||||||
r.scope["user_id"] = account.id
|
|
||||||
if not settings.is_user_allowed(account.id):
|
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "User not allowed.")
|
|
||||||
|
|
||||||
user = await get_user_from_account(account)
|
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not found.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not found.")
|
||||||
await _check_user_extension_access(user.id, r["path"])
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -198,29 +349,36 @@ async def access_token_payload(
|
|||||||
return AccessTokenPayload(**payload)
|
return AccessTokenPayload(**payload)
|
||||||
|
|
||||||
|
|
||||||
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
async def check_admin(
|
||||||
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
|
account: Annotated[Account, Depends(check_account_exists)],
|
||||||
|
) -> Account:
|
||||||
|
if (
|
||||||
|
account.id != settings.super_user
|
||||||
|
and account.id not in settings.lnbits_admin_users
|
||||||
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN, "User not authorized. No admin privileges."
|
HTTPStatus.FORBIDDEN, "User not authorized. No admin privileges."
|
||||||
)
|
)
|
||||||
if not user.has_password:
|
if not account.has_password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN, "Admin users must have credentials configured."
|
HTTPStatus.FORBIDDEN, "Admin users must have credentials configured."
|
||||||
)
|
)
|
||||||
|
|
||||||
return user
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
async def check_super_user(
|
||||||
if user.id != settings.super_user:
|
account: Annotated[Account, Depends(check_admin)],
|
||||||
|
) -> Account:
|
||||||
|
if account.id != settings.super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN, "User not authorized. No super user privileges."
|
HTTPStatus.FORBIDDEN, "User not authorized. No super user privileges."
|
||||||
)
|
)
|
||||||
if not user.has_password:
|
if not account.has_password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN, "Super user must have credentials configured."
|
HTTPStatus.FORBIDDEN, "Super user must have credentials configured."
|
||||||
)
|
)
|
||||||
return user
|
return account
|
||||||
|
|
||||||
|
|
||||||
def parse_filters(model: type[TFilterModel]):
|
def parse_filters(model: type[TFilterModel]):
|
||||||
@@ -280,9 +438,17 @@ async def check_user_extension_access(
|
|||||||
return SimpleStatus(success=True, message="OK")
|
return SimpleStatus(success=True, message="OK")
|
||||||
|
|
||||||
|
|
||||||
async def _check_user_extension_access(user_id: str, path: str):
|
async def _check_user_access(r: Request, user_id: str, conn: Connection | None = None):
|
||||||
|
if not settings.is_user_allowed(user_id):
|
||||||
|
raise HTTPException(HTTPStatus.FORBIDDEN, "User not allowed.")
|
||||||
|
await _check_user_extension_access(user_id, r["path"], conn=conn)
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_user_extension_access(
|
||||||
|
user_id: str, path: str, conn: Connection | None = None
|
||||||
|
):
|
||||||
ext_id = path_segments(path)[0]
|
ext_id = path_segments(path)[0]
|
||||||
status = await check_user_extension_access(user_id, ext_id)
|
status = await check_user_extension_access(user_id, ext_id, conn=conn)
|
||||||
if not status.success:
|
if not status.success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
@@ -291,12 +457,12 @@ async def _check_user_extension_access(user_id: str, path: str):
|
|||||||
|
|
||||||
|
|
||||||
async def _get_account_from_token(
|
async def _get_account_from_token(
|
||||||
access_token: str, path: str, method: str
|
access_token: str, path: str, method: str, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
try:
|
try:
|
||||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
||||||
return await _get_account_from_jwt_payload(
|
return await _get_account_from_jwt_payload(
|
||||||
AccessTokenPayload(**payload), path, method
|
AccessTokenPayload(**payload), path, method, conn=conn
|
||||||
)
|
)
|
||||||
|
|
||||||
except jwt.ExpiredSignatureError as exc:
|
except jwt.ExpiredSignatureError as exc:
|
||||||
@@ -309,33 +475,35 @@ async def _get_account_from_token(
|
|||||||
|
|
||||||
|
|
||||||
async def _get_account_from_jwt_payload(
|
async def _get_account_from_jwt_payload(
|
||||||
payload: AccessTokenPayload, path: str, method: str
|
payload: AccessTokenPayload, path: str, method: str, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
account = None
|
account = None
|
||||||
if payload.sub:
|
if payload.sub:
|
||||||
account = await get_account_by_username(payload.sub)
|
account = await get_account_by_username(payload.sub, conn=conn)
|
||||||
elif payload.usr:
|
elif payload.usr:
|
||||||
account = await get_account(payload.usr)
|
account = await get_account(payload.usr, conn=conn)
|
||||||
elif payload.email:
|
elif payload.email:
|
||||||
account = await get_account_by_email(payload.email)
|
account = await get_account_by_email(payload.email, conn=conn)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if payload.api_token_id:
|
if payload.api_token_id:
|
||||||
await _check_account_api_access(account.id, payload.api_token_id, path, method)
|
await _check_account_api_access(
|
||||||
|
account.id, payload.api_token_id, path, method, conn=conn
|
||||||
|
)
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def _check_account_api_access(
|
async def _check_account_api_access(
|
||||||
user_id: str, token_id: str, path: str, method: str
|
user_id: str, token_id: str, path: str, method: str, conn: Connection | None = None
|
||||||
):
|
):
|
||||||
segments = path.split("/")
|
segments = path.split("/")
|
||||||
if len(segments) < 3:
|
if len(segments) < 3:
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Not an API endpoint.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Not an API endpoint.")
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user_id)
|
acls = await get_user_access_control_lists(user_id, conn=conn)
|
||||||
acl = acls.get_acl_by_token_id(token_id)
|
acl = acls.get_acl_by_token_id(token_id)
|
||||||
if not acl:
|
if not acl:
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Invalid token id.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Invalid token id.")
|
||||||
@@ -359,3 +527,28 @@ def url_for_interceptor(original_method):
|
|||||||
# Upgraded extensions modify the path.
|
# Upgraded extensions modify the path.
|
||||||
# This interceptor ensures that the path is normalized.
|
# This interceptor ensures that the path is normalized.
|
||||||
Request.url_for = url_for_interceptor(Request.url_for) # type: ignore[method-assign]
|
Request.url_for = url_for_interceptor(Request.url_for) # type: ignore[method-assign]
|
||||||
|
|
||||||
|
|
||||||
|
async def check_admin_ui() -> None:
|
||||||
|
if not settings.lnbits_admin_ui:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail="Admin UI is disabled."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_extension_builder(
|
||||||
|
user: Annotated[User, Depends(check_user_exists)],
|
||||||
|
) -> None:
|
||||||
|
if not settings.lnbits_extensions_builder_activate_non_admins and not user.admin:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.FORBIDDEN,
|
||||||
|
"Extension Builder is disabled for non admin users.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_first_install():
|
||||||
|
if not settings.first_install:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Super user account has already been configured.",
|
||||||
|
)
|
||||||
|
|||||||
@@ -138,7 +138,9 @@ def register_exception_handlers(app: FastAPI): # noqa: C901
|
|||||||
|
|
||||||
@app.exception_handler(404)
|
@app.exception_handler(404)
|
||||||
async def error_handler_404(request: Request, exc: HTTPException):
|
async def error_handler_404(request: Request, exc: HTTPException):
|
||||||
logger.error(f"404: {request.url.path} {exc.status_code}: {exc.detail}")
|
|
||||||
|
if not request.url.path.endswith("routes.json"):
|
||||||
|
logger.error(f"404: {request.url.path} {exc.status_code}: {exc.detail}")
|
||||||
|
|
||||||
if not _is_browser_request(request):
|
if not _is_browser_request(request):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from loguru import logger
|
|||||||
from lnbits.fiat.base import FiatProvider
|
from lnbits.fiat.base import FiatProvider
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
from .paypal import PayPalWallet
|
||||||
from .stripe import StripeWallet
|
from .stripe import StripeWallet
|
||||||
|
|
||||||
fiat_module = importlib.import_module("lnbits.fiat")
|
fiat_module = importlib.import_module("lnbits.fiat")
|
||||||
@@ -15,6 +16,7 @@ fiat_module = importlib.import_module("lnbits.fiat")
|
|||||||
|
|
||||||
class FiatProviderType(Enum):
|
class FiatProviderType(Enum):
|
||||||
stripe = "StripeWallet"
|
stripe = "StripeWallet"
|
||||||
|
paypal = "PayPalWallet"
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_provider(name: str) -> FiatProvider | None:
|
async def get_fiat_provider(name: str) -> FiatProvider | None:
|
||||||
@@ -49,5 +51,6 @@ fiat_providers: dict[str, FiatProvider] = {}
|
|||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"PayPalWallet",
|
||||||
"StripeWallet",
|
"StripeWallet",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint, urlsafe_short_hash
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
from .base import (
|
||||||
|
FiatInvoiceResponse,
|
||||||
|
FiatPaymentFailedStatus,
|
||||||
|
FiatPaymentPendingStatus,
|
||||||
|
FiatPaymentResponse,
|
||||||
|
FiatPaymentStatus,
|
||||||
|
FiatPaymentSuccessStatus,
|
||||||
|
FiatProvider,
|
||||||
|
FiatStatusResponse,
|
||||||
|
FiatSubscriptionPaymentOptions,
|
||||||
|
FiatSubscriptionResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PayPalCheckoutOptions(BaseModel):
|
||||||
|
class Config:
|
||||||
|
extra = "ignore"
|
||||||
|
|
||||||
|
success_url: str | None = None
|
||||||
|
cancel_url: str | None = None
|
||||||
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class PayPalCreateInvoiceOptions(BaseModel):
|
||||||
|
class Config:
|
||||||
|
extra = "ignore"
|
||||||
|
|
||||||
|
checkout: PayPalCheckoutOptions | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PayPalWallet(FiatProvider):
|
||||||
|
"""https://developer.paypal.com/api/rest/"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
logger.debug("Initializing PayPalWallet")
|
||||||
|
self._settings_fields = self._settings_connection_fields()
|
||||||
|
if not settings.paypal_api_endpoint:
|
||||||
|
raise ValueError("Cannot initialize PayPalWallet: missing endpoint.")
|
||||||
|
if not settings.paypal_client_id:
|
||||||
|
raise ValueError("Cannot initialize PayPalWallet: missing client id.")
|
||||||
|
if not settings.paypal_client_secret:
|
||||||
|
raise ValueError("Cannot initialize PayPalWallet: missing client secret.")
|
||||||
|
|
||||||
|
self.endpoint = normalize_endpoint(settings.paypal_api_endpoint)
|
||||||
|
self.headers = {
|
||||||
|
"User-Agent": f"PayPal Alan:{settings.version}",
|
||||||
|
}
|
||||||
|
self._access_token: str | None = None
|
||||||
|
self._token_expires_at: float = 0
|
||||||
|
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
||||||
|
logger.info("PayPalWallet initialized.")
|
||||||
|
|
||||||
|
async def cleanup(self):
|
||||||
|
try:
|
||||||
|
await self.client.aclose()
|
||||||
|
except RuntimeError as e:
|
||||||
|
logger.warning(f"Error closing PayPal wallet connection: {e}")
|
||||||
|
|
||||||
|
async def status(
|
||||||
|
self, only_check_settings: bool | None = False
|
||||||
|
) -> FiatStatusResponse:
|
||||||
|
if only_check_settings:
|
||||||
|
if self._settings_fields != self._settings_connection_fields():
|
||||||
|
return FiatStatusResponse("Connection settings have changed.", 0)
|
||||||
|
return FiatStatusResponse(balance=0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self._ensure_access_token()
|
||||||
|
return FiatStatusResponse(balance=0)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return FiatStatusResponse(f"Unable to connect to {self.endpoint}.", 0)
|
||||||
|
|
||||||
|
async def create_invoice(
|
||||||
|
self,
|
||||||
|
amount: float,
|
||||||
|
payment_hash: str,
|
||||||
|
currency: str,
|
||||||
|
memo: str | None = None,
|
||||||
|
extra: dict[str, Any] | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> FiatInvoiceResponse:
|
||||||
|
opts = self._parse_create_opts(extra or {})
|
||||||
|
if opts is None:
|
||||||
|
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self._ensure_access_token()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=False, error_message="Unable to authenticate."
|
||||||
|
)
|
||||||
|
|
||||||
|
co = opts.checkout or PayPalCheckoutOptions()
|
||||||
|
success_url = (
|
||||||
|
co.success_url
|
||||||
|
or settings.paypal_payment_success_url
|
||||||
|
or "https://lnbits.com"
|
||||||
|
)
|
||||||
|
cancel_url = co.cancel_url or success_url
|
||||||
|
|
||||||
|
order_data = {
|
||||||
|
"intent": "CAPTURE",
|
||||||
|
"purchase_units": [
|
||||||
|
{
|
||||||
|
"amount": {
|
||||||
|
"currency_code": currency.upper(),
|
||||||
|
"value": f"{amount:.2f}",
|
||||||
|
},
|
||||||
|
"custom_id": payment_hash[:127], # PayPal limit
|
||||||
|
"invoice_id": payment_hash[:127],
|
||||||
|
"description": memo or "LNbits Invoice",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"application_context": {
|
||||||
|
"return_url": success_url,
|
||||||
|
"cancel_url": cancel_url,
|
||||||
|
"shipping_preference": "NO_SHIPPING",
|
||||||
|
"user_action": "PAY_NOW",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = await self.client.post(
|
||||||
|
"/v2/checkout/orders", json=order_data, headers=self._auth_headers()
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
order_id = data.get("id")
|
||||||
|
approval_url = self._get_approval_url(data.get("links") or [])
|
||||||
|
if not order_id or not approval_url:
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=False, error_message="Server error: missing id or approval url"
|
||||||
|
)
|
||||||
|
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=f"fiat_paypal_{order_id}",
|
||||||
|
payment_request=approval_url,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def create_subscription(
|
||||||
|
self,
|
||||||
|
subscription_id: str,
|
||||||
|
quantity: int,
|
||||||
|
payment_options: FiatSubscriptionPaymentOptions,
|
||||||
|
**kwargs,
|
||||||
|
) -> FiatSubscriptionResponse:
|
||||||
|
success_url = (
|
||||||
|
payment_options.success_url
|
||||||
|
or settings.paypal_payment_success_url
|
||||||
|
or "https://lnbits.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not payment_options.subscription_request_id:
|
||||||
|
payment_options.subscription_request_id = urlsafe_short_hash()
|
||||||
|
payment_options.extra = payment_options.extra or {}
|
||||||
|
payment_options.extra["subscription_request_id"] = (
|
||||||
|
payment_options.subscription_request_id
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self._ensure_access_token()
|
||||||
|
payload = {
|
||||||
|
"plan_id": subscription_id,
|
||||||
|
"quantity": str(quantity),
|
||||||
|
"custom_id": self._serialize_metadata(payment_options),
|
||||||
|
"application_context": {
|
||||||
|
"return_url": success_url,
|
||||||
|
"cancel_url": success_url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r = await self.client.post(
|
||||||
|
"/v1/billing/subscriptions",
|
||||||
|
json=payload,
|
||||||
|
headers=self._auth_headers(),
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
approval_url = self._get_approval_url(data.get("links") or [])
|
||||||
|
if not approval_url:
|
||||||
|
return FiatSubscriptionResponse(
|
||||||
|
ok=False, error_message="Server error: missing approval url"
|
||||||
|
)
|
||||||
|
|
||||||
|
return FiatSubscriptionResponse(
|
||||||
|
ok=True,
|
||||||
|
checkout_session_url=approval_url,
|
||||||
|
subscription_request_id=payment_options.subscription_request_id,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return FiatSubscriptionResponse(
|
||||||
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def cancel_subscription(
|
||||||
|
self,
|
||||||
|
subscription_id: str,
|
||||||
|
correlation_id: str,
|
||||||
|
**kwargs,
|
||||||
|
) -> FiatSubscriptionResponse:
|
||||||
|
try:
|
||||||
|
await self._ensure_access_token()
|
||||||
|
r = await self.client.post(
|
||||||
|
f"/v1/billing/subscriptions/{subscription_id}/cancel",
|
||||||
|
json={"reason": f"Cancelled by {correlation_id}"},
|
||||||
|
headers=self._auth_headers(),
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
return FiatSubscriptionResponse(ok=True)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return FiatSubscriptionResponse(
|
||||||
|
ok=False, error_message="Unable to cancel subscription."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
|
||||||
|
raise NotImplementedError("PayPal does not support paying invoices directly.")
|
||||||
|
|
||||||
|
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||||
|
try:
|
||||||
|
await self._ensure_access_token()
|
||||||
|
order_id = self._normalize_paypal_id(checking_id)
|
||||||
|
r = await self.client.get(
|
||||||
|
f"/v2/checkout/orders/{order_id}", headers=self._auth_headers()
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
return self._status_from_order(r.json())
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(f"Error getting PayPal order status: {exc}")
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
async def get_payment_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||||
|
raise NotImplementedError("PayPal does not support outgoing payments.")
|
||||||
|
|
||||||
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
logger.warning(
|
||||||
|
"PayPal does not support paid invoices stream. Use webhooks instead."
|
||||||
|
)
|
||||||
|
mock_queue: asyncio.Queue[str] = asyncio.Queue(0)
|
||||||
|
while settings.lnbits_running:
|
||||||
|
value = await mock_queue.get()
|
||||||
|
yield value
|
||||||
|
|
||||||
|
def _status_from_order(self, order: dict[str, Any]) -> FiatPaymentStatus:
|
||||||
|
status = (order.get("status") or "").upper()
|
||||||
|
if status in ["COMPLETED", "APPROVED"]:
|
||||||
|
return FiatPaymentSuccessStatus()
|
||||||
|
if status in ["VOIDED", "CANCELLED", "CANCELED"]:
|
||||||
|
return FiatPaymentFailedStatus()
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
def _normalize_paypal_id(self, checking_id: str) -> str:
|
||||||
|
return (
|
||||||
|
checking_id.replace("fiat_paypal_", "", 1)
|
||||||
|
if checking_id.startswith("fiat_paypal_")
|
||||||
|
else checking_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def _serialize_metadata(
|
||||||
|
self, payment_options: FiatSubscriptionPaymentOptions
|
||||||
|
) -> str:
|
||||||
|
md = {
|
||||||
|
"wallet_id": payment_options.wallet_id,
|
||||||
|
"memo": payment_options.memo,
|
||||||
|
"extra": payment_options.extra,
|
||||||
|
"tag": payment_options.tag,
|
||||||
|
"subscription_request_id": payment_options.subscription_request_id,
|
||||||
|
}
|
||||||
|
raw = json.dumps(md)
|
||||||
|
return raw[:127] # PayPal custom_id limit
|
||||||
|
|
||||||
|
def _parse_create_opts(
|
||||||
|
self, raw_opts: dict[str, Any]
|
||||||
|
) -> PayPalCreateInvoiceOptions | None:
|
||||||
|
try:
|
||||||
|
return PayPalCreateInvoiceOptions.parse_obj(raw_opts)
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.warning(f"Invalid PayPal options: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _ensure_access_token(self):
|
||||||
|
if self._access_token and time.time() < self._token_expires_at:
|
||||||
|
return
|
||||||
|
|
||||||
|
r = await self.client.post(
|
||||||
|
"/v1/oauth2/token",
|
||||||
|
data={"grant_type": "client_credentials"},
|
||||||
|
auth=(settings.paypal_client_id or "", settings.paypal_client_secret or ""),
|
||||||
|
headers={"Accept": "application/json"},
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
token = data.get("access_token")
|
||||||
|
expires_in = int(data.get("expires_in") or 300)
|
||||||
|
if not token:
|
||||||
|
raise ValueError("Unable to retrieve PayPal access token.")
|
||||||
|
self._access_token = token
|
||||||
|
self._token_expires_at = time.time() + expires_in - 30
|
||||||
|
|
||||||
|
def _auth_headers(self) -> dict[str, str]:
|
||||||
|
return {**self.headers, "Authorization": f"Bearer {self._access_token}"}
|
||||||
|
|
||||||
|
def _get_approval_url(self, links: list[dict[str, Any]]) -> str | None:
|
||||||
|
for link in links:
|
||||||
|
if link.get("rel") == "approve":
|
||||||
|
return link.get("href")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _settings_connection_fields(self) -> str:
|
||||||
|
return "-".join(
|
||||||
|
[
|
||||||
|
str(settings.paypal_api_endpoint),
|
||||||
|
str(settings.paypal_client_id),
|
||||||
|
str(settings.paypal_client_secret),
|
||||||
|
str(settings.paypal_webhook_id),
|
||||||
|
]
|
||||||
|
)
|
||||||
+23
-2
@@ -35,6 +35,7 @@ class StripeTerminalOptions(BaseModel):
|
|||||||
|
|
||||||
capture_method: Literal["automatic", "manual"] = "automatic"
|
capture_method: Literal["automatic", "manual"] = "automatic"
|
||||||
metadata: dict[str, str] = Field(default_factory=dict)
|
metadata: dict[str, str] = Field(default_factory=dict)
|
||||||
|
reader_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class StripeCheckoutOptions(BaseModel):
|
class StripeCheckoutOptions(BaseModel):
|
||||||
@@ -170,7 +171,7 @@ class StripeWallet(FiatProvider):
|
|||||||
("line_items[0][price]", subscription_id),
|
("line_items[0][price]", subscription_id),
|
||||||
("line_items[0][quantity]", f"{quantity}"),
|
("line_items[0][quantity]", f"{quantity}"),
|
||||||
]
|
]
|
||||||
subscription_data = {**payment_options.dict(), "lnbits_action": "subscription"}
|
subscription_data = {**payment_options.dict(), "alan_action": "subscription"}
|
||||||
subscription_data["extra"] = json.dumps(subscription_data.get("extra") or {})
|
subscription_data["extra"] = json.dumps(subscription_data.get("extra") or {})
|
||||||
|
|
||||||
form_data += self._encode_metadata(
|
form_data += self._encode_metadata(
|
||||||
@@ -295,6 +296,15 @@ class StripeWallet(FiatProvider):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
async def _process_terminal_payment_intent(
|
||||||
|
self, reader_id: str, payment_intent_id: str
|
||||||
|
) -> None:
|
||||||
|
data = {"payment_intent": payment_intent_id}
|
||||||
|
r = await self.client.post(
|
||||||
|
f"/v1/terminal/readers/{reader_id}/process_payment_intent", data=data
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
async def _create_checkout_invoice(
|
async def _create_checkout_invoice(
|
||||||
self,
|
self,
|
||||||
amount_cents: int,
|
amount_cents: int,
|
||||||
@@ -315,7 +325,7 @@ class StripeWallet(FiatProvider):
|
|||||||
("mode", "payment"),
|
("mode", "payment"),
|
||||||
("success_url", success_url),
|
("success_url", success_url),
|
||||||
("metadata[payment_hash]", payment_hash),
|
("metadata[payment_hash]", payment_hash),
|
||||||
("metadata[lnbits_action]", "invoice"),
|
("metadata[alan_action]", "invoice"),
|
||||||
("line_items[0][price_data][currency]", currency.lower()),
|
("line_items[0][price_data][currency]", currency.lower()),
|
||||||
("line_items[0][price_data][product_data][name]", line_item_name),
|
("line_items[0][price_data][product_data][name]", line_item_name),
|
||||||
("line_items[0][price_data][unit_amount]", str(amount_cents)),
|
("line_items[0][price_data][unit_amount]", str(amount_cents)),
|
||||||
@@ -378,6 +388,17 @@ class StripeWallet(FiatProvider):
|
|||||||
ok=False,
|
ok=False,
|
||||||
error_message="Error: missing PaymentIntent or client_secret",
|
error_message="Error: missing PaymentIntent or client_secret",
|
||||||
)
|
)
|
||||||
|
if term.reader_id:
|
||||||
|
try:
|
||||||
|
await self._process_terminal_payment_intent(term.reader_id, pi_id)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=False,
|
||||||
|
error_message=(
|
||||||
|
"Error: unable to process PaymentIntent on reader"
|
||||||
|
),
|
||||||
|
)
|
||||||
return FiatInvoiceResponse(
|
return FiatInvoiceResponse(
|
||||||
ok=True, checking_id=pi_id, payment_request=client_secret
|
ok=True, checking_id=pi_id, payment_request=client_secret
|
||||||
)
|
)
|
||||||
|
|||||||
+29
-17
@@ -18,6 +18,7 @@ from pydantic.schema import field_schema
|
|||||||
from lnbits.jinja2_templating import Jinja2Templates
|
from lnbits.jinja2_templating import Jinja2Templates
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import AESCipher
|
||||||
|
from lnbits.utils.exchange_rates import currencies
|
||||||
|
|
||||||
from .db import FilterModel
|
from .db import FilterModel
|
||||||
|
|
||||||
@@ -67,9 +68,10 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
folders.extend(additional_folders)
|
folders.extend(additional_folders)
|
||||||
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
||||||
t.env.globals["static_url_for"] = static_url_for
|
t.env.globals["static_url_for"] = static_url_for
|
||||||
|
t.env.globals["normalize_path"] = normalize_path
|
||||||
|
|
||||||
window_settings = {
|
window_settings = {
|
||||||
"AD_SPACE": settings.lnbits_ad_space.split(","),
|
"AD_SPACE": settings.lnbits_ad_space,
|
||||||
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
||||||
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
||||||
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
||||||
@@ -90,6 +92,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
||||||
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
||||||
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
||||||
|
"LNBITS_APPLE_TOUCH_ICON": settings.lnbits_apple_touch_icon,
|
||||||
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
||||||
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
||||||
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
||||||
@@ -97,15 +100,22 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
||||||
"LNBITS_VERSION": settings.version,
|
"LNBITS_VERSION": settings.version,
|
||||||
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
||||||
"USE_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
||||||
"USE_DEFAULT_THEME": settings.lnbits_default_theme,
|
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
|
||||||
"USE_DEFAULT_BORDER": settings.lnbits_default_border,
|
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
|
||||||
"USE_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
||||||
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||||
|
"LNBITS_EXTENSIONS_REVIEWS_URL": settings.lnbits_extensions_reviews_url,
|
||||||
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
||||||
"has_holdinvoice": settings.has_holdinvoice,
|
"has_holdinvoice": settings.has_holdinvoice,
|
||||||
|
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
||||||
|
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
|
||||||
|
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
|
||||||
|
"LNBITS_CURRENCIES": list(currencies.keys()),
|
||||||
|
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
|
||||||
|
"CACHE_KEY": settings.server_startup_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||||
@@ -198,6 +208,11 @@ def is_valid_username(username: str) -> bool:
|
|||||||
return re.fullmatch(username_regex, username) is not None
|
return re.fullmatch(username_regex, username) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_label(label: str) -> bool:
|
||||||
|
label_regex = r"([A-Za-z0-9 ._-]{1,100}$)"
|
||||||
|
return re.fullmatch(label_regex, label) is not None
|
||||||
|
|
||||||
|
|
||||||
def is_valid_external_id(external_id: str) -> bool:
|
def is_valid_external_id(external_id: str) -> bool:
|
||||||
if len(external_id) > 256:
|
if len(external_id) > 256:
|
||||||
return False
|
return False
|
||||||
@@ -353,17 +368,6 @@ def normalize_path(path: str | None) -> str:
|
|||||||
return "/" + "/".join(path_segments(path))
|
return "/" + "/".join(path_segments(path))
|
||||||
|
|
||||||
|
|
||||||
def safe_upload_file_path(filename: str, directory: str = "images") -> Path:
|
|
||||||
image_folder = Path(settings.lnbits_data_folder, directory)
|
|
||||||
file_path = image_folder / filename
|
|
||||||
# Prevent dir traversal attack
|
|
||||||
if image_folder.resolve() not in file_path.resolve().parents:
|
|
||||||
raise ValueError("Unsafe filename.")
|
|
||||||
# Prevent filename with subdirectories
|
|
||||||
file_path = image_folder / filename.split("/")[-1]
|
|
||||||
return file_path.resolve()
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_endpoint(endpoint: str, add_proto=True) -> str:
|
def normalize_endpoint(endpoint: str, add_proto=True) -> str:
|
||||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||||
if add_proto:
|
if add_proto:
|
||||||
@@ -397,3 +401,11 @@ def is_snake_case(v: str) -> bool:
|
|||||||
|
|
||||||
def lowercase_first_letter(s: str) -> str:
|
def lowercase_first_letter(s: str) -> str:
|
||||||
return s[:1].lower() + s[1:] if s else s
|
return s[:1].lower() + s[1:] if s else s
|
||||||
|
|
||||||
|
|
||||||
|
def sha256s(value: str) -> str:
|
||||||
|
"""
|
||||||
|
SHA256 applied on a string value.
|
||||||
|
Returns the hex as a string.
|
||||||
|
"""
|
||||||
|
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
||||||
|
|||||||
+51
-7
@@ -52,6 +52,13 @@ class ExtensionsSettings(LNbitsSettings):
|
|||||||
lnbits_user_default_extensions: list[str] = Field(default=[])
|
lnbits_user_default_extensions: list[str] = Field(default=[])
|
||||||
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
||||||
lnbits_extensions_builder_activate_non_admins: bool = Field(default=False)
|
lnbits_extensions_builder_activate_non_admins: bool = Field(default=False)
|
||||||
|
lnbits_extensions_reviews_url: str = Field(
|
||||||
|
default="https://demo.lnbits.com/paidreviews/api/v1/AdFzLjzuKFLsdk4Bcnff6r",
|
||||||
|
description="""
|
||||||
|
URL for the paid reviews.
|
||||||
|
Regular users can view this URL (not secret).
|
||||||
|
""",
|
||||||
|
)
|
||||||
lnbits_extensions_manifests: list[str] = Field(
|
lnbits_extensions_manifests: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
"https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json"
|
"https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json"
|
||||||
@@ -270,6 +277,7 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
lnbits_allowed_currencies: list[str] = Field(default=[])
|
lnbits_allowed_currencies: list[str] = Field(default=[])
|
||||||
lnbits_default_accounting_currency: str | None = Field(default=None)
|
lnbits_default_accounting_currency: str | None = Field(default=None)
|
||||||
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
|
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
|
||||||
|
lnbits_apple_touch_icon: str | None = Field(default=None)
|
||||||
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
||||||
lnbits_default_theme: str = Field(default="salvador")
|
lnbits_default_theme: str = Field(default="salvador")
|
||||||
lnbits_default_border: str = Field(default="hard-border")
|
lnbits_default_border: str = Field(default="hard-border")
|
||||||
@@ -280,8 +288,11 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
class OpsSettings(LNbitsSettings):
|
class OpsSettings(LNbitsSettings):
|
||||||
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
||||||
lnbits_hide_api: bool = Field(default=False)
|
lnbits_hide_api: bool = Field(default=False)
|
||||||
lnbits_upload_size_bytes: int = Field(default=512_000, ge=0) # 500kb
|
|
||||||
lnbits_upload_allowed_types: list[str] = Field(
|
|
||||||
|
class AssetSettings(LNbitsSettings):
|
||||||
|
lnbits_max_asset_size_mb: float = Field(default=2.5, ge=0.0)
|
||||||
|
lnbits_assets_allowed_mime_types: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
"image/png",
|
"image/png",
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
@@ -297,6 +308,18 @@ class OpsSettings(LNbitsSettings):
|
|||||||
"heics",
|
"heics",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
lnbits_asset_thumbnail_width: int = Field(default=128, ge=0)
|
||||||
|
lnbits_asset_thumbnail_height: int = Field(default=128, ge=0)
|
||||||
|
lnbits_asset_thumbnail_format: str = Field(default="png")
|
||||||
|
|
||||||
|
lnbits_max_assets_per_user: int = Field(default=1, ge=0)
|
||||||
|
lnbits_assets_no_limit_users: list[str] = Field(default=[])
|
||||||
|
|
||||||
|
def is_unlimited_assets_user(self, user_id: str) -> bool:
|
||||||
|
return (
|
||||||
|
settings.is_admin_user(user_id)
|
||||||
|
or user_id in self.lnbits_assets_no_limit_users
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FeeSettings(LNbitsSettings):
|
class FeeSettings(LNbitsSettings):
|
||||||
@@ -429,7 +452,7 @@ class NotificationsSettings(LNbitsSettings):
|
|||||||
|
|
||||||
lnbits_notification_settings_update: bool = Field(default=True)
|
lnbits_notification_settings_update: bool = Field(default=True)
|
||||||
lnbits_notification_credit_debit: bool = Field(default=True)
|
lnbits_notification_credit_debit: bool = Field(default=True)
|
||||||
notification_balance_delta_changed: bool = Field(default=True)
|
notification_balance_delta_threshold_sats: int = Field(default=1, ge=0)
|
||||||
lnbits_notification_server_start_stop: bool = Field(default=True)
|
lnbits_notification_server_start_stop: bool = Field(default=True)
|
||||||
lnbits_notification_watchdog: bool = Field(default=False)
|
lnbits_notification_watchdog: bool = Field(default=False)
|
||||||
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
|
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
|
||||||
@@ -591,7 +614,6 @@ class BreezLiquidSdkFundingSource(LNbitsSettings):
|
|||||||
class BoltzFundingSource(LNbitsSettings):
|
class BoltzFundingSource(LNbitsSettings):
|
||||||
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
|
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
|
||||||
boltz_client_macaroon: str | None = Field(default=None)
|
boltz_client_macaroon: str | None = Field(default=None)
|
||||||
boltz_client_wallet: str | None = Field(default="lnbits")
|
|
||||||
boltz_client_password: str = Field(default="")
|
boltz_client_password: str = Field(default="")
|
||||||
boltz_client_cert: str | None = Field(default=None)
|
boltz_client_cert: str | None = Field(default=None)
|
||||||
boltz_mnemonic: str | None = Field(default=None)
|
boltz_mnemonic: str | None = Field(default=None)
|
||||||
@@ -632,6 +654,20 @@ class StripeFiatProvider(LNbitsSettings):
|
|||||||
stripe_limits: FiatProviderLimits = Field(default_factory=FiatProviderLimits)
|
stripe_limits: FiatProviderLimits = Field(default_factory=FiatProviderLimits)
|
||||||
|
|
||||||
|
|
||||||
|
class PayPalFiatProvider(LNbitsSettings):
|
||||||
|
paypal_enabled: bool = Field(default=False)
|
||||||
|
paypal_api_endpoint: str = Field(default="https://api-m.paypal.com")
|
||||||
|
paypal_client_id: str | None = Field(default=None)
|
||||||
|
paypal_client_secret: str | None = Field(default=None)
|
||||||
|
paypal_payment_success_url: str = Field(default="https://lnbits.com")
|
||||||
|
paypal_payment_webhook_url: str = Field(
|
||||||
|
default="https://your-lnbits-domain-here.com/api/v1/callback/paypal"
|
||||||
|
)
|
||||||
|
paypal_webhook_id: str | None = Field(default=None)
|
||||||
|
|
||||||
|
paypal_limits: FiatProviderLimits = Field(default_factory=FiatProviderLimits)
|
||||||
|
|
||||||
|
|
||||||
class LightningSettings(LNbitsSettings):
|
class LightningSettings(LNbitsSettings):
|
||||||
lightning_invoice_expiry: int = Field(default=3600, gt=0)
|
lightning_invoice_expiry: int = Field(default=3600, gt=0)
|
||||||
|
|
||||||
@@ -667,7 +703,7 @@ class FundingSourcesSettings(
|
|||||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
|
|
||||||
|
|
||||||
class FiatProvidersSettings(StripeFiatProvider):
|
class FiatProvidersSettings(StripeFiatProvider, PayPalFiatProvider):
|
||||||
def is_fiat_provider_enabled(self, provider: str | None) -> bool:
|
def is_fiat_provider_enabled(self, provider: str | None) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if a specific fiat provider is enabled.
|
Checks if a specific fiat provider is enabled.
|
||||||
@@ -676,7 +712,8 @@ class FiatProvidersSettings(StripeFiatProvider):
|
|||||||
return False
|
return False
|
||||||
if provider == "stripe":
|
if provider == "stripe":
|
||||||
return self.stripe_enabled
|
return self.stripe_enabled
|
||||||
# Add checks for other fiat providers here as needed
|
if provider == "paypal":
|
||||||
|
return self.paypal_enabled
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_fiat_providers_for_user(self, user_id: str) -> list[str]:
|
def get_fiat_providers_for_user(self, user_id: str) -> list[str]:
|
||||||
@@ -690,7 +727,12 @@ class FiatProvidersSettings(StripeFiatProvider):
|
|||||||
):
|
):
|
||||||
allowed_providers.append("stripe")
|
allowed_providers.append("stripe")
|
||||||
|
|
||||||
# Add other fiat providers here as needed
|
if self.paypal_enabled and (
|
||||||
|
not self.paypal_limits.allowed_users
|
||||||
|
or user_id in self.paypal_limits.allowed_users
|
||||||
|
):
|
||||||
|
allowed_providers.append("paypal")
|
||||||
|
|
||||||
return allowed_providers
|
return allowed_providers
|
||||||
|
|
||||||
def get_fiat_provider_limits(self, provider_name: str) -> FiatProviderLimits | None:
|
def get_fiat_provider_limits(self, provider_name: str) -> FiatProviderLimits | None:
|
||||||
@@ -747,6 +789,7 @@ class AuthSettings(LNbitsSettings):
|
|||||||
# How many seconds after login the user is allowed to update its credentials.
|
# How many seconds after login the user is allowed to update its credentials.
|
||||||
# A fresh login is required afterwards.
|
# A fresh login is required afterwards.
|
||||||
auth_credetials_update_threshold: int = Field(default=120, gt=0)
|
auth_credetials_update_threshold: int = Field(default=120, gt=0)
|
||||||
|
auth_authentication_cache_minutes: int = Field(default=10, ge=0)
|
||||||
|
|
||||||
def is_auth_method_allowed(self, method: AuthMethods):
|
def is_auth_method_allowed(self, method: AuthMethods):
|
||||||
return method.value in self.auth_allowed_methods
|
return method.value in self.auth_allowed_methods
|
||||||
@@ -867,6 +910,7 @@ class EditableSettings(
|
|||||||
ExtensionsSettings,
|
ExtensionsSettings,
|
||||||
ThemesSettings,
|
ThemesSettings,
|
||||||
OpsSettings,
|
OpsSettings,
|
||||||
|
AssetSettings,
|
||||||
FeeSettings,
|
FeeSettings,
|
||||||
ExchangeProvidersSettings,
|
ExchangeProvidersSettings,
|
||||||
SecuritySettings,
|
SecuritySettings,
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+17
-17
File diff suppressed because one or more lines are too long
+124
-1
@@ -185,6 +185,11 @@ body[data-theme=salvador] [data-theme=salvador] .q-stepper--dark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.gradient-bg {
|
body.gradient-bg {
|
||||||
|
background-image: linear-gradient(to bottom right, #fff, var(--q-primary));
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.gradient-bg.body--dark {
|
||||||
background-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
|
background-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
@@ -212,7 +217,54 @@ body.body--dark .q-header,
|
|||||||
body.body--dark .q-drawer {
|
body.body--dark .q-drawer {
|
||||||
--q-dark: rgba(29, 29, 29, 0.3);
|
--q-dark: rgba(29, 29, 29, 0.3);
|
||||||
background-color: var(--q-dark);
|
background-color: var(--q-dark);
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px) brightness(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--size: 100px;
|
||||||
|
--gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home .btn-fixed-width {
|
||||||
|
/* width: 45%; */
|
||||||
|
}
|
||||||
|
.home .wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--gap);
|
||||||
|
margin: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.home .marquee {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
gap: var(--gap);
|
||||||
|
height: max-content;
|
||||||
|
mask-image: linear-gradient(to right, hsla(0, 0%, 0%, 0), hsl(0, 0%, 0%) 20%, hsl(0, 0%, 0%) 80%, hsla(0, 0%, 0%, 0));
|
||||||
|
}
|
||||||
|
.home .marquee__group {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
gap: var(--gap);
|
||||||
|
min-width: 100%;
|
||||||
|
animation: scroll-x 60s linear infinite;
|
||||||
|
}
|
||||||
|
.home .marquee:hover .marquee__group {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
.home .marquee__group div {
|
||||||
|
width: var(--size);
|
||||||
|
}
|
||||||
|
@keyframes scroll-x {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(calc(-100% - var(--gap)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
@@ -341,3 +393,74 @@ video {
|
|||||||
.q-dialog__inner--minimized {
|
.q-dialog__inner--minimized {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.first-install {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.first-install .grid {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.first-install .hero-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.first-install .hero {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 250px;
|
||||||
|
background-image: url(/static/images/logos/lnbits.svg);
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.first-install .grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 1rem;
|
||||||
|
}
|
||||||
|
.first-install .hero-wrapper {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-list-card {
|
||||||
|
margin-top: 1px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-list-card:first-child {
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.wallet-card {
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.mobile-simple .wallet-wrapper {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 50% !important;
|
||||||
|
left: 50% !important;
|
||||||
|
transform: translate(-50%, -50%) !important;
|
||||||
|
}
|
||||||
|
.mobile-simple .wallet-card {
|
||||||
|
width: 90% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.error-code {
|
||||||
|
font-size: clamp(15vh, 20vw, 30vh);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
font-size: clamp(1.5rem, 3vw, 3.75rem);
|
||||||
|
font-weight: 300;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,12 +25,14 @@ window.localisation.en = {
|
|||||||
reconnect: 'Reconnect',
|
reconnect: 'Reconnect',
|
||||||
open_channel: 'Open Channel',
|
open_channel: 'Open Channel',
|
||||||
open: 'Open',
|
open: 'Open',
|
||||||
|
clear: 'Clear',
|
||||||
close_channel: 'Close Channel',
|
close_channel: 'Close Channel',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
restart: 'Restart server',
|
restart: 'Restart server',
|
||||||
image_library: 'Image Library',
|
image_library: 'Image Library',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
save_tooltip: 'Save your changes',
|
save_tooltip: 'Save your changes',
|
||||||
|
must_save: 'You have unsaved changes',
|
||||||
credit_debit: 'Credit / Debit',
|
credit_debit: 'Credit / Debit',
|
||||||
credit_hint: 'Press Enter to credit/debit wallet (negative values allowed)',
|
credit_hint: 'Press Enter to credit/debit wallet (negative values allowed)',
|
||||||
credit_label: '{denomination} to credit/debit',
|
credit_label: '{denomination} to credit/debit',
|
||||||
@@ -52,9 +54,17 @@ window.localisation.en = {
|
|||||||
stored_paylinks: 'Stored LNURL pay links',
|
stored_paylinks: 'Stored LNURL pay links',
|
||||||
wallet: 'Wallet: ',
|
wallet: 'Wallet: ',
|
||||||
wallet_name: 'Wallet name',
|
wallet_name: 'Wallet name',
|
||||||
|
wallet_type: 'Wallet type',
|
||||||
|
shared_wallet: 'Shared Wallet',
|
||||||
|
share_wallet: 'Share Wallet',
|
||||||
|
update_permissions: 'Update Permissions',
|
||||||
|
shared_wallet_id: 'Shared Wallet ID',
|
||||||
|
shared_wallet_desc:
|
||||||
|
"You have been invited to have access to someone else's wallet.",
|
||||||
wallets: 'Wallets',
|
wallets: 'Wallets',
|
||||||
exclude_wallets: 'Exclude Wallets',
|
exclude_wallets: 'Exclude Wallets',
|
||||||
add_wallet: 'Add wallet',
|
add_wallet: 'Add wallet',
|
||||||
|
reject_wallet: 'Reject wallet',
|
||||||
add_new_wallet: 'Add a new wallet',
|
add_new_wallet: 'Add a new wallet',
|
||||||
pin_wallet: 'Pin wallet',
|
pin_wallet: 'Pin wallet',
|
||||||
delete_wallet: 'Delete wallet',
|
delete_wallet: 'Delete wallet',
|
||||||
@@ -64,6 +74,8 @@ window.localisation.en = {
|
|||||||
update_name: 'Update name',
|
update_name: 'Update name',
|
||||||
fiat_tracking: 'Fiat tracking',
|
fiat_tracking: 'Fiat tracking',
|
||||||
fiat_providers: 'Fiat providers',
|
fiat_providers: 'Fiat providers',
|
||||||
|
fiat_warning_bitcoin:
|
||||||
|
'Fiat providers can get twitchy about anything bitcoin, so avoid using word "bitcoin" in your memos!',
|
||||||
currency: 'Currency',
|
currency: 'Currency',
|
||||||
update_currency: 'Update currency',
|
update_currency: 'Update currency',
|
||||||
press_to_claim: 'Press to claim bitcoin',
|
press_to_claim: 'Press to claim bitcoin',
|
||||||
@@ -72,6 +84,10 @@ window.localisation.en = {
|
|||||||
donate: 'Donate',
|
donate: 'Donate',
|
||||||
view_github: 'View on GitHub',
|
view_github: 'View on GitHub',
|
||||||
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
||||||
|
voidwallet_active_user:
|
||||||
|
'Funding source unavailable. Please contact your admin to configure.',
|
||||||
|
voidwallet_active_admin:
|
||||||
|
'Funding source unavailable. Click here to configure.',
|
||||||
service_fee_badge: 'Service fee: {amount} % per transaction',
|
service_fee_badge: 'Service fee: {amount} % per transaction',
|
||||||
service_fee_max_badge:
|
service_fee_max_badge:
|
||||||
'Service fee: {amount} % per transaction (max {max} {denom})',
|
'Service fee: {amount} % per transaction (max {max} {denom})',
|
||||||
@@ -137,6 +153,8 @@ window.localisation.en = {
|
|||||||
extensions: 'Extensions',
|
extensions: 'Extensions',
|
||||||
no_extensions: "You don't have any extensions installed :(",
|
no_extensions: "You don't have any extensions installed :(",
|
||||||
created: 'Created',
|
created: 'Created',
|
||||||
|
created_at: 'Created At',
|
||||||
|
updated_at: 'Updated At',
|
||||||
search_extensions: 'Search extensions',
|
search_extensions: 'Search extensions',
|
||||||
search_wallets: 'Search wallets',
|
search_wallets: 'Search wallets',
|
||||||
extension_sources: 'Extension Sources',
|
extension_sources: 'Extension Sources',
|
||||||
@@ -147,6 +165,7 @@ window.localisation.en = {
|
|||||||
repository: 'Repository',
|
repository: 'Repository',
|
||||||
confirm_continue: 'Are you sure you want to continue?',
|
confirm_continue: 'Are you sure you want to continue?',
|
||||||
manage_extension_details: 'Install/uninstall extension',
|
manage_extension_details: 'Install/uninstall extension',
|
||||||
|
upload: 'Upload',
|
||||||
install: 'Install',
|
install: 'Install',
|
||||||
uninstall: 'Uninstall',
|
uninstall: 'Uninstall',
|
||||||
drop_db: 'Remove Data',
|
drop_db: 'Remove Data',
|
||||||
@@ -168,6 +187,25 @@ window.localisation.en = {
|
|||||||
'Only admin accounts can create extensions',
|
'Only admin accounts can create extensions',
|
||||||
admin_only: 'Admin Only',
|
admin_only: 'Admin Only',
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
|
reviews_url: 'Reviews URL',
|
||||||
|
reviews_url_label: 'Reviews server URL',
|
||||||
|
reviews_url_hint:
|
||||||
|
'Full PaidReviews URL including the settings id (e.g. https://example.com/paidreviews/SETTINGS_ID)',
|
||||||
|
reviews_open: 'View reviews',
|
||||||
|
reviews_leave: 'Leave a review',
|
||||||
|
reviews_name: 'Your name',
|
||||||
|
reviews_comment: 'Your review',
|
||||||
|
reviews_rating: 'Rating',
|
||||||
|
reviews_submit: 'Submit review',
|
||||||
|
reviews_loading: 'Loading reviews...',
|
||||||
|
reviews_refresh: 'Refresh reviews',
|
||||||
|
reviews_error_load: 'Could not load reviews',
|
||||||
|
reviews_url_not_configured: 'Reviews URL not configured',
|
||||||
|
reviews_pay_invoice: 'Pay invoice',
|
||||||
|
reviews_invoice_paid: 'Invoice paid',
|
||||||
|
reviews_invoice_title: 'Pay this invoice to submit your review',
|
||||||
|
reviews_count: 'Reviews',
|
||||||
|
no_reviews: 'No reviews yet',
|
||||||
extension_has_free_release: 'Has free releases',
|
extension_has_free_release: 'Has free releases',
|
||||||
extension_has_paid_release: 'Has paid releases',
|
extension_has_paid_release: 'Has paid releases',
|
||||||
extension_depends_on: 'Depends on:',
|
extension_depends_on: 'Depends on:',
|
||||||
@@ -220,6 +258,7 @@ window.localisation.en = {
|
|||||||
webhook_url: 'Webhook URL',
|
webhook_url: 'Webhook URL',
|
||||||
webhook_url_hint:
|
webhook_url_hint:
|
||||||
'Webhook URL to send the payment details to. It will be called when the payment is completed.',
|
'Webhook URL to send the payment details to. It will be called when the payment is completed.',
|
||||||
|
copy_webhook_url: 'Copy webhook URL',
|
||||||
webhook_events_list: 'The following events must be supported by the webhook:',
|
webhook_events_list: 'The following events must be supported by the webhook:',
|
||||||
webhook_stripe_description:
|
webhook_stripe_description:
|
||||||
'One the stripe side you must configure a webhook with a URL that points to your LNbits server.',
|
'One the stripe side you must configure a webhook with a URL that points to your LNbits server.',
|
||||||
@@ -302,7 +341,7 @@ window.localisation.en = {
|
|||||||
|
|
||||||
notification_balance_delta_changed: 'Balance Delta Changed',
|
notification_balance_delta_changed: 'Balance Delta Changed',
|
||||||
notification_balance_delta_changed_desc:
|
notification_balance_delta_changed_desc:
|
||||||
'Notify when the diference between the node balance and the LNbits balance has changed even by 1 sat. This runs every minute.',
|
'Notify when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
|
||||||
|
|
||||||
enable_watchdog: 'Enable Watchdog Switch',
|
enable_watchdog: 'Enable Watchdog Switch',
|
||||||
enable_watchdog_desc:
|
enable_watchdog_desc:
|
||||||
@@ -395,6 +434,8 @@ window.localisation.en = {
|
|||||||
first_name: 'First Name',
|
first_name: 'First Name',
|
||||||
last_name: 'Last Name',
|
last_name: 'Last Name',
|
||||||
picture: 'Picture',
|
picture: 'Picture',
|
||||||
|
user_picture_desc:
|
||||||
|
'URL to an image to use as profile picture. You can upload it as an asset.',
|
||||||
verify_email: 'Verify email with',
|
verify_email: 'Verify email with',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
update_account: 'Update Account',
|
update_account: 'Update Account',
|
||||||
@@ -409,6 +450,7 @@ window.localisation.en = {
|
|||||||
look_and_feel: 'Look and Feel',
|
look_and_feel: 'Look and Feel',
|
||||||
endpoint: 'Endpoint',
|
endpoint: 'Endpoint',
|
||||||
api: 'API',
|
api: 'API',
|
||||||
|
api_stripe: 'API',
|
||||||
api_token: 'API Token',
|
api_token: 'API Token',
|
||||||
api_tokens: 'API Tokens',
|
api_tokens: 'API Tokens',
|
||||||
access_control_list: 'Access Control List',
|
access_control_list: 'Access Control List',
|
||||||
@@ -419,6 +461,26 @@ window.localisation.en = {
|
|||||||
toggle_gradient: 'Toggle Gradient',
|
toggle_gradient: 'Toggle Gradient',
|
||||||
gradient_background: 'Gradient Background',
|
gradient_background: 'Gradient Background',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
|
assets: 'Assets',
|
||||||
|
max_asset_size_mb: 'Max Asset Size (MB)',
|
||||||
|
max_asset_size_mb_desc:
|
||||||
|
'The maximum allowed size for asset uploads in megabytes (can use decimal values).',
|
||||||
|
assets_allowed_mime_types: 'Allowed MIME Types',
|
||||||
|
assets_allowed_mime_types_desc:
|
||||||
|
'The MIME types that are allowed for asset uploads. No value means all uploads are allowed.',
|
||||||
|
thumbnail_width: 'Thumbnail Width',
|
||||||
|
thumbnail_width_desc: 'Width of the generated thumbnail in pixels.',
|
||||||
|
thumbnail_height: 'Thumbnail Height',
|
||||||
|
thumbnail_height_desc: 'Height of the generated thumbnail in pixels.',
|
||||||
|
thumbnail_format: 'Thumbnail Format',
|
||||||
|
thumbnail_format_desc:
|
||||||
|
'Image format of the generated thumbnail (PNG, JPEG, etc.).',
|
||||||
|
max_assets_per_user: 'Max Assets Per User',
|
||||||
|
max_assets_per_user_desc:
|
||||||
|
'The maximum number of assets a user can upload. Zero means upload forbidden.',
|
||||||
|
assets_no_limit_users: 'Users Without Asset Limits',
|
||||||
|
assets_no_limit_users_desc:
|
||||||
|
'These users can upload an unlimited number of assets (user id based).',
|
||||||
color_scheme: 'Color Scheme',
|
color_scheme: 'Color Scheme',
|
||||||
visible_wallet_count: 'Visible Wallet Count',
|
visible_wallet_count: 'Visible Wallet Count',
|
||||||
admin_settings: 'Admin Settings',
|
admin_settings: 'Admin Settings',
|
||||||
@@ -526,13 +588,18 @@ window.localisation.en = {
|
|||||||
reserve_fee_in_percent: 'Reserve fee in percent',
|
reserve_fee_in_percent: 'Reserve fee in percent',
|
||||||
payment_wait_time: 'Payment Wait Time (sec)',
|
payment_wait_time: 'Payment Wait Time (sec)',
|
||||||
payment_wait_time_desc:
|
payment_wait_time_desc:
|
||||||
'How long to wait when making a payment before marking it as pending. Set higher values for HODL invoices, Boltz, etc.',
|
'Wait time before marking an outgoing payment as pending. Default: 5s; raise for slow-settling invoices.',
|
||||||
|
payment_wait_time_tooltip:
|
||||||
|
'Controls how long LNbits waits for an outgoing payment attempt to confirm before marking it as pending. Higher values help when paying slow-settling invoices (e.g., HODL invoices, Boltz). The payment will be rechecked later and updated automatically or manually.',
|
||||||
server_management: 'Server Management',
|
server_management: 'Server Management',
|
||||||
base_url: 'Base URL',
|
base_url: 'Base URL',
|
||||||
base_url_label: 'Static/Base url for the server',
|
base_url_label: 'Static/Base url for the server',
|
||||||
authentication: 'Authentication',
|
authentication: 'Authentication',
|
||||||
auth_token_expiry_label: 'Token expire minutes',
|
auth_token_expiry_label: 'Token expire minutes',
|
||||||
auth_token_expiry_hint: 'Time in minutes until the token expires',
|
auth_token_expiry_hint: 'Time in minutes until the token expires',
|
||||||
|
auth_authentication_cache_label: 'Cache time (minutes)',
|
||||||
|
auth_authentication_cache_hint:
|
||||||
|
'Time in minutes to cache successful authentication (0 to disable)',
|
||||||
auth_allowed_methods_label: 'Allowed authorization methods',
|
auth_allowed_methods_label: 'Allowed authorization methods',
|
||||||
auth_allowed_methods_hint: 'Select authorization methods',
|
auth_allowed_methods_hint: 'Select authorization methods',
|
||||||
auth_nostr_label: 'Nostr Request URL',
|
auth_nostr_label: 'Nostr Request URL',
|
||||||
@@ -597,6 +664,8 @@ window.localisation.en = {
|
|||||||
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
||||||
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
||||||
ui_qr_code_logo_hint: 'QR code and favicon logo url',
|
ui_qr_code_logo_hint: 'QR code and favicon logo url',
|
||||||
|
ui_apple_touch_icon: 'Apple Touch Icon',
|
||||||
|
ui_apple_touch_icon_hint: 'Apple touch icon url',
|
||||||
ui_custom_image: 'Custom Image',
|
ui_custom_image: 'Custom Image',
|
||||||
ui_custom_image_label: 'URL to custom image',
|
ui_custom_image_label: 'URL to custom image',
|
||||||
ui_custom_image_hint: 'Image showed at homepage/login',
|
ui_custom_image_hint: 'Image showed at homepage/login',
|
||||||
@@ -655,12 +724,18 @@ window.localisation.en = {
|
|||||||
view_list: 'View wallets as list',
|
view_list: 'View wallets as list',
|
||||||
view_column: 'View wallets as rows',
|
view_column: 'View wallets as rows',
|
||||||
filter_payments: 'Filter payments',
|
filter_payments: 'Filter payments',
|
||||||
|
filter_labels: 'Filter labels',
|
||||||
filter_date: 'Filter by date',
|
filter_date: 'Filter by date',
|
||||||
websocket_example: 'Websocket example',
|
websocket_example: 'Websocket example',
|
||||||
|
client_id: 'Client ID',
|
||||||
secret_key: 'Secret Key',
|
secret_key: 'Secret Key',
|
||||||
signing_secret: 'Signing Secret',
|
signing_secret: 'Signing Secret',
|
||||||
signing_secret_hint:
|
signing_secret_hint:
|
||||||
'Signing secret for the webhook. Messages will be signed with this secret.',
|
'Signing secret for the webhook. Messages will be signed with this secret.',
|
||||||
|
webhook_id: 'Webhook ID',
|
||||||
|
webhook_id_hint: 'PayPal webhook ID used to verify incoming events.',
|
||||||
|
webhook_paypal_description:
|
||||||
|
'On the PayPal side configure a webhook pointing to your LNbits server.',
|
||||||
callback_success_url: 'Callback Success URL',
|
callback_success_url: 'Callback Success URL',
|
||||||
callback_success_url_hint:
|
callback_success_url_hint:
|
||||||
'The user will be redirected to this URL after the payment is successful',
|
'The user will be redirected to this URL after the payment is successful',
|
||||||
@@ -670,5 +745,18 @@ window.localisation.en = {
|
|||||||
paid: 'Paid',
|
paid: 'Paid',
|
||||||
funding_source_retries: 'Max Retries',
|
funding_source_retries: 'Max Retries',
|
||||||
funding_source_retries_desc:
|
funding_source_retries_desc:
|
||||||
'Maximum number of retries for funding sources, before it falls back to VoidWallet.'
|
'Maximum number of retries for funding sources, before it falls back to VoidWallet.',
|
||||||
|
add_label: 'Add Label',
|
||||||
|
label: 'Label',
|
||||||
|
labels: 'Labels',
|
||||||
|
label_filter: 'Label Filter',
|
||||||
|
no_labels_defined: 'No labels defined yet',
|
||||||
|
manage_labels: 'Manage Labels',
|
||||||
|
update_label: 'Update Label',
|
||||||
|
delete_label: 'Delete Label',
|
||||||
|
add_remove_labels: 'Add or Remove Labels',
|
||||||
|
payment_labels_updated: 'Payment labels updated',
|
||||||
|
color: 'Color',
|
||||||
|
sort: 'Sort',
|
||||||
|
sort_by: 'Sort by'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -628,10 +628,15 @@ window.localisation.fi = {
|
|||||||
filter_payments: 'Suodata maksuja',
|
filter_payments: 'Suodata maksuja',
|
||||||
filter_date: 'Suodata päiväyksellä',
|
filter_date: 'Suodata päiväyksellä',
|
||||||
websocket_example: 'Websocket example',
|
websocket_example: 'Websocket example',
|
||||||
|
client_id: 'Client ID',
|
||||||
secret_key: 'Secret Key',
|
secret_key: 'Secret Key',
|
||||||
signing_secret: 'Signing Secret',
|
signing_secret: 'Signing Secret',
|
||||||
signing_secret_hint:
|
signing_secret_hint:
|
||||||
'Signing secret for the webhook. Messages will be signed with this secret.',
|
'Signing secret for the webhook. Messages will be signed with this secret.',
|
||||||
|
webhook_id: 'Webhook ID',
|
||||||
|
webhook_id_hint: 'PayPal webhook ID used to verify incoming events.',
|
||||||
|
webhook_paypal_description:
|
||||||
|
'On the PayPal side configure a webhook pointing to your LNbits server.',
|
||||||
callback_success_url: 'Callback Success URL',
|
callback_success_url: 'Callback Success URL',
|
||||||
callback_success_url_hint:
|
callback_success_url_hint:
|
||||||
'The user will be redirected to this URL after the payment is successful'
|
'The user will be redirected to this URL after the payment is successful'
|
||||||
|
|||||||
@@ -1,696 +0,0 @@
|
|||||||
window.AdminPageLogic = {
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
settings: {},
|
|
||||||
logs: [],
|
|
||||||
library_images: [],
|
|
||||||
serverlogEnabled: false,
|
|
||||||
lnbits_theme_options: [
|
|
||||||
'classic',
|
|
||||||
'bitcoin',
|
|
||||||
'flamingo',
|
|
||||||
'cyber',
|
|
||||||
'freedom',
|
|
||||||
'mint',
|
|
||||||
'autumn',
|
|
||||||
'monochrome',
|
|
||||||
'salvador'
|
|
||||||
],
|
|
||||||
reactionOptions: [
|
|
||||||
'none',
|
|
||||||
'confettiBothSides',
|
|
||||||
'confettiFireworks',
|
|
||||||
'confettiStars',
|
|
||||||
'confettiTop'
|
|
||||||
],
|
|
||||||
globalBorderOptions: [
|
|
||||||
'retro-border',
|
|
||||||
'hard-border',
|
|
||||||
'neon-border',
|
|
||||||
'no-border'
|
|
||||||
],
|
|
||||||
auditData: {},
|
|
||||||
statusData: {},
|
|
||||||
statusDataTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'date',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('date'),
|
|
||||||
field: 'date'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'message',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('memo'),
|
|
||||||
field: 'message'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
formData: {
|
|
||||||
lnbits_exchange_rate_providers: []
|
|
||||||
},
|
|
||||||
chartReady: false,
|
|
||||||
formAddAdmin: '',
|
|
||||||
formAddUser: '',
|
|
||||||
formAddStripeUser: '',
|
|
||||||
hideInputToggle: true,
|
|
||||||
formAddExtensionsManifest: '',
|
|
||||||
nostrNotificationIdentifier: '',
|
|
||||||
emailNotificationAddress: '',
|
|
||||||
formAllowedIPs: '',
|
|
||||||
formCallbackUrlRule: '',
|
|
||||||
formBlockedIPs: '',
|
|
||||||
nostrAcceptedUrl: '',
|
|
||||||
formAddIncludePath: '',
|
|
||||||
formAddExcludePath: '',
|
|
||||||
formAddIncludeResponseCode: '',
|
|
||||||
isSuperUser: false,
|
|
||||||
wallet: {},
|
|
||||||
cancel: {},
|
|
||||||
colors: [
|
|
||||||
'primary',
|
|
||||||
'secondary',
|
|
||||||
'accent',
|
|
||||||
'positive',
|
|
||||||
'negative',
|
|
||||||
'info',
|
|
||||||
'warning',
|
|
||||||
'red',
|
|
||||||
'yellow',
|
|
||||||
'orange'
|
|
||||||
],
|
|
||||||
tab: 'funding',
|
|
||||||
needsRestart: false,
|
|
||||||
exchangesTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Exchange Name',
|
|
||||||
field: 'name',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'api_url',
|
|
||||||
align: 'left',
|
|
||||||
label: 'URL',
|
|
||||||
field: 'api_url',
|
|
||||||
sortable: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'path',
|
|
||||||
align: 'left',
|
|
||||||
label: 'JSON Path',
|
|
||||||
field: 'path',
|
|
||||||
sortable: false
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'exclude_to',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Exclude Currencies',
|
|
||||||
field: 'exclude_to',
|
|
||||||
sortable: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ticker_conversion',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Ticker Conversion',
|
|
||||||
field: 'ticker_conversion',
|
|
||||||
sortable: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
sortBy: 'name',
|
|
||||||
rowsPerPage: 100,
|
|
||||||
page: 1,
|
|
||||||
rowsNumber: 100
|
|
||||||
},
|
|
||||||
search: null,
|
|
||||||
hideEmpty: true
|
|
||||||
},
|
|
||||||
exchangeData: {
|
|
||||||
selectedProvider: null,
|
|
||||||
showTickerConversion: false,
|
|
||||||
convertFromTicker: null,
|
|
||||||
convertToTicker: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created() {
|
|
||||||
await this.getSettings()
|
|
||||||
await this.getAudit()
|
|
||||||
await this.getUploadedImages()
|
|
||||||
this.balance = +'{{ balance|safe }}'
|
|
||||||
const hash = window.location.hash.replace('#', '')
|
|
||||||
if (hash === 'exchange_providers') {
|
|
||||||
this.showExchangeProvidersTab(hash)
|
|
||||||
}
|
|
||||||
if (hash) {
|
|
||||||
this.tab = hash
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
lnbitsVersion() {
|
|
||||||
return LNBITS_VERSION
|
|
||||||
},
|
|
||||||
checkChanges() {
|
|
||||||
return !_.isEqual(this.settings, this.formData)
|
|
||||||
},
|
|
||||||
updateAvailable() {
|
|
||||||
return LNBITS_VERSION !== this.statusData.version
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
addAdminUser() {
|
|
||||||
let addUser = this.formAddAdmin
|
|
||||||
let admin_users = this.formData.lnbits_admin_users
|
|
||||||
if (addUser && addUser.length && !admin_users.includes(addUser)) {
|
|
||||||
this.formData.lnbits_admin_users = [...admin_users, addUser]
|
|
||||||
this.formAddAdmin = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeAdminUser(user) {
|
|
||||||
let admin_users = this.formData.lnbits_admin_users
|
|
||||||
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
|
||||||
},
|
|
||||||
addAllowedUser() {
|
|
||||||
let addUser = this.formAddUser
|
|
||||||
let allowed_users = this.formData.lnbits_allowed_users
|
|
||||||
if (addUser && addUser.length && !allowed_users.includes(addUser)) {
|
|
||||||
this.formData.lnbits_allowed_users = [...allowed_users, addUser]
|
|
||||||
this.formAddUser = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeAllowedUser(user) {
|
|
||||||
let allowed_users = this.formData.lnbits_allowed_users
|
|
||||||
this.formData.lnbits_allowed_users = allowed_users.filter(u => u !== user)
|
|
||||||
},
|
|
||||||
addStripeAllowedUser() {
|
|
||||||
const addUser = this.formAddStripeUser || ''
|
|
||||||
if (
|
|
||||||
addUser.length &&
|
|
||||||
!this.formData.stripe_limits.allowed_users.includes(addUser)
|
|
||||||
) {
|
|
||||||
this.formData.stripe_limits.allowed_users = [
|
|
||||||
...this.formData.stripe_limits.allowed_users,
|
|
||||||
addUser
|
|
||||||
]
|
|
||||||
this.formAddStripeUser = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeStripeAllowedUser(user) {
|
|
||||||
this.formData.stripe_limits.allowed_users =
|
|
||||||
this.formData.stripe_limits.allowed_users.filter(u => u !== user)
|
|
||||||
},
|
|
||||||
addIncludePath() {
|
|
||||||
if (!this.formAddIncludePath) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const paths = this.formData.lnbits_audit_include_paths
|
|
||||||
if (!paths.includes(this.formAddIncludePath)) {
|
|
||||||
this.formData.lnbits_audit_include_paths = [
|
|
||||||
...paths,
|
|
||||||
this.formAddIncludePath
|
|
||||||
]
|
|
||||||
}
|
|
||||||
this.formAddIncludePath = ''
|
|
||||||
},
|
|
||||||
removeIncludePath(path) {
|
|
||||||
this.formData.lnbits_audit_include_paths =
|
|
||||||
this.formData.lnbits_audit_include_paths.filter(p => p !== path)
|
|
||||||
},
|
|
||||||
addExcludePath() {
|
|
||||||
if (!this.formAddExcludePath) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const paths = this.formData.lnbits_audit_exclude_paths
|
|
||||||
if (!paths.includes(this.formAddExcludePath)) {
|
|
||||||
this.formData.lnbits_audit_exclude_paths = [
|
|
||||||
...paths,
|
|
||||||
this.formAddExcludePath
|
|
||||||
]
|
|
||||||
}
|
|
||||||
this.formAddExcludePath = ''
|
|
||||||
},
|
|
||||||
|
|
||||||
removeExcludePath(path) {
|
|
||||||
this.formData.lnbits_audit_exclude_paths =
|
|
||||||
this.formData.lnbits_audit_exclude_paths.filter(p => p !== path)
|
|
||||||
},
|
|
||||||
addIncludeResponseCode() {
|
|
||||||
if (!this.formAddIncludeResponseCode) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const codes = this.formData.lnbits_audit_http_response_codes
|
|
||||||
if (!codes.includes(this.formAddIncludeResponseCode)) {
|
|
||||||
this.formData.lnbits_audit_http_response_codes = [
|
|
||||||
...codes,
|
|
||||||
this.formAddIncludeResponseCode
|
|
||||||
]
|
|
||||||
}
|
|
||||||
this.formAddIncludeResponseCode = ''
|
|
||||||
},
|
|
||||||
removeIncludeResponseCode(code) {
|
|
||||||
this.formData.lnbits_audit_http_response_codes =
|
|
||||||
this.formData.lnbits_audit_http_response_codes.filter(c => c !== code)
|
|
||||||
},
|
|
||||||
addExtensionsManifest() {
|
|
||||||
const addManifest = this.formAddExtensionsManifest.trim()
|
|
||||||
const manifests = this.formData.lnbits_extensions_manifests
|
|
||||||
if (
|
|
||||||
addManifest &&
|
|
||||||
addManifest.length &&
|
|
||||||
!manifests.includes(addManifest)
|
|
||||||
) {
|
|
||||||
this.formData.lnbits_extensions_manifests = [...manifests, addManifest]
|
|
||||||
this.formAddExtensionsManifest = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeExtensionsManifest(manifest) {
|
|
||||||
const manifests = this.formData.lnbits_extensions_manifests
|
|
||||||
this.formData.lnbits_extensions_manifests = manifests.filter(
|
|
||||||
m => m !== manifest
|
|
||||||
)
|
|
||||||
},
|
|
||||||
addNostrNotificationIdentifier() {
|
|
||||||
const identifer = this.nostrNotificationIdentifier.trim()
|
|
||||||
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
|
|
||||||
if (identifer && identifer.length && !identifiers.includes(identifer)) {
|
|
||||||
this.formData.lnbits_nostr_notifications_identifiers = [
|
|
||||||
...identifiers,
|
|
||||||
identifer
|
|
||||||
]
|
|
||||||
this.nostrNotificationIdentifier = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeNostrNotificationIdentifier(identifer) {
|
|
||||||
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
|
|
||||||
this.formData.lnbits_nostr_notifications_identifiers = identifiers.filter(
|
|
||||||
m => m !== identifer
|
|
||||||
)
|
|
||||||
},
|
|
||||||
addEmailNotificationAddress() {
|
|
||||||
const email = this.emailNotificationAddress.trim()
|
|
||||||
const emails = this.formData.lnbits_email_notifications_to_emails
|
|
||||||
if (email && email.length && !emails.includes(email)) {
|
|
||||||
this.formData.lnbits_email_notifications_to_emails = [...emails, email]
|
|
||||||
this.emailNotificationAddress = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeEmailNotificationAddress(email) {
|
|
||||||
const emails = this.formData.lnbits_email_notifications_to_emails
|
|
||||||
this.formData.lnbits_email_notifications_to_emails = emails.filter(
|
|
||||||
m => m !== email
|
|
||||||
)
|
|
||||||
},
|
|
||||||
hideInputsToggle() {
|
|
||||||
this.hideInputToggle = !this.hideInputToggle
|
|
||||||
},
|
|
||||||
async toggleServerLog() {
|
|
||||||
this.serverlogEnabled = !this.serverlogEnabled
|
|
||||||
if (this.serverlogEnabled) {
|
|
||||||
const wsProto = location.protocol !== 'http:' ? 'wss://' : 'ws://'
|
|
||||||
const digestHex = await LNbits.utils.digestMessage(this.g.user.id)
|
|
||||||
const localUrl =
|
|
||||||
wsProto +
|
|
||||||
document.domain +
|
|
||||||
':' +
|
|
||||||
location.port +
|
|
||||||
'/api/v1/ws/' +
|
|
||||||
digestHex
|
|
||||||
this.ws = new WebSocket(localUrl)
|
|
||||||
this.ws.addEventListener('message', async ({data}) => {
|
|
||||||
this.logs.push(data.toString())
|
|
||||||
const scrollArea = this.$refs.logScroll
|
|
||||||
if (scrollArea) {
|
|
||||||
const scrollTarget = scrollArea.getScrollTarget()
|
|
||||||
const duration = 0
|
|
||||||
scrollArea.setScrollPosition(scrollTarget.scrollHeight, duration)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.ws.close()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addAllowedIPs() {
|
|
||||||
const allowedIPs = this.formAllowedIPs.trim()
|
|
||||||
const allowed_ips = this.formData.lnbits_allowed_ips
|
|
||||||
if (
|
|
||||||
allowedIPs &&
|
|
||||||
allowedIPs.length &&
|
|
||||||
!allowed_ips.includes(allowedIPs)
|
|
||||||
) {
|
|
||||||
this.formData.lnbits_allowed_ips = [...allowed_ips, allowedIPs]
|
|
||||||
this.formAllowedIPs = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeAllowedIPs(allowed_ip) {
|
|
||||||
const allowed_ips = this.formData.lnbits_allowed_ips
|
|
||||||
this.formData.lnbits_allowed_ips = allowed_ips.filter(
|
|
||||||
a => a !== allowed_ip
|
|
||||||
)
|
|
||||||
},
|
|
||||||
addBlockedIPs() {
|
|
||||||
const blockedIPs = this.formBlockedIPs.trim()
|
|
||||||
const blocked_ips = this.formData.lnbits_blocked_ips
|
|
||||||
if (
|
|
||||||
blockedIPs &&
|
|
||||||
blockedIPs.length &&
|
|
||||||
!blocked_ips.includes(blockedIPs)
|
|
||||||
) {
|
|
||||||
this.formData.lnbits_blocked_ips = [...blocked_ips, blockedIPs]
|
|
||||||
this.formBlockedIPs = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeBlockedIPs(blocked_ip) {
|
|
||||||
const blocked_ips = this.formData.lnbits_blocked_ips
|
|
||||||
this.formData.lnbits_blocked_ips = blocked_ips.filter(
|
|
||||||
b => b !== blocked_ip
|
|
||||||
)
|
|
||||||
},
|
|
||||||
addCallbackUrlRule() {
|
|
||||||
const allowedCallback = this.formCallbackUrlRule.trim()
|
|
||||||
const allowedCallbacks = this.formData.lnbits_callback_url_rules
|
|
||||||
if (
|
|
||||||
allowedCallback &&
|
|
||||||
allowedCallback.length &&
|
|
||||||
!allowedCallbacks.includes(allowedCallback)
|
|
||||||
) {
|
|
||||||
this.formData.lnbits_callback_url_rules = [
|
|
||||||
...allowedCallbacks,
|
|
||||||
allowedCallback
|
|
||||||
]
|
|
||||||
this.formCallbackUrlRule = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeCallbackUrlRule(allowedCallback) {
|
|
||||||
const allowedCallbacks = this.formData.lnbits_callback_url_rules
|
|
||||||
this.formData.lnbits_callback_url_rules = allowedCallbacks.filter(
|
|
||||||
a => a !== allowedCallback
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
addNostrUrl() {
|
|
||||||
const url = this.nostrAcceptedUrl.trim()
|
|
||||||
this.removeNostrUrl(url)
|
|
||||||
this.formData.nostr_absolute_request_urls.push(url)
|
|
||||||
this.nostrAcceptedUrl = ''
|
|
||||||
},
|
|
||||||
removeNostrUrl(url) {
|
|
||||||
this.formData.nostr_absolute_request_urls =
|
|
||||||
this.formData.nostr_absolute_request_urls.filter(b => b !== url)
|
|
||||||
},
|
|
||||||
addExchangeProvider() {
|
|
||||||
this.formData.lnbits_exchange_rate_providers = [
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
api_url: '',
|
|
||||||
path: '',
|
|
||||||
exclude_to: []
|
|
||||||
},
|
|
||||||
...this.formData.lnbits_exchange_rate_providers
|
|
||||||
]
|
|
||||||
},
|
|
||||||
removeExchangeProvider(provider) {
|
|
||||||
this.formData.lnbits_exchange_rate_providers =
|
|
||||||
this.formData.lnbits_exchange_rate_providers.filter(p => p !== provider)
|
|
||||||
},
|
|
||||||
removeExchangeTickerConversion(provider, ticker) {
|
|
||||||
provider.ticker_conversion = provider.ticker_conversion.filter(
|
|
||||||
t => t !== ticker
|
|
||||||
)
|
|
||||||
this.touchSettings()
|
|
||||||
},
|
|
||||||
addExchangeTickerConversion() {
|
|
||||||
if (!this.exchangeData.selectedProvider) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.exchangeData.selectedProvider.ticker_conversion.push(
|
|
||||||
`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`
|
|
||||||
)
|
|
||||||
this.touchSettings()
|
|
||||||
this.exchangeData.showTickerConversion = false
|
|
||||||
},
|
|
||||||
showTickerConversionDialog(provider) {
|
|
||||||
this.exchangeData.convertFromTicker = null
|
|
||||||
this.exchangeData.convertToTicker = null
|
|
||||||
this.exchangeData.selectedProvider = provider
|
|
||||||
this.exchangeData.showTickerConversion = true
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultSetting(fieldName) {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
`/admin/api/v1/settings/default?field_name=${fieldName}`
|
|
||||||
)
|
|
||||||
.then(response => {
|
|
||||||
this.formData[fieldName] = response.data.default_value
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
restartServer() {
|
|
||||||
LNbits.api
|
|
||||||
.request('GET', '/admin/api/v1/restart/')
|
|
||||||
.then(response => {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Success! Restarted Server',
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
this.needsRestart = false
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
formatDate(date) {
|
|
||||||
return moment
|
|
||||||
.utc(date * 1000)
|
|
||||||
.local()
|
|
||||||
.fromNow()
|
|
||||||
},
|
|
||||||
sendTestEmail() {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/admin/api/v1/testemail',
|
|
||||||
this.g.user.wallets[0].adminkey
|
|
||||||
)
|
|
||||||
.then(response => {
|
|
||||||
if (response.data.status === 'error') {
|
|
||||||
throw new Error(response.data.message)
|
|
||||||
}
|
|
||||||
this.$q.notify({
|
|
||||||
message: 'Test email sent!',
|
|
||||||
color: 'positive'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.$q.notify({
|
|
||||||
message: error.message,
|
|
||||||
color: 'negative'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getAudit() {
|
|
||||||
LNbits.api
|
|
||||||
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
|
||||||
.then(response => {
|
|
||||||
this.auditData = response.data
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
getExchangeRateHistory() {
|
|
||||||
LNbits.api
|
|
||||||
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
|
|
||||||
.then(response => {
|
|
||||||
this.initExchangeChart(response.data)
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async getSettings() {
|
|
||||||
await LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/admin/api/v1/settings',
|
|
||||||
this.g.user.wallets[0].adminkey
|
|
||||||
)
|
|
||||||
.then(response => {
|
|
||||||
this.isSuperUser = response.data.is_super_user || false
|
|
||||||
this.settings = response.data
|
|
||||||
this.formData = {...this.settings}
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
updateSettings() {
|
|
||||||
const data = _.omit(this.formData, [
|
|
||||||
'is_super_user',
|
|
||||||
'lnbits_allowed_funding_sources',
|
|
||||||
'touch'
|
|
||||||
])
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'PUT',
|
|
||||||
'/admin/api/v1/settings',
|
|
||||||
this.g.user.wallets[0].adminkey,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
.then(response => {
|
|
||||||
this.needsRestart =
|
|
||||||
this.settings.lnbits_backend_wallet_class !==
|
|
||||||
this.formData.lnbits_backend_wallet_class
|
|
||||||
this.settings = this.formData
|
|
||||||
this.formData = _.clone(this.settings)
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'positive',
|
|
||||||
message: `Success! Settings changed! ${
|
|
||||||
this.needsRestart ? 'Restart required!' : ''
|
|
||||||
}`,
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
deleteSettings() {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to restore settings to default?')
|
|
||||||
.onOk(() => {
|
|
||||||
LNbits.api
|
|
||||||
.request('DELETE', '/admin/api/v1/settings')
|
|
||||||
.then(response => {
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'positive',
|
|
||||||
message:
|
|
||||||
'Success! Restored settings to defaults. Restarting...',
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
this.$q.localStorage.clear()
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onImageInput(e) {
|
|
||||||
const file = e.target.files[0]
|
|
||||||
if (file) {
|
|
||||||
this.uploadImage(file)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uploadImage(file) {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'POST',
|
|
||||||
'/admin/api/v1/images',
|
|
||||||
this.g.user.wallets[0].adminkey,
|
|
||||||
formData,
|
|
||||||
{headers: {'Content-Type': 'multipart/form-data'}}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Image uploaded!',
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
this.getUploadedImages()
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
getUploadedImages() {
|
|
||||||
LNbits.api
|
|
||||||
.request('GET', '/admin/api/v1/images', this.g.user.wallets[0].inkey)
|
|
||||||
.then(response => {
|
|
||||||
this.library_images = response.data.map(image => ({
|
|
||||||
...image,
|
|
||||||
url: `${window.origin}/${image.directory}/${image.filename}`
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
deleteImage(filename) {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to delete this image?')
|
|
||||||
.onOk(() => {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'DELETE',
|
|
||||||
`/admin/api/v1/images/${filename}`,
|
|
||||||
this.g.user.wallets[0].adminkey
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Image deleted!',
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
this.getUploadedImages()
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
checkFiatProvider(providerName) {
|
|
||||||
LNbits.api
|
|
||||||
.request('PUT', `/api/v1/fiat/check/${providerName}`)
|
|
||||||
.then(response => {
|
|
||||||
response
|
|
||||||
const data = response.data
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: data.success ? 'positive' : 'warning',
|
|
||||||
message: data.message,
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
downloadBackup() {
|
|
||||||
window.open('/admin/api/v1/backup', '_blank')
|
|
||||||
},
|
|
||||||
showExchangeProvidersTab(tabName) {
|
|
||||||
if (tabName === 'exchange_providers') {
|
|
||||||
this.getExchangeRateHistory()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
touchSettings() {
|
|
||||||
this.formData.touch = null
|
|
||||||
},
|
|
||||||
initExchangeChart(data) {
|
|
||||||
const xValues = data.map(d =>
|
|
||||||
Quasar.date.formatDate(new Date(d.timestamp * 1000), 'HH:mm')
|
|
||||||
)
|
|
||||||
const exchanges = [
|
|
||||||
...this.formData.lnbits_exchange_rate_providers,
|
|
||||||
{name: 'LNbits'}
|
|
||||||
]
|
|
||||||
const datasets = exchanges.map(exchange => ({
|
|
||||||
label: exchange.name,
|
|
||||||
data: data.map(d => d.rates[exchange.name]),
|
|
||||||
pointStyle: true,
|
|
||||||
borderWidth: exchange.name === 'LNbits' ? 4 : 1,
|
|
||||||
tension: 0.4
|
|
||||||
}))
|
|
||||||
this.exchangeRatesChart = new Chart(
|
|
||||||
this.$refs.exchangeRatesChart.getContext('2d'),
|
|
||||||
{
|
|
||||||
type: 'line',
|
|
||||||
options: {
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
labels: xValues,
|
|
||||||
datasets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user