Compare commits
180
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5388e23a9 | ||
|
|
d01893f577 | ||
|
|
1fc15cde81 | ||
|
|
c711be4d60 | ||
|
|
ccb9dda164 | ||
|
|
100bc45185 | ||
|
|
222d1c8990 | ||
|
|
69c414fabf | ||
|
|
b33aa41b9a | ||
|
|
68a36dfd18 | ||
|
|
84a958af6e | ||
|
|
7db645d388 | ||
|
|
30e61eacf3 | ||
|
|
de496ed9e8 | ||
|
|
8cb35d9aac | ||
|
|
d662b03370 | ||
|
|
ef78ca7db3 | ||
|
|
c97c5842ad | ||
|
|
abce6196de | ||
|
|
e7d3be9854 | ||
|
|
e9dc8c775d | ||
|
|
31a2d68285 | ||
|
|
bf1ad185b9 | ||
|
|
62de35541c | ||
|
|
1a61435868 | ||
|
|
03ba28d0cb | ||
|
|
2b260bfdaf | ||
|
|
30ab42c0b2 | ||
|
|
f603026d5e | ||
|
|
84e6b3a9a8 | ||
|
|
3aafc0724f | ||
|
|
b978ce11fc | ||
|
|
656c6cac5b | ||
|
|
45e773b66f | ||
|
|
f692ac6f12 | ||
|
|
78202a027b | ||
|
|
28fa0906a6 | ||
|
|
91354f8be4 | ||
|
|
5f5d45e89f | ||
|
|
52bb125a25 | ||
|
|
2ca1ed897c | ||
|
|
631f4e8455 | ||
|
|
b22f88b264 | ||
|
|
c08bc02930 | ||
|
|
e020a7c5a0 | ||
|
|
d2e8914835 | ||
|
|
5e79ca35ab | ||
|
|
4690d178a2 | ||
|
|
c5825aa4c3 | ||
|
|
ebe0c4e3c3 | ||
|
|
383bdb6312 | ||
|
|
a947686d79 | ||
|
|
28e9d32b44 | ||
|
|
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 |
@@ -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
|
||||||
|
|||||||
+4
-3
@@ -6,6 +6,10 @@
|
|||||||
# The following settings are ONLY set in your .env file.
|
# The following settings are ONLY set in your .env file.
|
||||||
# They are NOT managed by the Admin UI and are not stored in the database.
|
# They are NOT managed by the Admin UI and are not stored in the database.
|
||||||
|
|
||||||
|
# === First Install Token ===
|
||||||
|
# if set the user is required to enter this token on the /first_install page
|
||||||
|
# FIRST_INSTALL_TOKEN="myaccesstoken"
|
||||||
|
|
||||||
# === Logging and Development ===
|
# === Logging and Development ===
|
||||||
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
@@ -18,7 +22,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 +190,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 +335,3 @@ LNBITS_RESERVE_FEE_PERCENT=1.0
|
|||||||
######################################
|
######################################
|
||||||
###### Logging and Development #######
|
###### Logging and Development #######
|
||||||
######################################
|
######################################
|
||||||
|
|
||||||
|
|||||||
@@ -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,14 +41,26 @@ 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
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Build and publish to pypi
|
- name: Set up Python 3.10
|
||||||
uses: JRubics/poetry-publish@v1.15
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
python-version: "3.10"
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Build the project
|
||||||
|
run: uv build
|
||||||
|
- name: Publish to pypi
|
||||||
|
env:
|
||||||
|
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
||||||
|
run: uv publish
|
||||||
|
|
||||||
|
appimage:
|
||||||
|
needs: [ release ]
|
||||||
|
uses: ./.github/workflows/appimage.yml
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
outputs:
|
outputs:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
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
|
||||||
@@ -22,10 +22,18 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: ${{ github.ref_name }}
|
tag: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
upload_url=$(gh release create "$tag" --generate-notes --draft --json upload_url -q '.upload_url')
|
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"
|
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:
|
||||||
@@ -35,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:
|
||||||
@@ -44,17 +53,22 @@ 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
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Build and publish to pypi
|
- name: Set up Python 3.10
|
||||||
uses: JRubics/poetry-publish@v1.15
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
python-version: "3.10"
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Build the project
|
||||||
|
run: uv build
|
||||||
|
- name: Publish to pypi
|
||||||
|
env:
|
||||||
|
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
||||||
|
run: uv publish
|
||||||
|
|
||||||
appimage:
|
appimage:
|
||||||
needs: [ release ]
|
needs: [ release ]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,9 +34,9 @@ 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 = {
|
||||||
@@ -46,7 +47,7 @@ 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)
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
if not wallet or not wallet.can_view_payments:
|
if not wallet or not wallet.can_view_payments:
|
||||||
return None
|
return None
|
||||||
values["wallet_id"] = wallet.source_wallet_id
|
values["wallet_id"] = wallet.source_wallet_id
|
||||||
@@ -69,7 +70,7 @@ 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)
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
if not wallet or not wallet.can_view_payments:
|
if not wallet or not wallet.can_view_payments:
|
||||||
return None
|
return None
|
||||||
payment = await (conn or db).fetchone(
|
payment = await (conn or db).fetchone(
|
||||||
@@ -124,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,
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ 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:
|
||||||
wallet = await get_wallet(wallet_id)
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
if not wallet or not wallet.can_view_payments:
|
if not wallet or not wallet.can_view_payments:
|
||||||
return Page(data=[], total=0)
|
return Page(data=[], total=0)
|
||||||
|
|
||||||
@@ -305,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()),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -315,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"
|
||||||
)
|
)
|
||||||
@@ -326,6 +335,7 @@ 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()
|
||||||
@@ -361,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 not wallet or not wallet.can_view_payments:
|
if not wallet or not wallet.can_view_payments:
|
||||||
return []
|
return []
|
||||||
balance = wallet.balance_msat
|
balance = wallet.balance_msat
|
||||||
values["wallet_id"] = wallet.source_wallet_id
|
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
|
||||||
|
|||||||
+94
-30
@@ -4,10 +4,12 @@ from typing import Any
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||||
from lnbits.core.crud.wallets import get_wallets
|
from lnbits.core.crud.wallets import clear_wallet_cache, 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
|
||||||
|
from lnbits.helpers import sha256s
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
Account,
|
Account,
|
||||||
@@ -30,9 +32,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
|
||||||
|
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
|||||||
"DELETE from accounts WHERE id = :user",
|
"DELETE from accounts WHERE id = :user",
|
||||||
{"user": user_id},
|
{"user": user_id},
|
||||||
)
|
)
|
||||||
|
await clear_user_id_cache(user_id)
|
||||||
|
|
||||||
|
|
||||||
async def get_accounts(
|
async def get_accounts(
|
||||||
@@ -69,6 +72,7 @@ async def get_accounts(
|
|||||||
accounts.email,
|
accounts.email,
|
||||||
accounts.pubkey,
|
accounts.pubkey,
|
||||||
accounts.external_id,
|
accounts.external_id,
|
||||||
|
accounts.activated,
|
||||||
SUM(COALESCE((
|
SUM(COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)) as balance_msat,
|
), 0)) as balance_msat,
|
||||||
@@ -93,12 +97,18 @@ async def get_accounts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
|
async def get_account(
|
||||||
|
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||||
|
) -> Account | None:
|
||||||
if len(user_id) == 0:
|
if len(user_id) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE id = :id",
|
"""
|
||||||
{"id": user_id},
|
SELECT * FROM accounts
|
||||||
|
WHERE id = :id AND (activated = true OR activated = :activated)
|
||||||
|
""",
|
||||||
|
{"id": user_id, "activated": active_only},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,66 +134,98 @@ async def delete_accounts_no_wallets(
|
|||||||
|
|
||||||
|
|
||||||
async def get_account_by_username(
|
async def get_account_by_username(
|
||||||
username: str, conn: Connection | None = None
|
username: str, active_only: bool = True, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
if len(username) == 0:
|
if len(username) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(username) = :username",
|
"""
|
||||||
{"username": username.lower()},
|
SELECT * FROM accounts
|
||||||
|
WHERE
|
||||||
|
LOWER(username) = :username
|
||||||
|
AND (activated = true OR activated = :activated)
|
||||||
|
""",
|
||||||
|
{"username": username.lower(), "activated": active_only},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_pubkey(
|
async def get_account_by_pubkey(
|
||||||
pubkey: str, conn: Connection | None = None
|
pubkey: str, active_only: bool = True, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
"""
|
||||||
{"pubkey": pubkey.lower()},
|
SELECT * FROM accounts
|
||||||
|
WHERE
|
||||||
|
LOWER(pubkey) = :pubkey
|
||||||
|
AND (activated = true OR activated = :activated)
|
||||||
|
""",
|
||||||
|
{"pubkey": pubkey.lower(), "activated": active_only},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_email(
|
async def get_account_by_email(
|
||||||
email: str, conn: Connection | None = None
|
email: str, active_only: bool = True, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
if len(email) == 0:
|
if len(email) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(email) = :email",
|
"""
|
||||||
{"email": email.lower()},
|
SELECT * FROM accounts
|
||||||
|
WHERE
|
||||||
|
LOWER(email) = :email
|
||||||
|
AND (activated = true OR activated = :activated)
|
||||||
|
""",
|
||||||
|
{"email": email.lower(), "activated": active_only},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_username_or_email(
|
async def get_account_by_username_or_email(
|
||||||
username_or_email: str, conn: Connection | None = None
|
username_or_email: str,
|
||||||
|
active_only: bool = True,
|
||||||
|
conn: Connection | None = None,
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM accounts
|
SELECT * FROM accounts
|
||||||
WHERE LOWER(email) = :value or LOWER(username) = :value
|
WHERE
|
||||||
|
(LOWER(email) = :value or LOWER(username) = :value)
|
||||||
|
AND (activated = true OR activated = :activated)
|
||||||
""",
|
""",
|
||||||
{"value": username_or_email.lower()},
|
{"value": username_or_email.lower(), "activated": active_only},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
async def get_user(
|
||||||
account = await get_account(user_id, conn)
|
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||||
if not account:
|
) -> User | None:
|
||||||
return None
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
return await get_user_from_account(account, conn)
|
account = await get_account(user_id, active_only, conn=conn)
|
||||||
|
if not account:
|
||||||
|
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,
|
||||||
|
activated=account.activated,
|
||||||
email=account.email,
|
email=account.email,
|
||||||
username=account.username,
|
username=account.username,
|
||||||
pubkey=account.pubkey,
|
pubkey=account.pubkey,
|
||||||
@@ -197,21 +239,43 @@ async def get_user_from_account(
|
|||||||
super_user=account.is_super_user,
|
super_user=account.is_super_user,
|
||||||
fiat_providers=account.fiat_providers,
|
fiat_providers=account.fiat_providers,
|
||||||
has_password=account.password_hash is not None,
|
has_password=account.password_hash is not None,
|
||||||
|
ui_customization=account.ui_customization or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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(
|
||||||
user_id: str, conn: Connection | None = None
|
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||||
) -> UserAcls:
|
) -> UserAcls:
|
||||||
user_acls = await (conn or db).fetchone(
|
user_acls = await (conn or db).fetchone(
|
||||||
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
"""
|
||||||
{"id": user_id},
|
SELECT id, access_control_list FROM accounts
|
||||||
|
WHERE id = :user_id AND (activated = true OR activated = :activated)
|
||||||
|
""",
|
||||||
|
{"user_id": user_id, "activated": active_only},
|
||||||
UserAcls,
|
UserAcls,
|
||||||
)
|
)
|
||||||
|
|
||||||
return user_acls or UserAcls(id=user_id)
|
return user_acls or UserAcls(id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def clear_user_id_cache(user_id: str):
|
||||||
|
user = await get_user(user_id, active_only=True)
|
||||||
|
if user:
|
||||||
|
clear_user_cache(user)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_user_cache(user: User):
|
||||||
|
user_cache_key: str | None = cache.pop(
|
||||||
|
f"auth:user:cache_key:{sha256s(user.id)}", None
|
||||||
|
)
|
||||||
|
if user_cache_key:
|
||||||
|
cache.pop(user_cache_key)
|
||||||
|
for wallet in user.wallets:
|
||||||
|
clear_wallet_cache(wallet)
|
||||||
|
|||||||
@@ -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, WalletType
|
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
|
||||||
|
|
||||||
@@ -44,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_id_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"""
|
||||||
@@ -63,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_id_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},
|
||||||
@@ -72,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_id_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)
|
||||||
@@ -222,11 +226,14 @@ async def get_wallet_for_key(
|
|||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
wallet = await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((
|
SELECT wallets.*, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)
|
), 0)
|
||||||
AS balance_msat FROM wallets
|
AS balance_msat FROM wallets
|
||||||
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
INNER JOIN accounts ON wallets.user = accounts.id
|
||||||
|
WHERE (adminkey = :key OR inkey = :key)
|
||||||
|
AND deleted = false
|
||||||
|
AND accounts.activated = true
|
||||||
""",
|
""",
|
||||||
{"key": key},
|
{"key": key},
|
||||||
Wallet,
|
Wallet,
|
||||||
@@ -240,6 +247,27 @@ async def get_wallet_for_key(
|
|||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
|
async def get_base_wallet_for_key(
|
||||||
|
key: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> BaseWallet | None:
|
||||||
|
wallet = await (conn or db).fetchone(
|
||||||
|
"""
|
||||||
|
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets
|
||||||
|
INNER JOIN accounts ON wallets.user = accounts.id
|
||||||
|
WHERE (adminkey = :key OR inkey = :key)
|
||||||
|
AND deleted = false
|
||||||
|
AND accounts.activated = true
|
||||||
|
""",
|
||||||
|
{"key": key},
|
||||||
|
BaseWallet,
|
||||||
|
)
|
||||||
|
if not wallet:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
async def get_source_wallet(
|
async def get_source_wallet(
|
||||||
wallet: Wallet, conn: Connection | None = None
|
wallet: Wallet, conn: Connection | None = None
|
||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
@@ -270,3 +298,16 @@ 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_id_cache(wallet_id: str):
|
||||||
|
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}")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_wallet_cache(wallet: Wallet):
|
||||||
|
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}")
|
||||||
|
|||||||
@@ -786,3 +786,81 @@ async def m038_add_labels_for_payments(db: Connection):
|
|||||||
ALTER TABLE apipayments ADD COLUMN labels TEXT
|
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}");
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m043_add_ui_customization_to_accounts(db: Connection):
|
||||||
|
"""
|
||||||
|
Adds ui_customization column to accounts.
|
||||||
|
Used for server side persistence of UI customization settings.
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
async def m044_add_activated_to_accounts(db: Connection):
|
||||||
|
"""
|
||||||
|
Adds activated column to accounts.
|
||||||
|
Used for account activation status.
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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):
|
||||||
@@ -267,10 +269,30 @@ class ExtensionRelease(BaseModel):
|
|||||||
async def get_github_releases(cls, org: str, repo: str) -> list[ExtensionRelease]:
|
async def get_github_releases(cls, org: str, repo: str) -> list[ExtensionRelease]:
|
||||||
try:
|
try:
|
||||||
github_releases = await cls.fetch_github_releases(org, repo)
|
github_releases = await cls.fetch_github_releases(org, repo)
|
||||||
return [
|
extension_releases = [
|
||||||
ExtensionRelease.from_github_release(f"{org}/{repo}", r)
|
ExtensionRelease.from_github_release(f"{org}/{repo}", r)
|
||||||
for r in github_releases
|
for r in github_releases
|
||||||
]
|
]
|
||||||
|
for release in extension_releases:
|
||||||
|
if not release.details_link:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
config = await ExtensionConfig.fetch_github_release_config(
|
||||||
|
org, repo, release.version
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
config = None
|
||||||
|
if not config:
|
||||||
|
continue
|
||||||
|
|
||||||
|
release.min_lnbits_version = config.min_lnbits_version
|
||||||
|
release.max_lnbits_version = config.max_lnbits_version
|
||||||
|
release.is_version_compatible = config.is_version_compatible()
|
||||||
|
|
||||||
|
release.icon = icon_to_github_url(f"{org}/{repo}", config.tile)
|
||||||
|
|
||||||
|
return extension_releases
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
return []
|
return []
|
||||||
@@ -606,6 +628,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 +797,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:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import random
|
||||||
|
import uuid
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
@@ -55,6 +57,29 @@ class DataField(BaseModel):
|
|||||||
field_type += ' = "sat"'
|
field_type += ' = "sat"'
|
||||||
return f"{field_name}: {field_type}"
|
return f"{field_name}: {field_type}"
|
||||||
|
|
||||||
|
def field_to_random_value(self) -> str:
|
||||||
|
field_name = camel_to_snake(self.name)
|
||||||
|
field_value: Any = f'"{self.type}"'
|
||||||
|
if self.type == "json":
|
||||||
|
field_value = '"{}"'
|
||||||
|
elif self.type == "wallet":
|
||||||
|
field_value = f'"{uuid.uuid4()}"'
|
||||||
|
elif self.type == "currency":
|
||||||
|
field_value = '"sat"'
|
||||||
|
elif self.type in ["str", "text"]:
|
||||||
|
field_value = f'"{field_name}_{urlsafe_short_hash()}"'
|
||||||
|
elif self.type == "int":
|
||||||
|
field_value = random.randint(1, 100) # noqa: S311
|
||||||
|
elif self.type == "float":
|
||||||
|
field_value = random.uniform(1.0, 100.0) # noqa: S311
|
||||||
|
elif self.type == "bool":
|
||||||
|
field_value = random.choice([True, False]) # noqa: S311
|
||||||
|
elif self.type == "datetime":
|
||||||
|
random_days = random.randint(-30, 30) # noqa: S311
|
||||||
|
random_date = datetime.now(timezone.utc) - timedelta(days=random_days)
|
||||||
|
field_value = f"""datetime.fromisoformat("{random_date.isoformat()}")"""
|
||||||
|
return f"{field_name} = {field_value},"
|
||||||
|
|
||||||
def field_to_js(self) -> str:
|
def field_to_js(self) -> str:
|
||||||
field_name = camel_to_snake(self.name)
|
field_name = camel_to_snake(self.name)
|
||||||
default_value = "null"
|
default_value = "null"
|
||||||
|
|||||||
@@ -6,23 +6,16 @@ from typing import Literal
|
|||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from lnurl import LnurlWithdrawResponse
|
from lnurl import LnurlWithdrawResponse
|
||||||
|
from loguru import logger
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
from lnbits.fiat import get_fiat_provider
|
|
||||||
from lnbits.fiat.base import (
|
from lnbits.fiat.base import (
|
||||||
FiatPaymentFailedStatus,
|
|
||||||
FiatPaymentPendingStatus,
|
|
||||||
FiatPaymentStatus,
|
FiatPaymentStatus,
|
||||||
FiatPaymentSuccessStatus,
|
|
||||||
)
|
)
|
||||||
from lnbits.utils.exchange_rates import allowed_currencies
|
from lnbits.utils.exchange_rates import allowed_currencies
|
||||||
from lnbits.wallets import get_funding_source
|
|
||||||
from lnbits.wallets.base import (
|
from lnbits.wallets.base import (
|
||||||
PaymentFailedStatus,
|
|
||||||
PaymentPendingStatus,
|
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
PaymentSuccessStatus,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -130,59 +123,23 @@ class Payment(BaseModel):
|
|||||||
"fiat_"
|
"fiat_"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
||||||
async def check_status(
|
async def check_status(
|
||||||
self, skip_internal_payment_notifications: bool | None = False
|
self, skip_internal_payment_notifications: bool | None = False
|
||||||
) -> PaymentStatus:
|
) -> PaymentStatus:
|
||||||
if self.is_internal:
|
logger.warning("payment.check_status() is deprecated.")
|
||||||
if self.success:
|
from lnbits.core.services.payments import check_payment_status
|
||||||
return PaymentSuccessStatus()
|
|
||||||
if self.failed:
|
|
||||||
return PaymentFailedStatus()
|
|
||||||
if self.is_in and self.fiat_provider:
|
|
||||||
fiat_status = await self.check_fiat_status(
|
|
||||||
skip_internal_payment_notifications
|
|
||||||
)
|
|
||||||
return PaymentStatus(paid=fiat_status.paid)
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
funding_source = get_funding_source()
|
|
||||||
if self.is_out:
|
|
||||||
status = await funding_source.get_payment_status(self.checking_id)
|
|
||||||
else:
|
|
||||||
status = await funding_source.get_invoice_status(self.checking_id)
|
|
||||||
return status
|
|
||||||
|
|
||||||
|
return await check_payment_status(self, skip_internal_payment_notifications)
|
||||||
|
|
||||||
|
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
||||||
async def check_fiat_status(
|
async def check_fiat_status(
|
||||||
self, skip_internal_payment_notifications: bool | None = False
|
self, skip_internal_payment_notifications: bool | None = False
|
||||||
) -> FiatPaymentStatus:
|
) -> FiatPaymentStatus:
|
||||||
if not self.is_internal:
|
logger.warning("payment.check_fiat_status() is deprecated.")
|
||||||
return FiatPaymentPendingStatus()
|
from lnbits.core.services.fiat_providers import check_fiat_status
|
||||||
if self.success:
|
|
||||||
return FiatPaymentSuccessStatus()
|
|
||||||
if self.failed:
|
|
||||||
return FiatPaymentFailedStatus()
|
|
||||||
|
|
||||||
if not self.fiat_provider:
|
return await check_fiat_status(self, skip_internal_payment_notifications)
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
|
|
||||||
checking_id = self.extra.get("fiat_checking_id")
|
|
||||||
if not checking_id:
|
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
|
|
||||||
fiat_provider = await get_fiat_provider(self.fiat_provider)
|
|
||||||
if not fiat_provider:
|
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
|
||||||
|
|
||||||
if skip_internal_payment_notifications:
|
|
||||||
return fiat_status
|
|
||||||
|
|
||||||
if fiat_status.success:
|
|
||||||
# notify receivers asynchronously
|
|
||||||
from lnbits.tasks import internal_invoice_queue
|
|
||||||
|
|
||||||
await internal_invoice_queue.put(self.checking_id)
|
|
||||||
|
|
||||||
return fiat_status
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentFilters(FilterModel):
|
class PaymentFilters(FilterModel):
|
||||||
@@ -196,7 +153,15 @@ class PaymentFilters(FilterModel):
|
|||||||
"labels",
|
"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
|
||||||
|
|||||||
@@ -172,14 +172,23 @@ 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):
|
||||||
|
activated: bool = True
|
||||||
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
|
||||||
pubkey: str | None = None
|
pubkey: str | None = None
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
ui_customization: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
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))
|
||||||
@@ -194,6 +203,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()
|
||||||
@@ -229,6 +242,7 @@ class Account(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
|
activated: bool = True
|
||||||
transaction_count: int | None = 0
|
transaction_count: int | None = 0
|
||||||
wallet_count: int | None = 0
|
wallet_count: int | None = 0
|
||||||
balance_msat: int | None = 0
|
balance_msat: int | None = 0
|
||||||
@@ -237,7 +251,7 @@ class AccountOverview(Account):
|
|||||||
|
|
||||||
class AccountFilters(FilterModel):
|
class AccountFilters(FilterModel):
|
||||||
__search_fields__ = [
|
__search_fields__ = [
|
||||||
"user",
|
"id",
|
||||||
"email",
|
"email",
|
||||||
"username",
|
"username",
|
||||||
"pubkey",
|
"pubkey",
|
||||||
@@ -245,17 +259,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
|
||||||
@@ -263,6 +278,7 @@ class AccountFilters(FilterModel):
|
|||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
activated: bool = True
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
@@ -276,6 +292,7 @@ class User(BaseModel):
|
|||||||
fiat_providers: list[str] = []
|
fiat_providers: list[str] = []
|
||||||
has_password: bool = False
|
has_password: bool = False
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
ui_customization: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wallet_ids(self) -> list[str]:
|
def wallet_ids(self) -> list[str]:
|
||||||
@@ -301,6 +318,7 @@ class RegisterUser(BaseModel):
|
|||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
invitation_code: str | None = Query(default=None, min_length=1, max_length=256)
|
||||||
|
|
||||||
|
|
||||||
class CreateUser(BaseModel):
|
class CreateUser(BaseModel):
|
||||||
@@ -344,6 +362,7 @@ class UpdateSuperuserPassword(BaseModel):
|
|||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
first_install_token: str | None = Query(None)
|
||||||
|
|
||||||
|
|
||||||
class LoginUsr(BaseModel):
|
class LoginUsr(BaseModel):
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from lnbits.db import FilterModel
|
|||||||
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
|
||||||
@@ -110,13 +110,16 @@ class WalletExtra(BaseModel):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
wallet_type: str = WalletType.LIGHTNING.value
|
|
||||||
|
|
||||||
|
class Wallet(BaseWallet):
|
||||||
|
name: str
|
||||||
# Must be set only for shared wallets
|
# Must be set only for shared wallets
|
||||||
shared_wallet_id: str | None = None
|
shared_wallet_id: str | None = None
|
||||||
deleted: bool = False
|
deleted: bool = False
|
||||||
@@ -230,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"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from .fiat_providers import check_fiat_status
|
||||||
from .funding_source import (
|
from .funding_source import (
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
switch_to_voidwallet,
|
switch_to_voidwallet,
|
||||||
@@ -7,6 +8,7 @@ from .notifications import enqueue_admin_notification, send_payment_notification
|
|||||||
from .payments import (
|
from .payments import (
|
||||||
calculate_fiat_amounts,
|
calculate_fiat_amounts,
|
||||||
cancel_hold_invoice,
|
cancel_hold_invoice,
|
||||||
|
check_payment_status,
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
check_wallet_limits,
|
check_wallet_limits,
|
||||||
create_fiat_invoice,
|
create_fiat_invoice,
|
||||||
@@ -40,6 +42,8 @@ __all__ = [
|
|||||||
"calculate_fiat_amounts",
|
"calculate_fiat_amounts",
|
||||||
"cancel_hold_invoice",
|
"cancel_hold_invoice",
|
||||||
"check_admin_settings",
|
"check_admin_settings",
|
||||||
|
"check_fiat_status",
|
||||||
|
"check_payment_status",
|
||||||
"check_transaction_status",
|
"check_transaction_status",
|
||||||
"check_wallet_limits",
|
"check_wallet_limits",
|
||||||
"check_webpush_settings",
|
"check_webpush_settings",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import io
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
|
from loguru import logger
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from lnbits.core.crud.assets import create_asset, get_user_assets_count
|
from lnbits.core.crud.assets import create_asset, get_user_assets_count
|
||||||
@@ -16,7 +17,7 @@ async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) ->
|
|||||||
if file.content_type.lower() not in settings.lnbits_assets_allowed_mime_types:
|
if file.content_type.lower() not in settings.lnbits_assets_allowed_mime_types:
|
||||||
raise ValueError(f"File type '{file.content_type}' not allowed.")
|
raise ValueError(f"File type '{file.content_type}' not allowed.")
|
||||||
|
|
||||||
if user_id not in settings.lnbits_assets_no_limit_users:
|
if not settings.is_unlimited_assets_user(user_id):
|
||||||
user_assets_count = await get_user_assets_count(user_id)
|
user_assets_count = await get_user_assets_count(user_id)
|
||||||
if user_assets_count >= settings.lnbits_max_assets_per_user:
|
if user_assets_count >= settings.lnbits_max_assets_per_user:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -29,17 +30,7 @@ async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) ->
|
|||||||
f"File limit of {settings.lnbits_max_asset_size_mb}MB exceeded."
|
f"File limit of {settings.lnbits_max_asset_size_mb}MB exceeded."
|
||||||
)
|
)
|
||||||
|
|
||||||
image = Image.open(io.BytesIO(contents))
|
thumb_buffer = thumbnail_from_bytes(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(
|
asset = Asset(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
@@ -48,9 +39,32 @@ async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) ->
|
|||||||
is_public=is_public,
|
is_public=is_public,
|
||||||
name=file.filename or "unnamed",
|
name=file.filename or "unnamed",
|
||||||
size_bytes=len(contents),
|
size_bytes=len(contents),
|
||||||
thumbnail_base64=base64.b64encode(thumb_buffer.getvalue()).decode("utf-8"),
|
thumbnail_base64=(
|
||||||
|
base64.b64encode(thumb_buffer.getvalue()).decode("utf-8")
|
||||||
|
if thumb_buffer
|
||||||
|
else None
|
||||||
|
),
|
||||||
data=contents,
|
data=contents,
|
||||||
)
|
)
|
||||||
|
|
||||||
await create_asset(asset)
|
await create_asset(asset)
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
|
||||||
|
def thumbnail_from_bytes(contents: bytes) -> io.BytesIO | None:
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
return thumb_buffer
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Failed to create thumbnail: {exc}")
|
||||||
|
return None
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from lnbits.helpers import (
|
|||||||
camel_to_words,
|
camel_to_words,
|
||||||
download_url,
|
download_url,
|
||||||
lowercase_first_letter,
|
lowercase_first_letter,
|
||||||
|
snake_to_camel,
|
||||||
)
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
@@ -195,13 +196,29 @@ def _copy_ext_stub_to_build_dir(
|
|||||||
ext_build_dir = Path(working_dir, new_ext_id, working_dir_name, new_ext_id)
|
ext_build_dir = Path(working_dir, new_ext_id, working_dir_name, new_ext_id)
|
||||||
shutil.rmtree(ext_build_dir, True)
|
shutil.rmtree(ext_build_dir, True)
|
||||||
|
|
||||||
shutil.copytree(ext_stub_cache_dir, ext_build_dir)
|
shutil.copytree(
|
||||||
|
ext_stub_cache_dir,
|
||||||
|
ext_build_dir,
|
||||||
|
ignore=shutil.ignore_patterns(
|
||||||
|
"__pycache__",
|
||||||
|
".git",
|
||||||
|
".env",
|
||||||
|
".venv",
|
||||||
|
"*.env",
|
||||||
|
"*.log",
|
||||||
|
"node_modules",
|
||||||
|
".mypy_cache",
|
||||||
|
".pytest_cache",
|
||||||
|
".ruff_cache",
|
||||||
|
),
|
||||||
|
)
|
||||||
return ext_build_dir
|
return ext_build_dir
|
||||||
|
|
||||||
|
|
||||||
def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None:
|
def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None:
|
||||||
parsed_data = _parse_extension_data(data)
|
parsed_data = _parse_extension_data(data)
|
||||||
for py_file in py_files:
|
test_files = [f"tests/{p.name}" for p in Path(ext_stub_dir, "tests").glob("*.py")]
|
||||||
|
for py_file in py_files + test_files:
|
||||||
template_path = Path(ext_stub_dir, py_file).as_posix()
|
template_path = Path(ext_stub_dir, py_file).as_posix()
|
||||||
rederer = _render_file(template_path, parsed_data)
|
rederer = _render_file(template_path, parsed_data)
|
||||||
with open(template_path, "w", encoding="utf-8") as f:
|
with open(template_path, "w", encoding="utf-8") as f:
|
||||||
@@ -209,7 +226,7 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
|||||||
|
|
||||||
_remove_lines_with_string(template_path, remove_line_marker)
|
_remove_lines_with_string(template_path, remove_line_marker)
|
||||||
|
|
||||||
template_path = Path(ext_stub_dir, "static", "js", "index.js").as_posix()
|
template_path = Path(ext_stub_dir, "static", "index.js").as_posix()
|
||||||
rederer = _render_file(
|
rederer = _render_file(
|
||||||
template_path, {"preview": data.preview_action, **parsed_data}
|
template_path, {"preview": data.preview_action, **parsed_data}
|
||||||
)
|
)
|
||||||
@@ -234,9 +251,7 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
|||||||
"settingsFormDialog.data",
|
"settingsFormDialog.data",
|
||||||
ext_stub_dir,
|
ext_stub_dir,
|
||||||
)
|
)
|
||||||
template_path = Path(
|
template_path = Path(ext_stub_dir, "static", "index.vue").as_posix()
|
||||||
ext_stub_dir, "templates", "extension_builder_stub", "index.html"
|
|
||||||
).as_posix()
|
|
||||||
rederer = _render_file(
|
rederer = _render_file(
|
||||||
template_path,
|
template_path,
|
||||||
{
|
{
|
||||||
@@ -263,12 +278,12 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
|||||||
"publicClientData",
|
"publicClientData",
|
||||||
ext_stub_dir,
|
ext_stub_dir,
|
||||||
)
|
)
|
||||||
public_template_path = Path(
|
public_template_path = Path(ext_stub_dir, "static", "public_page.vue")
|
||||||
ext_stub_dir, "templates", "extension_builder_stub", "public_page.html"
|
public_component_path = Path(ext_stub_dir, "static", "public_page.js")
|
||||||
)
|
|
||||||
template_path = public_template_path.as_posix()
|
template_path = public_template_path.as_posix()
|
||||||
if not data.public_page.has_public_page:
|
if not data.public_page.has_public_page:
|
||||||
public_template_path.unlink(missing_ok=True)
|
public_template_path.unlink(missing_ok=True)
|
||||||
|
public_component_path.unlink(missing_ok=True)
|
||||||
else:
|
else:
|
||||||
rederer = _render_file(
|
rederer = _render_file(
|
||||||
template_path,
|
template_path,
|
||||||
@@ -308,8 +323,10 @@ def zip_directory(source_dir, zip_path):
|
|||||||
|
|
||||||
def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) -> None:
|
def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) -> None:
|
||||||
extension_dir_path = extension_dir.as_posix()
|
extension_dir_path = extension_dir.as_posix()
|
||||||
|
# the order of fields is important, do not chage
|
||||||
rename_values = {
|
rename_values = {
|
||||||
"extension_builder_stub_name": data.name,
|
"extension_builder_stub_name": data.name,
|
||||||
|
"extension_builder_stub_camel_name": snake_to_camel(data.id, True),
|
||||||
"extension_builder_stub_short_description": data.short_description or "",
|
"extension_builder_stub_short_description": data.short_description or "",
|
||||||
"extension_builder_stub": data.id,
|
"extension_builder_stub": data.id,
|
||||||
"OwnerData": data.owner_data.name,
|
"OwnerData": data.owner_data.name,
|
||||||
@@ -328,7 +345,7 @@ def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) ->
|
|||||||
directory=extension_dir_path,
|
directory=extension_dir_path,
|
||||||
old_text=old_text,
|
old_text=old_text,
|
||||||
new_text=new_text,
|
new_text=new_text,
|
||||||
file_extensions=[".py", ".js", ".html", ".md", ".json", ".toml"],
|
file_extensions=[".py", ".js", ".vue", ".html", ".md", ".json", ".toml"],
|
||||||
)
|
)
|
||||||
|
|
||||||
_rename_files_and_dirs_in_directory(
|
_rename_files_and_dirs_in_directory(
|
||||||
@@ -406,6 +423,7 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
|||||||
"owner_data": {
|
"owner_data": {
|
||||||
"name": data.owner_data.name,
|
"name": data.owner_data.name,
|
||||||
"editable": data.owner_data.editable,
|
"editable": data.owner_data.editable,
|
||||||
|
"fields": [field.name for field in data.owner_data.fields],
|
||||||
"js_fields": [
|
"js_fields": [
|
||||||
field.field_to_js()
|
field.field_to_js()
|
||||||
for field in data.owner_data.fields
|
for field in data.owner_data.fields
|
||||||
@@ -432,6 +450,18 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
|||||||
],
|
],
|
||||||
"db_fields": [field.field_to_db() for field in data.owner_data.fields],
|
"db_fields": [field.field_to_db() for field in data.owner_data.fields],
|
||||||
"all_fields": [field.field_to_py() for field in data.owner_data.fields],
|
"all_fields": [field.field_to_py() for field in data.owner_data.fields],
|
||||||
|
"random_fields_values": [
|
||||||
|
field.field_to_random_value() for field in data.owner_data.fields
|
||||||
|
],
|
||||||
|
"public_fields": [
|
||||||
|
field.field_to_py()
|
||||||
|
for field in data.owner_data.fields
|
||||||
|
if field.name
|
||||||
|
in [
|
||||||
|
data.public_page.owner_data_fields.name,
|
||||||
|
data.public_page.owner_data_fields.description,
|
||||||
|
]
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"client_data": {
|
"client_data": {
|
||||||
"name": data.client_data.name,
|
"name": data.client_data.name,
|
||||||
@@ -457,6 +487,9 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
|||||||
],
|
],
|
||||||
"db_fields": [field.field_to_db() for field in data.client_data.fields],
|
"db_fields": [field.field_to_db() for field in data.client_data.fields],
|
||||||
"all_fields": [field.field_to_py() for field in data.client_data.fields],
|
"all_fields": [field.field_to_py() for field in data.client_data.fields],
|
||||||
|
"random_fields_values": [
|
||||||
|
field.field_to_random_value() for field in data.client_data.fields
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"settings_data": {
|
"settings_data": {
|
||||||
"enabled": data.settings_data.enabled,
|
"enabled": data.settings_data.enabled,
|
||||||
@@ -536,7 +569,7 @@ def _rename_files_and_dirs_in_directory(directory, old_text, new_text):
|
|||||||
logger.warning(f"Failed to rename directory {old_dir_path}: {e}")
|
logger.warning(f"Failed to rename directory {old_dir_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _is_excluded_dir(path):
|
def _is_excluded_dir(path: str) -> bool:
|
||||||
for excluded_dir in excluded_dirs:
|
for excluded_dir in excluded_dirs:
|
||||||
if path.startswith(excluded_dir):
|
if path.startswith(excluded_dir):
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -10,6 +12,12 @@ from lnbits.core.models import CreatePayment, Payment, PaymentState
|
|||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.fiat import get_fiat_provider
|
from lnbits.fiat import get_fiat_provider
|
||||||
|
from lnbits.fiat.base import (
|
||||||
|
FiatPaymentFailedStatus,
|
||||||
|
FiatPaymentPendingStatus,
|
||||||
|
FiatPaymentStatus,
|
||||||
|
FiatPaymentSuccessStatus,
|
||||||
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -27,6 +35,40 @@ async def handle_fiat_payment_confirmation(
|
|||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_fiat_status(
|
||||||
|
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
||||||
|
) -> FiatPaymentStatus:
|
||||||
|
if not payment.is_internal:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
if payment.success:
|
||||||
|
return FiatPaymentSuccessStatus()
|
||||||
|
if payment.failed:
|
||||||
|
return FiatPaymentFailedStatus()
|
||||||
|
|
||||||
|
if not payment.fiat_provider:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
checking_id = payment.extra.get("fiat_checking_id")
|
||||||
|
if not checking_id:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
fiat_provider = await get_fiat_provider(payment.fiat_provider)
|
||||||
|
if not fiat_provider:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
||||||
|
|
||||||
|
if skip_internal_payment_notifications:
|
||||||
|
return fiat_status
|
||||||
|
|
||||||
|
if fiat_status.success:
|
||||||
|
# notify receivers asynchronously
|
||||||
|
from lnbits.tasks import internal_invoice_queue
|
||||||
|
|
||||||
|
await internal_invoice_queue.put(payment.checking_id)
|
||||||
|
|
||||||
|
return fiat_status
|
||||||
|
|
||||||
|
|
||||||
def check_stripe_signature(
|
def check_stripe_signature(
|
||||||
payload: bytes,
|
payload: bytes,
|
||||||
sig_header: str | None,
|
sig_header: str | None,
|
||||||
@@ -70,6 +112,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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -286,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())
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount
|
|||||||
from lnbits.wallets import fake_wallet, get_funding_source
|
from lnbits.wallets import fake_wallet, get_funding_source
|
||||||
from lnbits.wallets.base import (
|
from lnbits.wallets.base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
|
PaymentFailedStatus,
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
@@ -47,7 +48,8 @@ from ..models import (
|
|||||||
PaymentState,
|
PaymentState,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from .notifications import send_payment_notification
|
from .fiat_providers import check_fiat_status
|
||||||
|
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] = {}
|
||||||
@@ -67,12 +69,14 @@ async def pay_invoice(
|
|||||||
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.")
|
||||||
|
|
||||||
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:
|
||||||
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:
|
if not wallet.can_send_payments:
|
||||||
raise PaymentError(
|
raise PaymentError(
|
||||||
"Wallet does not have permission to pay invoices.",
|
"Wallet does not have permission to pay invoices.",
|
||||||
@@ -95,10 +99,12 @@ async def pay_invoice(
|
|||||||
labels=labels,
|
labels=labels,
|
||||||
)
|
)
|
||||||
|
|
||||||
payment = await _pay_invoice(wallet.source_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
|
||||||
|
|
||||||
@@ -197,20 +203,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,
|
||||||
labels=data.labels,
|
payment_hash=data.payment_hash,
|
||||||
)
|
labels=data.labels,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
|
||||||
if data.lnurl_withdraw:
|
if data.lnurl_withdraw:
|
||||||
try:
|
try:
|
||||||
@@ -355,13 +363,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(
|
||||||
status = await payment.check_status()
|
payment: Payment, conn: Connection | None = None
|
||||||
|
) -> Payment:
|
||||||
|
status = await check_payment_status(payment)
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -610,7 +620,29 @@ async def check_transaction_status(
|
|||||||
if payment.status == PaymentState.SUCCESS.value:
|
if payment.status == PaymentState.SUCCESS.value:
|
||||||
return PaymentSuccessStatus(fee_msat=payment.fee)
|
return PaymentSuccessStatus(fee_msat=payment.fee)
|
||||||
|
|
||||||
return await payment.check_status()
|
return await check_payment_status(payment)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_payment_status(
|
||||||
|
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
||||||
|
) -> PaymentStatus:
|
||||||
|
if payment.is_internal:
|
||||||
|
if payment.success:
|
||||||
|
return PaymentSuccessStatus()
|
||||||
|
if payment.failed:
|
||||||
|
return PaymentFailedStatus()
|
||||||
|
if payment.is_in and payment.fiat_provider:
|
||||||
|
fiat_status = await check_fiat_status(
|
||||||
|
payment, skip_internal_payment_notifications
|
||||||
|
)
|
||||||
|
return PaymentStatus(paid=fiat_status.paid)
|
||||||
|
return PaymentPendingStatus()
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
if payment.is_out:
|
||||||
|
status = await funding_source.get_payment_status(payment.checking_id)
|
||||||
|
else:
|
||||||
|
status = await funding_source.get_invoice_status(payment.checking_id)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
async def get_payments_daily_stats(
|
async def get_payments_daily_stats(
|
||||||
@@ -698,6 +730,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
|
||||||
|
|
||||||
@@ -706,6 +739,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")
|
||||||
|
|
||||||
@@ -727,6 +761,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,
|
||||||
@@ -741,7 +776,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
|
||||||
@@ -814,7 +849,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
|
||||||
@@ -857,7 +893,7 @@ async def _verify_external_payment(
|
|||||||
raise PaymentError("Payment already paid.", status="success")
|
raise PaymentError("Payment already paid.", status="success")
|
||||||
|
|
||||||
# payment failed
|
# payment failed
|
||||||
status = await payment.check_status()
|
status = await check_payment_status(payment)
|
||||||
if status.failed:
|
if status.failed:
|
||||||
raise PaymentError(
|
raise PaymentError(
|
||||||
"Payment is failed node, retrying is not possible.", status="failed"
|
"Payment is failed node, retrying is not possible.", status="failed"
|
||||||
@@ -1022,3 +1058,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,11 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.crud.settings import set_settings_field
|
||||||
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.extensions import UserExtension
|
from lnbits.core.models.extensions import UserExtension
|
||||||
|
from lnbits.core.models.users import RegisterUser
|
||||||
|
from lnbits.db import Connection
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
SuperSettings,
|
SuperSettings,
|
||||||
@@ -48,37 +52,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.")
|
||||||
|
|
||||||
@@ -184,3 +194,25 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
|||||||
|
|
||||||
editable_settings = EditableSettings.from_dict(settings.dict())
|
editable_settings = EditableSettings.from_dict(settings.dict())
|
||||||
return await create_admin_settings(account.id, editable_settings.dict())
|
return await create_admin_settings(account.id, editable_settings.dict())
|
||||||
|
|
||||||
|
|
||||||
|
async def check_register_activation_settings(data: RegisterUser):
|
||||||
|
if not settings.lnbits_require_user_activation:
|
||||||
|
return None
|
||||||
|
if settings.lnbits_user_activation_by_invitation_code:
|
||||||
|
code = data.invitation_code.strip() if data.invitation_code else ""
|
||||||
|
if len(code) == 0:
|
||||||
|
raise ValueError("Invitation code cannot be empty.")
|
||||||
|
|
||||||
|
if code == settings.lnbits_register_reusable_activation_code:
|
||||||
|
return None
|
||||||
|
if code in settings.lnbits_register_one_time_activation_codes:
|
||||||
|
settings.lnbits_register_one_time_activation_codes.remove(code)
|
||||||
|
await set_settings_field(
|
||||||
|
"lnbits_register_one_time_activation_codes",
|
||||||
|
settings.lnbits_register_one_time_activation_codes,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
raise ValueError("Invalid invitation code.")
|
||||||
|
|
||||||
|
raise ValueError("No activation method provided.")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ from urllib.parse import urlparse
|
|||||||
from fastapi import APIRouter, Depends, File
|
from fastapi import APIRouter, Depends, File
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from lnbits.core.models import User
|
|
||||||
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,
|
||||||
@@ -67,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
|
||||||
|
|
||||||
|
|
||||||
@@ -77,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())
|
||||||
@@ -94,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(
|
||||||
@@ -110,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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ from lnbits.core.crud.assets import (
|
|||||||
)
|
)
|
||||||
from lnbits.core.models.assets import AssetFilters, AssetInfo, AssetUpdate
|
from lnbits.core.models.assets import AssetFilters, AssetInfo, AssetUpdate
|
||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
from lnbits.core.models.users import User
|
from lnbits.core.models.users import AccountId
|
||||||
from lnbits.core.services.assets import create_user_asset
|
from lnbits.core.services.assets import create_user_asset
|
||||||
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_id_exists,
|
||||||
optional_user_id,
|
optional_user_id,
|
||||||
parse_filters,
|
parse_filters,
|
||||||
)
|
)
|
||||||
@@ -35,10 +35,10 @@ upload_file_param = File(...)
|
|||||||
summary="Get paginated list user assets",
|
summary="Get paginated list user assets",
|
||||||
)
|
)
|
||||||
async def api_get_user_assets(
|
async def api_get_user_assets(
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
filters: Filters = Depends(parse_filters(AssetFilters)),
|
filters: Filters = Depends(parse_filters(AssetFilters)),
|
||||||
) -> Page[AssetInfo]:
|
) -> Page[AssetInfo]:
|
||||||
return await get_user_assets(user.id, filters=filters)
|
return await get_user_assets(account_id.id, filters=filters)
|
||||||
|
|
||||||
|
|
||||||
@asset_router.get(
|
@asset_router.get(
|
||||||
@@ -48,20 +48,20 @@ async def api_get_user_assets(
|
|||||||
)
|
)
|
||||||
async def api_get_asset(
|
async def api_get_asset(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
) -> AssetInfo:
|
) -> AssetInfo:
|
||||||
asset_info = await get_user_asset_info(user.id, asset_id)
|
asset_info = await get_user_asset_info(account_id.id, asset_id)
|
||||||
if not asset_info:
|
if not asset_info:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
return asset_info
|
return asset_info
|
||||||
|
|
||||||
|
|
||||||
@asset_router.get(
|
@asset_router.get(
|
||||||
"/{asset_id}/binary",
|
"/{asset_id}/data",
|
||||||
name="Get user asset binary",
|
name="Get user asset data",
|
||||||
summary="Get user asset binary data by ID",
|
summary="Get user asset data data by ID",
|
||||||
)
|
)
|
||||||
async def api_get_asset_binary(
|
async def api_get_asset_data(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
user_id: str | None = Depends(optional_user_id),
|
user_id: str | None = Depends(optional_user_id),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
@@ -120,12 +120,12 @@ async def api_get_asset_thumbnail(
|
|||||||
async def api_update_asset(
|
async def api_update_asset(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
data: AssetUpdate,
|
data: AssetUpdate,
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
) -> AssetInfo:
|
) -> AssetInfo:
|
||||||
if user.admin:
|
if account_id.is_admin_id:
|
||||||
asset_info = await get_asset_info(asset_id)
|
asset_info = await get_asset_info(asset_id)
|
||||||
else:
|
else:
|
||||||
asset_info = await get_user_asset_info(user.id, asset_id)
|
asset_info = await get_user_asset_info(account_id.id, asset_id)
|
||||||
|
|
||||||
if not asset_info:
|
if not asset_info:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
@@ -144,13 +144,13 @@ async def api_update_asset(
|
|||||||
summary="Upload user assets",
|
summary="Upload user assets",
|
||||||
)
|
)
|
||||||
async def api_upload_asset(
|
async def api_upload_asset(
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
file: UploadFile = upload_file_param,
|
file: UploadFile = upload_file_param,
|
||||||
public_asset: bool = False,
|
public_asset: bool = False,
|
||||||
) -> AssetInfo:
|
) -> AssetInfo:
|
||||||
asset = await create_user_asset(user.id, file, public_asset)
|
asset = await create_user_asset(account_id.id, file, public_asset)
|
||||||
|
|
||||||
asset_info = await get_user_asset_info(user.id, asset.id)
|
asset_info = await get_user_asset_info(account_id.id, asset.id)
|
||||||
if not asset_info:
|
if not asset_info:
|
||||||
raise ValueError("Failed to retrieve asset info after upload.")
|
raise ValueError("Failed to retrieve asset info after upload.")
|
||||||
|
|
||||||
@@ -164,11 +164,11 @@ async def api_upload_asset(
|
|||||||
)
|
)
|
||||||
async def api_delete_asset(
|
async def api_delete_asset(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
user: User = Depends(check_user_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
asset = await get_user_asset(user.id, asset_id)
|
asset = await get_user_asset(account_id.id, asset_id)
|
||||||
if not asset:
|
if not asset:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
|
||||||
await delete_user_asset(user.id, asset_id)
|
await delete_user_asset(account_id.id, asset_id)
|
||||||
return SimpleStatus(success=True, message="Asset deleted successfully.")
|
return SimpleStatus(success=True, message="Asset deleted successfully.")
|
||||||
|
|||||||
+121
-45
@@ -4,9 +4,10 @@ import json
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from time import time
|
from time import time
|
||||||
|
from typing import Annotated
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request
|
||||||
from fastapi.responses import JSONResponse, RedirectResponse
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
from fastapi_sso.sso.base import OpenID, SSOBase
|
from fastapi_sso.sso.base import OpenID, SSOBase
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -25,8 +26,16 @@ 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.core.services.users import update_user_account
|
from lnbits.core.services.users import (
|
||||||
from lnbits.decorators import access_token_payload, check_user_exists
|
check_register_activation_settings,
|
||||||
|
update_user_account,
|
||||||
|
)
|
||||||
|
from lnbits.decorators import (
|
||||||
|
access_token_payload,
|
||||||
|
check_account_exists,
|
||||||
|
check_admin,
|
||||||
|
check_user_exists,
|
||||||
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
decrypt_internal_message,
|
decrypt_internal_message,
|
||||||
@@ -80,6 +89,7 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
|||||||
account = await get_account_by_username_or_email(data.username)
|
account = await get_account_by_username_or_email(data.username)
|
||||||
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.")
|
||||||
|
|
||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@@ -88,7 +98,7 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
||||||
event = _nostr_nip98_event(request)
|
event = _nostr_nip98_event(request)
|
||||||
account = await get_account_by_pubkey(event["pubkey"])
|
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
|
||||||
if not account:
|
if not account:
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
@@ -96,6 +106,8 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
extra=UserExtra(provider="nostr"),
|
extra=UserExtra(provider="nostr"),
|
||||||
)
|
)
|
||||||
await create_user_account(account)
|
await create_user_account(account)
|
||||||
|
if not account.activated:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
|
||||||
return _auth_success_response(account.username or "", account.id, account.email)
|
return _auth_success_response(account.username or "", account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@@ -116,14 +128,71 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
|||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.post("/impersonate", description="Login via the User ID of another user")
|
||||||
|
async def impersonate_user(
|
||||||
|
data: LoginUsr,
|
||||||
|
user: User = Depends(check_admin),
|
||||||
|
cookie_access_token: Annotated[str | None, Cookie()] = None,
|
||||||
|
) -> JSONResponse:
|
||||||
|
if not cookie_access_token:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.UNAUTHORIZED, "Only cookie based impersonation is allowed."
|
||||||
|
)
|
||||||
|
if data.usr == user.id:
|
||||||
|
raise HTTPException(HTTPStatus.FORBIDDEN, "You cannot impersonate yourself.")
|
||||||
|
if settings.is_admin_user(data.usr):
|
||||||
|
# this check includes the superuser
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.FORBIDDEN, "You cannot impersonate another admin user."
|
||||||
|
)
|
||||||
|
|
||||||
|
account = await get_account(data.usr)
|
||||||
|
if not account:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
|
||||||
|
|
||||||
|
response = _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
|
response.set_cookie(
|
||||||
|
"admin_access_token", cookie_access_token, httponly=True, max_age=max_age
|
||||||
|
)
|
||||||
|
response.set_cookie("is_lnbits_user_impersonated", "true", max_age=max_age)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.delete(
|
||||||
|
"/impersonate", description="Stop impersonation and go back to admin"
|
||||||
|
)
|
||||||
|
async def stop_impersonate_user(
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
|
admin_access_token: Annotated[str | None, Cookie()] = None,
|
||||||
|
) -> JSONResponse:
|
||||||
|
if not admin_access_token:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.UNAUTHORIZED,
|
||||||
|
"No admin access token found to stop impersonation.",
|
||||||
|
)
|
||||||
|
response = JSONResponse(
|
||||||
|
{"access_token": admin_access_token, "token_type": "bearer"}
|
||||||
|
)
|
||||||
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
|
response.set_cookie(
|
||||||
|
"cookie_access_token", admin_access_token, httponly=True, max_age=max_age
|
||||||
|
)
|
||||||
|
response.delete_cookie("admin_access_token")
|
||||||
|
response.delete_cookie("is_access_token_expired")
|
||||||
|
response.delete_cookie("is_lnbits_user_impersonated")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@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:
|
||||||
@@ -136,7 +205,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")
|
||||||
@@ -144,13 +213,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)
|
||||||
@@ -175,33 +244,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.")
|
||||||
@@ -218,18 +284,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.")
|
||||||
@@ -299,12 +363,14 @@ async def register(data: RegisterUser) -> JSONResponse:
|
|||||||
if not is_valid_username(data.username):
|
if not is_valid_username(data.username):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
||||||
|
|
||||||
if await get_account_by_username(data.username):
|
if await get_account_by_username(data.username, active_only=False):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||||
|
|
||||||
if data.email and not is_valid_email_address(data.email):
|
if data.email and not is_valid_email_address(data.email):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||||
|
|
||||||
|
await check_register_activation_settings(data)
|
||||||
|
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
email=data.email,
|
email=data.email,
|
||||||
@@ -318,24 +384,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)
|
||||||
@@ -344,23 +406,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:
|
||||||
@@ -417,17 +475,13 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
|||||||
return _auth_success_response(account.username, user_id, account.email)
|
return _auth_success_response(account.username, user_id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/update")
|
@auth_router.patch("")
|
||||||
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.")
|
||||||
|
|
||||||
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:
|
||||||
@@ -437,10 +491,32 @@ async def update(
|
|||||||
return await get_user_from_account(account)
|
return await get_user_from_account(account)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.patch("/ui")
|
||||||
|
async def update_ui_customization(
|
||||||
|
req: Request, account: Account = Depends(check_account_exists)
|
||||||
|
) -> Account:
|
||||||
|
ui_customization = await req.json()
|
||||||
|
account.ui_customization = {**(account.ui_customization or {}), **ui_customization}
|
||||||
|
|
||||||
|
if len(account.ui_customization or {}) > 1000 * 1024:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST, "UI customization too large. Drop some fields."
|
||||||
|
)
|
||||||
|
|
||||||
|
await update_user_account(account)
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/first_install")
|
@auth_router.put("/first_install")
|
||||||
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||||
if not settings.first_install:
|
if not settings.first_install:
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
|
||||||
|
if settings.first_install_token:
|
||||||
|
if not data.first_install_token:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.")
|
||||||
|
if settings.first_install_token != data.first_install_token:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid first_install_token.")
|
||||||
|
|
||||||
account = await get_account(settings.super_user)
|
account = await get_account(settings.super_user)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
||||||
@@ -459,7 +535,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
|||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||||
|
|
||||||
redirect_path = "/wallet"
|
redirect_path = "/wallet"
|
||||||
account = await get_account_by_email(email)
|
account = await get_account_by_email(email, active_only=False)
|
||||||
|
|
||||||
if verified_user_id:
|
if verified_user_id:
|
||||||
if account:
|
if account:
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ from lnbits.core.crud.payments import (
|
|||||||
from lnbits.core.models.misc import SimpleStatus
|
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_fiat_status,
|
||||||
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 +39,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 +61,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 +71,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 check_fiat_status(payment)
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -76,7 +111,7 @@ async def _handle_stripe_checkout_session_completed(event: dict):
|
|||||||
payment = await get_standalone_payment(payment_hash)
|
payment = await get_standalone_payment(payment_hash)
|
||||||
if not payment:
|
if not payment:
|
||||||
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
|
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
|
||||||
await payment.check_fiat_status()
|
await check_fiat_status(payment)
|
||||||
|
|
||||||
|
|
||||||
async def _handle_stripe_subscription_invoice_paid(event: dict):
|
async def _handle_stripe_subscription_invoice_paid(event: dict):
|
||||||
@@ -121,7 +156,7 @@ async def _handle_stripe_subscription_invoice_paid(event: dict):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await payment.check_fiat_status()
|
await check_fiat_status(payment)
|
||||||
|
|
||||||
|
|
||||||
async def _get_stripe_subscription_payment_options(
|
async def _get_stripe_subscription_payment_options(
|
||||||
@@ -132,7 +167,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 +178,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 check_fiat_status(payment)
|
||||||
|
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 check_fiat_status(payment)
|
||||||
|
|
||||||
|
|
||||||
|
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,9 +40,11 @@ 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 lnbits.settings import settings
|
||||||
|
|
||||||
@@ -142,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."
|
||||||
)
|
)
|
||||||
@@ -163,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(
|
||||||
@@ -177,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.")
|
||||||
@@ -219,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."
|
||||||
@@ -376,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(
|
||||||
@@ -422,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
|
||||||
@@ -435,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:
|
||||||
@@ -456,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(
|
||||||
@@ -498,11 +514,22 @@ async def delete_extension_db(ext_id: str):
|
|||||||
|
|
||||||
# TODO: create a response model for this
|
# TODO: create a response model for this
|
||||||
@extension_router.get("/all")
|
@extension_router.get("/all")
|
||||||
async def extensions(user: User = Depends(check_user_exists)):
|
async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||||
installed_exts: list[InstallableExtension] = await get_installed_extensions()
|
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]
|
installed_exts_ids = [e.id for e in installed_exts]
|
||||||
|
|
||||||
installable_exts = await InstallableExtension.get_installable_extensions()
|
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_ids = [e.id for e in installable_exts]
|
||||||
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
|
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
|
||||||
|
|
||||||
@@ -510,7 +537,7 @@ async def extensions(user: User = Depends(check_user_exists)):
|
|||||||
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
|
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
|
||||||
if installed_ext and installed_ext.meta:
|
if installed_ext and installed_ext.meta:
|
||||||
installed_release = installed_ext.meta.installed_release
|
installed_release = installed_ext.meta.installed_release
|
||||||
if installed_ext.meta.pay_to_enable and not user.admin:
|
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
|
# not a security leak, but better not to share the wallet id
|
||||||
installed_ext.meta.pay_to_enable.wallet = None
|
installed_ext.meta.pay_to_enable.wallet = None
|
||||||
pay_to_enable = installed_ext.meta.pay_to_enable
|
pay_to_enable = installed_ext.meta.pay_to_enable
|
||||||
@@ -528,10 +555,6 @@ async def extensions(user: User = Depends(check_user_exists)):
|
|||||||
e.short_description = installed_ext.short_description
|
e.short_description = installed_ext.short_description
|
||||||
e.icon = installed_ext.icon
|
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 = [
|
extension_data = [
|
||||||
{
|
{
|
||||||
"id": ext.id,
|
"id": ext.id,
|
||||||
@@ -573,3 +596,49 @@ async def extensions(user: User = Depends(check_user_exists)):
|
|||||||
for ext in installable_exts
|
for ext in installable_exts
|
||||||
]
|
]
|
||||||
return extension_data
|
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)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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,
|
||||||
@@ -16,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,
|
||||||
@@ -26,9 +26,9 @@ 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_extension_builder,
|
check_extension_builder,
|
||||||
check_user_exists,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
@@ -84,9 +84,9 @@ async def api_build_extension(data: ExtensionData) -> FileResponse:
|
|||||||
)
|
)
|
||||||
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
|
||||||
@@ -110,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
|
||||||
@@ -128,10 +128,10 @@ async def api_deploy_extension(
|
|||||||
)
|
)
|
||||||
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:
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
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
|
||||||
@@ -25,12 +23,7 @@ from lnbits.decorators import (
|
|||||||
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 ...utils.exchange_rates import allowed_currencies
|
from ..crud import get_user
|
||||||
from ..crud import (
|
|
||||||
create_wallet,
|
|
||||||
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
|
||||||
@@ -42,118 +35,51 @@ async def favicon():
|
|||||||
return RedirectResponse(settings.lnbits_qr_logo)
|
return RedirectResponse(settings.lnbits_qr_logo)
|
||||||
|
|
||||||
|
|
||||||
@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("/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(
|
@generic_router.get(
|
||||||
"/extensions/builder/preview/{ext_id}",
|
"/extensions/builder/preview/{ext_id}/{resource}",
|
||||||
name="extensions builder",
|
name="extensions builder",
|
||||||
dependencies=[Depends(check_extension_builder)],
|
dependencies=[Depends(check_extension_builder)],
|
||||||
)
|
)
|
||||||
async def extensions_builder_preview(
|
async def extensions_builder_preview(
|
||||||
request: Request,
|
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
|
resource: str | None = None,
|
||||||
page_name: str | None = None,
|
page_name: str | None = None,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
) -> HTMLResponse:
|
) -> FileResponse:
|
||||||
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"
|
|
||||||
if page_name == "public_page":
|
|
||||||
html_file_name = "public_page.html"
|
|
||||||
|
|
||||||
html_file_path = Path(
|
file_name = "index"
|
||||||
|
if page_name == "public_page":
|
||||||
|
file_name = "public_page"
|
||||||
|
|
||||||
|
resource_path = Path(
|
||||||
|
settings.extension_builder_working_dir_path,
|
||||||
"extension_builder_stub",
|
"extension_builder_stub",
|
||||||
ext_id,
|
ext_id,
|
||||||
working_dir_name,
|
working_dir_name,
|
||||||
ext_id,
|
ext_id,
|
||||||
"templates",
|
"static",
|
||||||
ext_id,
|
|
||||||
html_file_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
html_file_full_path = Path(
|
file_ext = ".vue" if resource == "template" else ".js"
|
||||||
settings.extension_builder_working_dir_path, html_file_path
|
file_full_path = Path(resource_path, file_name + file_ext)
|
||||||
)
|
|
||||||
|
|
||||||
if not html_file_full_path.is_file():
|
if not file_full_path.is_file():
|
||||||
return template_renderer().TemplateResponse(
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
||||||
request,
|
|
||||||
"error.html",
|
|
||||||
{
|
|
||||||
"err": f"Extension {ext_id} not found",
|
|
||||||
"message": "Please 'Refresh Preview' first.",
|
|
||||||
},
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
html_file_path.as_posix(),
|
|
||||||
{
|
|
||||||
"user": user.json(),
|
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
response = FileResponse(file_full_path.absolute().as_posix())
|
||||||
response.headers["Content-Security-Policy"] = (
|
response.headers["Content-Security-Policy"] = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
"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
|
||||||
|
|
||||||
|
|
||||||
@@ -254,6 +180,8 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
|||||||
|
|
||||||
|
|
||||||
@generic_router.get("/payments")
|
@generic_router.get("/payments")
|
||||||
|
@generic_router.get("/wallet")
|
||||||
|
@generic_router.get("/wallet/{wallet_id}")
|
||||||
@generic_router.get("/wallets")
|
@generic_router.get("/wallets")
|
||||||
@generic_router.get("/account")
|
@generic_router.get("/account")
|
||||||
@generic_router.get("/extensions")
|
@generic_router.get("/extensions")
|
||||||
@@ -264,6 +192,9 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
|||||||
@generic_router.get(
|
@generic_router.get(
|
||||||
"/extensions/builder", dependencies=[Depends(check_extension_builder)]
|
"/extensions/builder", dependencies=[Depends(check_extension_builder)]
|
||||||
)
|
)
|
||||||
|
@generic_router.get(
|
||||||
|
"/extensions/builder/preview", dependencies=[Depends(check_extension_builder)]
|
||||||
|
)
|
||||||
async def index(
|
async def index(
|
||||||
request: Request, user: User = Depends(check_user_exists)
|
request: Request, user: User = Depends(check_user_exists)
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
@@ -280,7 +211,7 @@ async def index(
|
|||||||
@generic_router.get("/node/public")
|
@generic_router.get("/node/public")
|
||||||
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
||||||
async def index_public(request: Request) -> HTMLResponse:
|
async def index_public(request: Request) -> HTMLResponse:
|
||||||
return template_renderer().TemplateResponse(request, "index_public.html")
|
return template_renderer().TemplateResponse(request, "index.html", {"public": True})
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/uuidv4/{hex_value}")
|
@generic_router.get("/uuidv4/{hex_value}")
|
||||||
@@ -341,7 +272,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"
|
|
||||||
|
|||||||
@@ -24,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
|
||||||
@@ -48,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
|
||||||
@@ -64,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
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from lnbits.core.crud.payments import (
|
|||||||
update_payment,
|
update_payment,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import get_account
|
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,
|
||||||
@@ -35,14 +36,16 @@ from lnbits.core.models import (
|
|||||||
SimpleStatus,
|
SimpleStatus,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.payments import UpdatePaymentLabels
|
from lnbits.core.models.payments import UpdatePaymentLabels
|
||||||
from lnbits.core.models.users import User
|
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,
|
||||||
@@ -67,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"])
|
||||||
@@ -82,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,
|
||||||
@@ -101,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -118,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)
|
||||||
|
|
||||||
@@ -138,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)
|
||||||
|
|
||||||
@@ -157,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -178,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(
|
||||||
@@ -202,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(
|
||||||
@@ -237,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,
|
||||||
@@ -268,9 +280,8 @@ async def api_payments_create(
|
|||||||
async def api_update_payment_labels(
|
async def api_update_payment_labels(
|
||||||
payment_hash: str,
|
payment_hash: str,
|
||||||
data: UpdatePaymentLabels,
|
data: UpdatePaymentLabels,
|
||||||
key_type: WalletTypeInfo = Depends(require_admin_key),
|
key_type: BaseWalletTypeInfo = Depends(require_base_admin_key),
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
|
|
||||||
payment = await get_standalone_payment(payment_hash, wallet_id=key_type.wallet.id)
|
payment = await get_standalone_payment(payment_hash, wallet_id=key_type.wallet.id)
|
||||||
if payment is None:
|
if payment is None:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Payment does not exist.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Payment does not exist.")
|
||||||
@@ -326,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}
|
||||||
@@ -335,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,8 @@ from lnbits.core.crud import (
|
|||||||
update_admin_settings,
|
update_admin_settings,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
|
||||||
|
from lnbits.core.crud.wallets import delete_wallet_by_id
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccountFilters,
|
AccountFilters,
|
||||||
AccountOverview,
|
AccountOverview,
|
||||||
@@ -72,7 +74,7 @@ async def api_get_users(
|
|||||||
summary="Get user by Id",
|
summary="Get user by Id",
|
||||||
)
|
)
|
||||||
async def api_get_user(user_id: str) -> User:
|
async def api_get_user(user_id: str) -> User:
|
||||||
user = await get_user(user_id)
|
user = await get_user(user_id, active_only=False)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||||
return user
|
return user
|
||||||
@@ -115,12 +117,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 +156,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 +168,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.",
|
||||||
@@ -199,7 +198,7 @@ async def api_users_reset_password(user_id: str) -> str:
|
|||||||
return f"reset_key_{reset_key_b64}"
|
return f"reset_key_{reset_key_b64}"
|
||||||
|
|
||||||
|
|
||||||
@users_router.get(
|
@users_router.put(
|
||||||
"/user/{user_id}/admin",
|
"/user/{user_id}/admin",
|
||||||
dependencies=[Depends(check_super_user)],
|
dependencies=[Depends(check_super_user)],
|
||||||
name="Give or revoke admin permsisions to a user",
|
name="Give or revoke admin permsisions to a user",
|
||||||
@@ -222,9 +221,46 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@users_router.put(
|
||||||
|
"/user/{user_id}/activate",
|
||||||
|
name="Activate or deactivate a user",
|
||||||
|
)
|
||||||
|
async def api_users_toggle_activated(
|
||||||
|
user_id: str, admin_account: Account = Depends(check_admin)
|
||||||
|
) -> SimpleStatus:
|
||||||
|
if user_id == settings.super_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Cannot deactivate super user.",
|
||||||
|
)
|
||||||
|
if user_id == admin_account.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="You cannot deactivate yourself.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings.is_admin_user(user_id):
|
||||||
|
settings.lnbits_admin_users.remove(user_id)
|
||||||
|
|
||||||
|
user_account = await get_account(user_id, active_only=False)
|
||||||
|
if not user_account:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="User not found.",
|
||||||
|
)
|
||||||
|
user_account.activated = not user_account.activated
|
||||||
|
await update_account(user_account)
|
||||||
|
await clear_user_id_cache(user_id)
|
||||||
|
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True,
|
||||||
|
message=f"User {'activated' if user_account.activated else 'deactivated'}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@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 +283,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 +331,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 +340,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.",
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ from fastapi import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import (
|
from lnbits.core.crud.wallets import (
|
||||||
|
clear_wallet_cache,
|
||||||
create_wallet,
|
create_wallet,
|
||||||
get_wallets_paginated,
|
get_wallets_paginated,
|
||||||
)
|
)
|
||||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
|
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.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
|
from lnbits.core.models.users import Account, AccountId
|
||||||
from lnbits.core.models.wallets import (
|
from lnbits.core.models.wallets import (
|
||||||
WalletsFilters,
|
WalletsFilters,
|
||||||
WalletSharePermission,
|
WalletSharePermission,
|
||||||
@@ -29,7 +31,8 @@ from lnbits.core.services.wallets import (
|
|||||||
)
|
)
|
||||||
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,
|
||||||
@@ -65,11 +68,11 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,7 +88,7 @@ async def api_invite_wallet_share(
|
|||||||
|
|
||||||
@wallet_router.delete("/share/invite/{share_request_id}")
|
@wallet_router.delete("/share/invite/{share_request_id}")
|
||||||
async def api_reject_wallet_invitation(
|
async def api_reject_wallet_invitation(
|
||||||
share_request_id: str, invited_user: User = Depends(check_user_exists)
|
share_request_id: str, invited_user: Account = Depends(check_account_exists)
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
await reject_wallet_invitation(invited_user.id, share_request_id)
|
await reject_wallet_invitation(invited_user.id, share_request_id)
|
||||||
return SimpleStatus(success=True, message="Invitation rejected.")
|
return SimpleStatus(success=True, message="Invitation rejected.")
|
||||||
@@ -124,12 +127,15 @@ 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")
|
||||||
|
|
||||||
|
clear_wallet_cache(wallet)
|
||||||
|
|
||||||
wallet.adminkey = uuid4().hex
|
wallet.adminkey = uuid4().hex
|
||||||
wallet.inkey = uuid4().hex
|
wallet.inkey = uuid4().hex
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
@@ -175,10 +181,10 @@ async def api_update_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(
|
||||||
@@ -189,11 +195,14 @@ 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:
|
||||||
if data.wallet_type == WalletType.LIGHTNING:
|
|
||||||
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 data.wallet_type == WalletType.LIGHTNING_SHARED:
|
||||||
if not data.shared_wallet_id:
|
if not data.shared_wallet_id:
|
||||||
@@ -202,11 +211,9 @@ async def api_create_wallet(
|
|||||||
"Shared wallet ID is required for shared wallets.",
|
"Shared wallet ID is required for shared wallets.",
|
||||||
)
|
)
|
||||||
return await create_lightning_shared_wallet(
|
return await create_lightning_shared_wallet(
|
||||||
user_id=key_info.wallet.user,
|
user_id=account_id.id,
|
||||||
source_wallet_id=data.shared_wallet_id,
|
source_wallet_id=data.shared_wallet_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
raise HTTPException(
|
# default WalletType.LIGHTNING:
|
||||||
HTTPStatus.BAD_REQUEST,
|
return await create_wallet(user_id=account_id.id, wallet_name=data.name)
|
||||||
f"Unknown wallet type: {data.wallet_type}.",
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+36
-17
@@ -235,11 +235,14 @@ class Connection(Compat):
|
|||||||
table_name: if provided some optimisations can be applied.
|
table_name: if provided some optimisations can be applied.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if table_name and not _valid_sql_name(table_name):
|
|
||||||
raise ValueError(f"Invalid table name: '{table_name}'.")
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -334,7 +337,8 @@ class Database(Compat):
|
|||||||
f = "%Y-%m-%d %H:%M:%S.%f"
|
f = "%Y-%m-%d %H:%M:%S.%f"
|
||||||
if "." not in value:
|
if "." not in value:
|
||||||
f = "%Y-%m-%d %H:%M:%S"
|
f = "%Y-%m-%d %H:%M:%S"
|
||||||
return datetime.strptime(value, f)
|
# Parse and add UTC timezone info
|
||||||
|
return datetime.strptime(value, f).replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
dbapi_connection.run_async(
|
dbapi_connection.run_async(
|
||||||
lambda connection: connection.set_type_codec(
|
lambda connection: connection.set_type_codec(
|
||||||
@@ -491,6 +495,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
|
||||||
@@ -518,7 +523,7 @@ 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 = {}
|
||||||
if op in {Operator.EVERY, Operator.ANY}:
|
if op in {Operator.EVERY, Operator.ANY, Operator.INCLUDE, Operator.EXCLUDE}:
|
||||||
raw_values = [v for rv in raw_values for v in rv.split(",")]
|
raw_values = [v for rv in raw_values for v in rv.split(",")]
|
||||||
|
|
||||||
for index, raw_value in enumerate(raw_values):
|
for index, raw_value in enumerate(raw_values):
|
||||||
@@ -533,20 +538,23 @@ 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"{clean_key} {self.op.as_sql} {placeholder}")
|
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:
|
||||||
stmt.append(f"{clean_key} {self.op.as_sql} :{key}")
|
stmt.append(f"{prefix}{self.field} {self.op.as_sql} :{key}")
|
||||||
|
|
||||||
if self.op == Operator.EVERY:
|
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)
|
statement = " AND ".join(stmt)
|
||||||
else:
|
else:
|
||||||
statement = " OR ".join(stmt)
|
statement = " OR ".join(stmt)
|
||||||
|
|
||||||
return f"({statement})"
|
return f"({statement})"
|
||||||
|
|
||||||
|
|
||||||
@@ -563,13 +571,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")
|
||||||
@@ -584,8 +593,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
|
||||||
@@ -611,7 +620,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:
|
||||||
@@ -631,6 +641,11 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
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):
|
class DbJsonEncoder(json.JSONEncoder):
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
@@ -742,7 +757,11 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
|
|||||||
if DB_TYPE == SQLITE:
|
if DB_TYPE == SQLITE:
|
||||||
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
||||||
else:
|
else:
|
||||||
_dict[key] = value
|
# Ensure PostgreSQL datetime values have timezone info
|
||||||
|
if isinstance(value, datetime) and value.tzinfo is None:
|
||||||
|
_dict[key] = value.replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
_dict[key] = value
|
||||||
continue
|
continue
|
||||||
if issubclass(type_, BaseModel):
|
if issubclass(type_, BaseModel):
|
||||||
_dict[key] = dict_to_submodel(type_, value)
|
_dict[key] = dict_to_submodel(type_, value)
|
||||||
|
|||||||
+222
-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,96 @@ 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,
|
||||||
|
)
|
||||||
|
cache.set(f"auth:user:cache_key:{sha256s(account.id)}", cache_key)
|
||||||
|
|
||||||
|
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 +350,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 +439,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 +458,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 +476,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.")
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def register_exception_handlers(app: FastAPI): # noqa: C901
|
|||||||
logger.error(f"RequestValidationError: {exc!s}")
|
logger.error(f"RequestValidationError: {exc!s}")
|
||||||
return render_html_error(request, exc) or JSONResponse(
|
return render_html_error(request, exc) or JSONResponse(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
content={"detail": str(exc)},
|
content={"detail": [dict(e) for e in exc.errors()]},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.exception_handler(HTTPException)
|
@app.exception_handler(HTTPException)
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
+18
-46
@@ -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,53 +68,13 @@ 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 = {
|
# used in base.html
|
||||||
"AD_SPACE": settings.lnbits_ad_space,
|
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
|
||||||
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
t.env.globals["LNBITS_APPLE_TOUCH_ICON"] = settings.lnbits_apple_touch_icon
|
||||||
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
t.env.globals["SETTINGS"] = settings.to_public().dict(by_alias=True)
|
||||||
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
t.env.globals["CURRENCIES"] = list(currencies.keys())
|
||||||
"HIDE_API": settings.lnbits_hide_api,
|
|
||||||
"SITE_TITLE": settings.lnbits_site_title,
|
|
||||||
"SITE_TAGLINE": settings.lnbits_site_tagline,
|
|
||||||
"SITE_DESCRIPTION": settings.lnbits_site_description,
|
|
||||||
"LNBITS_ADMIN_UI": settings.lnbits_admin_ui,
|
|
||||||
"LNBITS_AUDIT_ENABLED": settings.lnbits_audit_enabled,
|
|
||||||
"LNBITS_AUTH_METHODS": settings.auth_allowed_methods,
|
|
||||||
"LNBITS_AUTH_KEYCLOAK_ORG": settings.keycloak_client_custom_org,
|
|
||||||
"LNBITS_AUTH_KEYCLOAK_ICON": settings.keycloak_client_custom_icon,
|
|
||||||
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
|
|
||||||
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
|
|
||||||
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
|
||||||
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
|
||||||
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
|
||||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
|
||||||
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
|
||||||
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
|
||||||
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
|
||||||
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
|
||||||
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
|
||||||
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
|
|
||||||
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
|
||||||
"LNBITS_VERSION": settings.version,
|
|
||||||
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
|
||||||
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
|
||||||
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
|
|
||||||
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
|
|
||||||
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
|
||||||
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
|
||||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
|
||||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
|
||||||
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
|
||||||
"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,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
|
||||||
for key, value in window_settings.items():
|
|
||||||
t.env.globals[key] = value
|
|
||||||
|
|
||||||
if settings.bundle_assets:
|
if settings.bundle_assets:
|
||||||
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
||||||
@@ -127,6 +88,9 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
||||||
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
||||||
|
|
||||||
|
# backwards compatibility for extensions (tpos)
|
||||||
|
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
|
||||||
|
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
@@ -384,6 +348,14 @@ def camel_to_snake(name: str) -> str:
|
|||||||
return name.lower()
|
return name.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def snake_to_camel(name: str, capitalize_first: bool | None = False) -> str:
|
||||||
|
components = name.split("_")
|
||||||
|
camel = components[0] + "".join(x.capitalize() for x in components[1:])
|
||||||
|
if capitalize_first:
|
||||||
|
camel = camel[0].upper() + camel[1:]
|
||||||
|
return camel
|
||||||
|
|
||||||
|
|
||||||
def is_camel_case(v: str) -> bool:
|
def is_camel_case(v: str) -> bool:
|
||||||
return re.match(r"^[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*$", v) is not None
|
return re.match(r"^[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*$", v) is not None
|
||||||
|
|
||||||
|
|||||||
+173
-4
@@ -41,6 +41,14 @@ class UsersSettings(LNbitsSettings):
|
|||||||
lnbits_admin_users: list[str] = Field(default=[])
|
lnbits_admin_users: list[str] = Field(default=[])
|
||||||
lnbits_allowed_users: list[str] = Field(default=[])
|
lnbits_allowed_users: list[str] = Field(default=[])
|
||||||
lnbits_allow_new_accounts: bool = Field(default=True)
|
lnbits_allow_new_accounts: bool = Field(default=True)
|
||||||
|
lnbits_require_user_activation: bool = Field(default=False)
|
||||||
|
|
||||||
|
lnbits_user_activation_by_email: bool = Field(default=False)
|
||||||
|
lnbits_user_activation_by_payment: bool = Field(default=False)
|
||||||
|
lnbits_user_activation_by_invitation_code: bool = Field(default=False)
|
||||||
|
|
||||||
|
lnbits_register_reusable_activation_code: str = Field(default="")
|
||||||
|
lnbits_register_one_time_activation_codes: list[str] = Field(default=[])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def new_accounts_allowed(self) -> bool:
|
def new_accounts_allowed(self) -> bool:
|
||||||
@@ -52,6 +60,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"
|
||||||
@@ -245,6 +260,11 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
)
|
)
|
||||||
lnbits_show_home_page_elements: bool = Field(default=True)
|
lnbits_show_home_page_elements: bool = Field(default=True)
|
||||||
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
||||||
|
|
||||||
|
lnbits_wallet_featured_button_label: str | None = Field(default=None)
|
||||||
|
lnbits_wallet_featured_button_url: str | None = Field(default=None)
|
||||||
|
lnbits_wallet_featured_button_icon: str | None = Field(default=None)
|
||||||
|
|
||||||
lnbits_custom_badge: str | None = Field(default=None)
|
lnbits_custom_badge: str | None = Field(default=None)
|
||||||
lnbits_custom_badge_color: str = Field(default="warning")
|
lnbits_custom_badge_color: str = Field(default="warning")
|
||||||
lnbits_theme_options: list[str] = Field(
|
lnbits_theme_options: list[str] = Field(
|
||||||
@@ -270,6 +290,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")
|
||||||
@@ -298,6 +319,10 @@ class AssetSettings(LNbitsSettings):
|
|||||||
"heic",
|
"heic",
|
||||||
"heif",
|
"heif",
|
||||||
"heics",
|
"heics",
|
||||||
|
"text/plain",
|
||||||
|
"text/json" "text/xml",
|
||||||
|
"application/json",
|
||||||
|
"application/pdf",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
lnbits_asset_thumbnail_width: int = Field(default=128, ge=0)
|
lnbits_asset_thumbnail_width: int = Field(default=128, ge=0)
|
||||||
@@ -307,6 +332,12 @@ class AssetSettings(LNbitsSettings):
|
|||||||
lnbits_max_assets_per_user: int = Field(default=1, ge=0)
|
lnbits_max_assets_per_user: int = Field(default=1, ge=0)
|
||||||
lnbits_assets_no_limit_users: list[str] = Field(default=[])
|
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):
|
||||||
lnbits_reserve_fee_min: int = Field(default=2000, ge=0)
|
lnbits_reserve_fee_min: int = Field(default=2000, ge=0)
|
||||||
@@ -600,7 +631,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)
|
||||||
@@ -641,6 +671,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)
|
||||||
|
|
||||||
@@ -676,7 +720,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.
|
||||||
@@ -685,7 +729,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]:
|
||||||
@@ -699,7 +744,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:
|
||||||
@@ -756,6 +806,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
|
||||||
@@ -940,6 +991,7 @@ class EnvSettings(LNbitsSettings):
|
|||||||
enable_log_to_file: bool = Field(default=True)
|
enable_log_to_file: bool = Field(default=True)
|
||||||
log_rotation: str = Field(default="100 MB")
|
log_rotation: str = Field(default="100 MB")
|
||||||
log_retention: str = Field(default="3 months")
|
log_retention: str = Field(default="3 months")
|
||||||
|
first_install_token: str | None = Field(default=None)
|
||||||
|
|
||||||
cleanup_wallets_days: int = Field(default=90, ge=0)
|
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
@@ -1088,6 +1140,123 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
|||||||
and ext_id in self.lnbits_all_extensions_ids
|
and ext_id in self.lnbits_all_extensions_ids
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_public(self) -> PublicSettings:
|
||||||
|
return PublicSettings.from_settings(self)
|
||||||
|
|
||||||
|
|
||||||
|
class PublicSettings(BaseModel):
|
||||||
|
"""
|
||||||
|
PublicSettings is used for templating and public information.
|
||||||
|
WARNING: do not expose private information, PublicSettings is exposed publicly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
allow_register: bool = Field(alias="allowRegister")
|
||||||
|
qr_logo: str = Field(alias="qrLogo")
|
||||||
|
site_title: str = Field(alias="siteTitle")
|
||||||
|
site_tagline: str = Field(alias="siteTagline")
|
||||||
|
site_description: str | None = Field(alias="siteDescription")
|
||||||
|
auth_methods: list[str] = Field(alias="authMethods")
|
||||||
|
keycloak_org: str | None = Field(alias="keycloakOrg")
|
||||||
|
keycloak_icon: str | None = Field(alias="keycloakIcon")
|
||||||
|
has_holdinvoice: bool = Field(alias="hasHoldinvoice")
|
||||||
|
has_nodemanager: bool = Field(alias="hasNodemanager")
|
||||||
|
show_nodemanager: bool = Field(alias="showNodemanager")
|
||||||
|
custom_logo: str | None = Field(alias="customLogo")
|
||||||
|
show_voidwallet: bool = Field(alias="showVoidwallet")
|
||||||
|
version: str = Field(alias="version")
|
||||||
|
show_home_page_elements: bool = Field(alias="showHomePageElements")
|
||||||
|
hide_api: bool = Field(alias="hideApi")
|
||||||
|
cache_key: str = Field(alias="cacheKey")
|
||||||
|
webpush_pubkey: str | None = Field(alias="webpushPubkey")
|
||||||
|
show_extensions: bool = Field(alias="showExtensions")
|
||||||
|
show_audit: bool = Field(alias="showAudit")
|
||||||
|
show_admin: bool = Field(alias="showAdmin")
|
||||||
|
ad_space: list[list[str]] = Field(alias="adSpace")
|
||||||
|
ad_space_title: str = Field(alias="adSpaceTitle")
|
||||||
|
show_ad_space: bool = Field(alias="showAdSpace")
|
||||||
|
custom_image: str | None = Field(alias="customImage")
|
||||||
|
custom_badge: str | None = Field(alias="customBadge")
|
||||||
|
custom_badge_color: str | None = Field(alias="customBadgeColor")
|
||||||
|
service_fee: float = Field(alias="serviceFee")
|
||||||
|
service_fee_max: int = Field(alias="serviceFeeMax")
|
||||||
|
service_fee_wallet: str | None = Field(alias="serviceFeeWallet")
|
||||||
|
theme_options: list[str] = Field(alias="themeOptions")
|
||||||
|
default_reaction: str = Field(alias="defaultReaction")
|
||||||
|
default_theme: str = Field(alias="defaultTheme")
|
||||||
|
default_border: str = Field(alias="defaultBorder")
|
||||||
|
default_bgimage: str | None = Field(alias="defaultBgimage")
|
||||||
|
default_gradient: bool = Field(alias="defaultGradient")
|
||||||
|
denomination: str | None = Field()
|
||||||
|
extensions: list[str] = Field()
|
||||||
|
allowed_currencies: list[str] = Field(alias="allowedCurrencies")
|
||||||
|
extensions_reviews_url: str = Field(alias="extensionsReviewsUrl")
|
||||||
|
ext_builder: bool = Field(alias="extBuilder")
|
||||||
|
nostr_configured: bool = Field(alias="nostrConfigured")
|
||||||
|
telegram_configured: bool = Field(alias="telegramConfigured")
|
||||||
|
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
|
||||||
|
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
|
||||||
|
wallet_featured_button_icon: str | None = Field(alias="walletFeaturedButtonIcon")
|
||||||
|
lnbits_user_activation_by_email: bool = Field(alias="userActivationByEmail")
|
||||||
|
lnbits_user_activation_by_payment: bool = Field(alias="userActivationByPayment")
|
||||||
|
lnbits_user_activation_by_invitation_code: bool = Field(
|
||||||
|
alias="userActivationByInvitationCode"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_settings(cls, settings: Settings):
|
||||||
|
ads = [x.split(";") for x in settings.lnbits_ad_space.split(",")]
|
||||||
|
return cls(
|
||||||
|
adSpace=ads,
|
||||||
|
adSpaceTitle=settings.lnbits_ad_space_title,
|
||||||
|
showAdSpace=settings.lnbits_ad_space_enabled and len(ads) > 0,
|
||||||
|
allowRegister=settings.new_accounts_allowed,
|
||||||
|
qrLogo=settings.lnbits_qr_logo,
|
||||||
|
siteDescription=settings.lnbits_site_description,
|
||||||
|
siteTitle=settings.lnbits_site_title,
|
||||||
|
siteTagline=settings.lnbits_site_tagline,
|
||||||
|
authMethods=settings.auth_allowed_methods,
|
||||||
|
keycloakOrg=settings.keycloak_client_custom_org,
|
||||||
|
keycloakIcon=settings.keycloak_client_custom_icon,
|
||||||
|
hasHoldinvoice=settings.has_holdinvoice,
|
||||||
|
hasNodemanager=settings.has_nodemanager,
|
||||||
|
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
|
||||||
|
customLogo=settings.lnbits_custom_logo,
|
||||||
|
showVoidwallet=settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||||
|
version=settings.version,
|
||||||
|
showHomePageElements=settings.lnbits_show_home_page_elements,
|
||||||
|
hideApi=settings.lnbits_hide_api,
|
||||||
|
cacheKey=str(settings.server_startup_time),
|
||||||
|
webpushPubkey=settings.lnbits_webpush_pubkey,
|
||||||
|
showExtensions=not settings.lnbits_extensions_deactivate_all,
|
||||||
|
showAudit=settings.lnbits_audit_enabled,
|
||||||
|
showAdmin=settings.lnbits_admin_ui,
|
||||||
|
customImage=settings.lnbits_custom_image,
|
||||||
|
customBadge=settings.lnbits_custom_badge,
|
||||||
|
customBadgeColor=settings.lnbits_custom_badge_color,
|
||||||
|
serviceFee=settings.lnbits_service_fee,
|
||||||
|
serviceFeeMax=settings.lnbits_service_fee_max,
|
||||||
|
serviceFeeWallet=settings.lnbits_service_fee_wallet,
|
||||||
|
themeOptions=settings.lnbits_theme_options,
|
||||||
|
defaultReaction=settings.lnbits_default_reaction,
|
||||||
|
defaultTheme=settings.lnbits_default_theme,
|
||||||
|
defaultBorder=settings.lnbits_default_border,
|
||||||
|
defaultGradient=settings.lnbits_default_gradient,
|
||||||
|
defaultBgimage=settings.lnbits_default_bgimage,
|
||||||
|
denomination=settings.lnbits_denomination,
|
||||||
|
extensions=list(settings.lnbits_installed_extensions_ids),
|
||||||
|
allowedCurrencies=settings.lnbits_allowed_currencies,
|
||||||
|
extensionsReviewsUrl=settings.lnbits_extensions_reviews_url,
|
||||||
|
extBuilder=settings.lnbits_extensions_builder_activate_non_admins,
|
||||||
|
nostrConfigured=settings.is_nostr_notifications_configured(),
|
||||||
|
telegramConfigured=settings.is_telegram_notifications_configured(),
|
||||||
|
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
|
||||||
|
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
|
||||||
|
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
|
||||||
|
userActivationByEmail=settings.lnbits_user_activation_by_email,
|
||||||
|
userActivationByPayment=settings.lnbits_user_activation_by_payment,
|
||||||
|
userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SuperSettings(EditableSettings):
|
class SuperSettings(EditableSettings):
|
||||||
super_user: str
|
super_user: str
|
||||||
|
|||||||
+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
@@ -217,7 +217,7 @@ 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 {
|
:root {
|
||||||
@@ -455,3 +455,12 @@ video {
|
|||||||
width: 90% !important;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,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',
|
||||||
@@ -169,6 +171,7 @@ window.localisation.en = {
|
|||||||
drop_db: 'Remove Data',
|
drop_db: 'Remove Data',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
|
disabled: 'Disabled',
|
||||||
pay_to_enable: 'Pay To Enable',
|
pay_to_enable: 'Pay To Enable',
|
||||||
enable_extension_details: 'Enable extension for current user',
|
enable_extension_details: 'Enable extension for current user',
|
||||||
disable: 'Disable',
|
disable: 'Disable',
|
||||||
@@ -176,6 +179,8 @@ window.localisation.en = {
|
|||||||
installed: 'Installed',
|
installed: 'Installed',
|
||||||
activated: 'Activated',
|
activated: 'Activated',
|
||||||
deactivated: 'Deactivated',
|
deactivated: 'Deactivated',
|
||||||
|
activate: 'Activate',
|
||||||
|
deactivate: 'Deactivate',
|
||||||
release_notes: 'Release Notes',
|
release_notes: 'Release Notes',
|
||||||
activate_extension_details: 'Make extension available/unavailable for users',
|
activate_extension_details: 'Make extension available/unavailable for users',
|
||||||
featured: 'Featured',
|
featured: 'Featured',
|
||||||
@@ -184,7 +189,28 @@ window.localisation.en = {
|
|||||||
only_admins_can_create_extensions:
|
only_admins_can_create_extensions:
|
||||||
'Only admin accounts can create extensions',
|
'Only admin accounts can create extensions',
|
||||||
admin_only: 'Admin Only',
|
admin_only: 'Admin Only',
|
||||||
|
make_user_admin: 'Make User Admin',
|
||||||
|
revoke_admin: 'Revoke Admin',
|
||||||
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:',
|
||||||
@@ -237,6 +263,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.',
|
||||||
@@ -258,7 +285,7 @@ window.localisation.en = {
|
|||||||
'Nip5 identifier to send notifications to',
|
'Nip5 identifier to send notifications to',
|
||||||
notifications_nostr_identifiers: 'Nostr Identifiers',
|
notifications_nostr_identifiers: 'Nostr Identifiers',
|
||||||
notifications_nostr_identifiers_desc:
|
notifications_nostr_identifiers_desc:
|
||||||
'List of identifiers to send notifications to',
|
'List of identifiers to send notifications to.',
|
||||||
|
|
||||||
notifications_telegram_config: 'Telegram Configuration',
|
notifications_telegram_config: 'Telegram Configuration',
|
||||||
notifications_enable_telegram: 'Enable Telegram',
|
notifications_enable_telegram: 'Enable Telegram',
|
||||||
@@ -428,6 +455,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',
|
||||||
@@ -495,6 +523,7 @@ window.localisation.en = {
|
|||||||
hash: 'Hash: ',
|
hash: 'Hash: ',
|
||||||
welcome_lnbits: 'Welcome to LNbits',
|
welcome_lnbits: 'Welcome to LNbits',
|
||||||
setup_su_account: 'Set up the Superuser account below.',
|
setup_su_account: 'Set up the Superuser account below.',
|
||||||
|
first_install_token: 'First Install Token (optional)',
|
||||||
create_ticker_converter: 'Create Currency Ticker Converter',
|
create_ticker_converter: 'Create Currency Ticker Converter',
|
||||||
enable_audit: 'Enable Audit',
|
enable_audit: 'Enable Audit',
|
||||||
recommended: 'Recommended',
|
recommended: 'Recommended',
|
||||||
@@ -574,6 +603,9 @@ window.localisation.en = {
|
|||||||
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',
|
||||||
@@ -632,12 +664,23 @@ window.localisation.en = {
|
|||||||
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
||||||
ui_default_wallet_name: 'Default Wallet Name',
|
ui_default_wallet_name: 'Default Wallet Name',
|
||||||
ui_default_theme: 'Default Theme',
|
ui_default_theme: 'Default Theme',
|
||||||
|
wallet_featured_button_label: 'Wallet Featured Button Label',
|
||||||
|
wallet_featured_button_label_hint:
|
||||||
|
'Show featured button on the wallet homepage',
|
||||||
|
wallet_featured_button_url: 'Featured Button URL',
|
||||||
|
wallet_featured_button_url_hint:
|
||||||
|
'On click the button will open this URL. Leave empty to hide the button.',
|
||||||
|
wallet_featured_button_icon: 'Featured Button Icon',
|
||||||
|
wallet_featured_button_icon_hint:
|
||||||
|
'Icon shown on the featured button (check quasar icons)',
|
||||||
lnbits_wallet: 'LNbits wallet',
|
lnbits_wallet: 'LNbits wallet',
|
||||||
denomination: 'Denomination',
|
denomination: 'Denomination',
|
||||||
denomination_hint: 'The name for the FakeWallet token',
|
denomination_hint: 'The name for the FakeWallet token',
|
||||||
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',
|
||||||
@@ -666,7 +709,28 @@ window.localisation.en = {
|
|||||||
allowed_users_label: 'User ID',
|
allowed_users_label: 'User ID',
|
||||||
allow_creation_user: 'Allow creation of new users',
|
allow_creation_user: 'Allow creation of new users',
|
||||||
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
||||||
|
require_user_activation: 'Require user activation',
|
||||||
|
require_user_activation_desc:
|
||||||
|
'New users will be activated only after they pass one of the confirmation methods. Admins can activate users manually from the admin panel.',
|
||||||
|
reusable_activation_code: 'Reusable activation code',
|
||||||
|
reusable_activation_code_label: 'Reusable activation code',
|
||||||
|
reusable_activation_code_hint:
|
||||||
|
'This activation code can be used multiple times by different users.',
|
||||||
|
one_time_activation_code: 'One-time activation codes',
|
||||||
|
one_time_activation_code_label: 'Add activation code',
|
||||||
|
one_time_activation_code_hint:
|
||||||
|
'List of one-time activation codes. Each code can be used only once, then will be reomved from the list.',
|
||||||
|
invitation_code: 'Invitation Code',
|
||||||
|
invitation_code_hint: 'The invitation code that you have received.',
|
||||||
|
email: 'Email',
|
||||||
|
email_confirmation_hint: 'Email address to send the confirmation code to.',
|
||||||
|
nostr_identifier: 'Nostr Identifier',
|
||||||
|
nostr_identifier_hint:
|
||||||
|
'Nostr nip5 identifier or <npub> to send the confirmation code to.',
|
||||||
|
|
||||||
new_user_not_allowed: 'Registration is disabled.',
|
new_user_not_allowed: 'Registration is disabled.',
|
||||||
|
start_user_impersonation: 'Impersonate this user',
|
||||||
|
stop_user_impersonation: 'Stop User Impersonation',
|
||||||
components: 'Components',
|
components: 'Components',
|
||||||
long_running_endpoints: 'Top 5 Long Running Endpoints',
|
long_running_endpoints: 'Top 5 Long Running Endpoints',
|
||||||
http_request_methods: 'HTTP Request Methods',
|
http_request_methods: 'HTTP Request Methods',
|
||||||
@@ -699,10 +763,15 @@ window.localisation.en = {
|
|||||||
filter_labels: 'Filter labels',
|
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',
|
||||||
@@ -716,11 +785,14 @@ window.localisation.en = {
|
|||||||
add_label: 'Add Label',
|
add_label: 'Add Label',
|
||||||
label: 'Label',
|
label: 'Label',
|
||||||
labels: 'Labels',
|
labels: 'Labels',
|
||||||
|
label_filter: 'Label Filter',
|
||||||
no_labels_defined: 'No labels defined yet',
|
no_labels_defined: 'No labels defined yet',
|
||||||
manage_labels: 'Manage Labels',
|
manage_labels: 'Manage Labels',
|
||||||
update_label: 'Update Label',
|
update_label: 'Update Label',
|
||||||
delete_label: 'Delete Label',
|
delete_label: 'Delete Label',
|
||||||
add_remove_labels: 'Add or Remove Labels',
|
add_remove_labels: 'Add or Remove Labels',
|
||||||
payment_labels_updated: 'Payment labels updated',
|
payment_labels_updated: 'Payment labels updated',
|
||||||
color: 'Color'
|
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'
|
||||||
|
|||||||
+34
-12
@@ -66,7 +66,7 @@ window._lnbitsApi = {
|
|||||||
name: name
|
name: name
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
register(username, email, password, password_repeat) {
|
register(username, email, password, password_repeat, invitation_code) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/v1/auth/register',
|
url: '/api/v1/auth/register',
|
||||||
@@ -74,7 +74,8 @@ window._lnbitsApi = {
|
|||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
password_repeat
|
password_repeat,
|
||||||
|
invitation_code
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -89,6 +90,12 @@ window._lnbitsApi = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
getAuthUser() {
|
||||||
|
return axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/auth'
|
||||||
|
})
|
||||||
|
},
|
||||||
login(username, password) {
|
login(username, password) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -117,19 +124,30 @@ window._lnbitsApi = {
|
|||||||
url: '/api/v1/auth/logout'
|
url: '/api/v1/auth/logout'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
impersonateUser(usr) {
|
||||||
|
return axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/auth/impersonate',
|
||||||
|
data: {usr}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
stopImpersonation() {
|
||||||
|
return axios({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: '/api/v1/auth/impersonate'
|
||||||
|
})
|
||||||
|
},
|
||||||
getAuthenticatedUser() {
|
getAuthenticatedUser() {
|
||||||
return this.request('get', '/api/v1/auth')
|
return this.request('get', '/api/v1/auth')
|
||||||
},
|
},
|
||||||
getWallet(wallet) {
|
getWallet(wallet) {
|
||||||
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
||||||
},
|
},
|
||||||
createWallet(wallet, name, walletType, ops = {}) {
|
createWallet(name, walletType, opts = {}) {
|
||||||
return this.request('post', '/api/v1/wallet', wallet.adminkey, {
|
return this.request('post', '/api/v1/wallet', null, {
|
||||||
name: name,
|
name: name,
|
||||||
wallet_type: walletType,
|
wallet_type: walletType,
|
||||||
...ops
|
...opts
|
||||||
}).then(res => {
|
|
||||||
window.location = '/wallet?wal=' + res.data.id
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateWallet(name, wallet) {
|
updateWallet(name, wallet) {
|
||||||
@@ -137,6 +155,9 @@ window._lnbitsApi = {
|
|||||||
name: name
|
name: name
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updateUiCustomization(data = {}) {
|
||||||
|
return this.request('patch', '/api/v1/auth/ui', null, data)
|
||||||
|
},
|
||||||
resetWalletKeys(wallet) {
|
resetWalletKeys(wallet) {
|
||||||
return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then(
|
return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then(
|
||||||
res => {
|
res => {
|
||||||
@@ -145,11 +166,7 @@ window._lnbitsApi = {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
deleteWallet(wallet) {
|
deleteWallet(wallet) {
|
||||||
return this.request('delete', `/api/v1/wallet/${wallet.id}`).then(_ => {
|
return this.request('delete', `/api/v1/wallet/${wallet.id}`)
|
||||||
let url = new URL(window.location.href)
|
|
||||||
url.searchParams.delete('wal')
|
|
||||||
window.location = url
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
getPayments(wallet, params) {
|
getPayments(wallet, params) {
|
||||||
return this.request(
|
return this.request(
|
||||||
@@ -171,5 +188,10 @@ window._lnbitsApi = {
|
|||||||
return this.request('GET', '/api/v1/currencies').then(response => {
|
return this.request('GET', '/api/v1/currencies').then(response => {
|
||||||
return ['sats', ...response.data]
|
return ['sats', ...response.data]
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
getDefaultSetting(fieldName) {
|
||||||
|
return LNbits.api
|
||||||
|
.request('GET', `/admin/api/v1/settings/default?field_name=${fieldName}`)
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-71
@@ -2,41 +2,28 @@ window.LNbits = {
|
|||||||
g: window.g,
|
g: window.g,
|
||||||
utils: window._lnbitsUtils,
|
utils: window._lnbitsUtils,
|
||||||
api: window._lnbitsApi,
|
api: window._lnbitsApi,
|
||||||
events: {
|
|
||||||
onInvoicePaid(wallet, cb) {
|
|
||||||
ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`)
|
|
||||||
ws.onmessage = ev => {
|
|
||||||
const data = JSON.parse(ev.data)
|
|
||||||
if (data.payment) {
|
|
||||||
cb(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ws.onclose
|
|
||||||
}
|
|
||||||
},
|
|
||||||
map: {
|
map: {
|
||||||
user(data) {
|
user(data) {
|
||||||
const obj = {
|
const obj = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
|
username: data.username,
|
||||||
admin: data.admin,
|
admin: data.admin,
|
||||||
email: data.email,
|
email: data.email,
|
||||||
extensions: data.extensions,
|
extensions: data.extensions,
|
||||||
wallets: data.wallets,
|
wallets: data.wallets,
|
||||||
fiat_providers: data.fiat_providers || [],
|
fiat_providers: data.fiat_providers || [],
|
||||||
super_user: data.super_user,
|
super_user: data.super_user,
|
||||||
extra: data.extra ?? {}
|
extra: data.extra ?? {},
|
||||||
|
hasPassword: data.has_password ?? false,
|
||||||
|
uiCustomization: data.ui_customization || {}
|
||||||
}
|
}
|
||||||
const mapWallet = this.wallet
|
const mapWallet = this.wallet
|
||||||
obj.wallets = obj.wallets
|
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
||||||
.map(obj => {
|
if (a.extra.pinned !== b.extra.pinned) {
|
||||||
return mapWallet(obj)
|
return a.extra.pinned ? -1 : 1
|
||||||
})
|
}
|
||||||
.sort((a, b) => {
|
return a.name.localeCompare(b.name)
|
||||||
if (a.extra.pinned !== b.extra.pinned) {
|
})
|
||||||
return a.extra.pinned ? -1 : 1
|
|
||||||
}
|
|
||||||
return a.name.localeCompare(b.name)
|
|
||||||
})
|
|
||||||
obj.walletOptions = obj.wallets.map(obj => {
|
obj.walletOptions = obj.wallets.map(obj => {
|
||||||
return {
|
return {
|
||||||
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
||||||
@@ -66,9 +53,6 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
newWallet.msat = data.balance_msat
|
newWallet.msat = data.balance_msat
|
||||||
newWallet.sat = Math.floor(data.balance_msat / 1000)
|
newWallet.sat = Math.floor(data.balance_msat / 1000)
|
||||||
newWallet.fsat = new Intl.NumberFormat(window.i18n.global.locale).format(
|
|
||||||
newWallet.sat
|
|
||||||
)
|
|
||||||
if (newWallet.walletType === 'lightning-shared') {
|
if (newWallet.walletType === 'lightning-shared') {
|
||||||
const perms = newWallet.sharePermissions
|
const perms = newWallet.sharePermissions
|
||||||
newWallet.canReceivePayments = perms.includes('receive-payments')
|
newWallet.canReceivePayments = perms.includes('receive-payments')
|
||||||
@@ -77,51 +61,6 @@ window.LNbits = {
|
|||||||
newWallet.url = `/wallet?&wal=${data.id}`
|
newWallet.url = `/wallet?&wal=${data.id}`
|
||||||
newWallet.storedPaylinks = data.stored_paylinks.links
|
newWallet.storedPaylinks = data.stored_paylinks.links
|
||||||
return newWallet
|
return newWallet
|
||||||
},
|
|
||||||
payment(data) {
|
|
||||||
obj = {
|
|
||||||
checking_id: data.checking_id,
|
|
||||||
status: data.status,
|
|
||||||
amount: data.amount,
|
|
||||||
fee: data.fee,
|
|
||||||
memo: data.memo,
|
|
||||||
time: data.time,
|
|
||||||
bolt11: data.bolt11,
|
|
||||||
preimage: data.preimage,
|
|
||||||
payment_hash: data.payment_hash,
|
|
||||||
expiry: data.expiry,
|
|
||||||
extra: data.extra ?? {},
|
|
||||||
wallet_id: data.wallet_id,
|
|
||||||
webhook: data.webhook,
|
|
||||||
webhook_status: data.webhook_status,
|
|
||||||
fiat_amount: data.fiat_amount,
|
|
||||||
fiat_currency: data.fiat_currency,
|
|
||||||
labels: data.labels
|
|
||||||
}
|
|
||||||
|
|
||||||
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
|
|
||||||
obj.dateFrom = moment.utc(data.created_at).local().fromNow()
|
|
||||||
|
|
||||||
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
|
|
||||||
obj.expirydateFrom = moment.utc(obj.expiry).local().fromNow()
|
|
||||||
obj.msat = obj.amount
|
|
||||||
obj.sat = obj.msat / 1000
|
|
||||||
obj.tag = obj.extra?.tag
|
|
||||||
obj.fsat = new Intl.NumberFormat(window.i18n.global.locale).format(
|
|
||||||
obj.sat
|
|
||||||
)
|
|
||||||
obj.isIn = obj.amount > 0
|
|
||||||
obj.isOut = obj.amount < 0
|
|
||||||
obj.isPending = obj.status === 'pending'
|
|
||||||
obj.isPaid = obj.status === 'success'
|
|
||||||
obj.isFailed = obj.status === 'failed'
|
|
||||||
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
|
|
||||||
try {
|
|
||||||
obj.details = JSON.parse(data.extra?.details || '{}')
|
|
||||||
} catch {
|
|
||||||
obj.details = {extraDetails: data.extra?.details}
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,44 +3,38 @@ window.app.component(QrcodeVue)
|
|||||||
window.app.component('lnbits-extension-rating', {
|
window.app.component('lnbits-extension-rating', {
|
||||||
template: '#lnbits-extension-rating',
|
template: '#lnbits-extension-rating',
|
||||||
name: 'lnbits-extension-rating',
|
name: 'lnbits-extension-rating',
|
||||||
props: ['rating']
|
|
||||||
})
|
|
||||||
|
|
||||||
window.app.component('lnbits-fsat', {
|
|
||||||
template: '<span>{{ fsat }}</span>',
|
|
||||||
props: {
|
props: {
|
||||||
amount: {
|
rating: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
clickable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fsat() {
|
displayRating() {
|
||||||
return LNbits.utils.formatSat(this.amount)
|
return Math.round((this.rating || 0) * 2) / 2
|
||||||
|
},
|
||||||
|
hasData() {
|
||||||
|
return this.count !== null && this.count !== undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick() {
|
||||||
|
if (!this.clickable) return
|
||||||
|
this.$emit('click')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
window.app.component('lnbits-manage', {
|
window.app.component('lnbits-manage', {
|
||||||
mixins: [window.windowMixin],
|
|
||||||
template: '#lnbits-manage',
|
template: '#lnbits-manage',
|
||||||
computed: {
|
|
||||||
showAdmin() {
|
|
||||||
return this.LNBITS_ADMIN_UI
|
|
||||||
},
|
|
||||||
showUsers() {
|
|
||||||
return this.LNBITS_ADMIN_UI
|
|
||||||
},
|
|
||||||
showNode() {
|
|
||||||
return this.LNBITS_NODE_UI
|
|
||||||
},
|
|
||||||
showAudit() {
|
|
||||||
return this.LNBITS_AUDIT_ENABLED
|
|
||||||
},
|
|
||||||
showExtensions() {
|
|
||||||
return this.LNBITS_EXTENSIONS_DEACTIVATE_ALL === false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
isActive(path) {
|
isActive(path) {
|
||||||
return window.location.pathname === path
|
return window.location.pathname === path
|
||||||
@@ -54,15 +48,8 @@ window.app.component('lnbits-manage', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
window.app.component('lnbits-payment-details', {
|
window.app.component('lnbits-payment-details', {
|
||||||
mixins: [window.windowMixin],
|
|
||||||
template: '#lnbits-payment-details',
|
template: '#lnbits-payment-details',
|
||||||
props: ['payment'],
|
props: ['payment'],
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
hasPreimage() {
|
hasPreimage() {
|
||||||
return (
|
return (
|
||||||
@@ -106,7 +93,6 @@ window.app.component('lnbits-payment-details', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
window.app.component('lnbits-lnurlpay-success-action', {
|
window.app.component('lnbits-lnurlpay-success-action', {
|
||||||
mixins: [window.windowMixin],
|
|
||||||
template: '#lnbits-lnurlpay-success-action',
|
template: '#lnbits-lnurlpay-success-action',
|
||||||
props: ['payment', 'success_action'],
|
props: ['payment', 'success_action'],
|
||||||
data() {
|
data() {
|
||||||
@@ -116,17 +102,16 @@ window.app.component('lnbits-lnurlpay-success-action', {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.success_action.tag !== 'aes') return null
|
if (this.success_action.tag !== 'aes') return null
|
||||||
decryptLnurlPayAES(this.success_action, this.payment.preimage).then(
|
this.utils
|
||||||
value => {
|
.decryptLnurlPayAES(this.success_action, this.payment.preimage)
|
||||||
|
.then(value => {
|
||||||
this.decryptedValue = value
|
this.decryptedValue = value
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
window.app.component('lnbits-notifications-btn', {
|
window.app.component('lnbits-notifications-btn', {
|
||||||
template: '#lnbits-notifications-btn',
|
template: '#lnbits-notifications-btn',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
props: ['pubkey'],
|
props: ['pubkey'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -297,7 +282,6 @@ window.app.component('lnbits-notifications-btn', {
|
|||||||
|
|
||||||
window.app.component('lnbits-dynamic-fields', {
|
window.app.component('lnbits-dynamic-fields', {
|
||||||
template: '#lnbits-dynamic-fields',
|
template: '#lnbits-dynamic-fields',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
props: ['options', 'modelValue'],
|
props: ['options', 'modelValue'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -330,7 +314,6 @@ window.app.component('lnbits-dynamic-fields', {
|
|||||||
|
|
||||||
window.app.component('lnbits-dynamic-chips', {
|
window.app.component('lnbits-dynamic-chips', {
|
||||||
template: '#lnbits-dynamic-chips',
|
template: '#lnbits-dynamic-chips',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
props: ['modelValue'],
|
props: ['modelValue'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -361,14 +344,10 @@ window.app.component('lnbits-dynamic-chips', {
|
|||||||
|
|
||||||
window.app.component('lnbits-update-balance', {
|
window.app.component('lnbits-update-balance', {
|
||||||
template: '#lnbits-update-balance',
|
template: '#lnbits-update-balance',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
props: ['wallet_id', 'small_btn'],
|
props: ['wallet_id', 'small_btn'],
|
||||||
computed: {
|
computed: {
|
||||||
denomination() {
|
|
||||||
return LNBITS_DENOMINATION
|
|
||||||
},
|
|
||||||
admin() {
|
admin() {
|
||||||
return user.super_user
|
return this.g.user?.super_user === true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -403,7 +382,6 @@ window.app.component('lnbits-update-balance', {
|
|||||||
|
|
||||||
window.app.component('user-id-only', {
|
window.app.component('user-id-only', {
|
||||||
template: '#user-id-only',
|
template: '#user-id-only',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
props: {
|
props: {
|
||||||
allowed_new_users: Boolean,
|
allowed_new_users: Boolean,
|
||||||
authAction: String,
|
authAction: String,
|
||||||
@@ -447,7 +425,6 @@ window.app.component('user-id-only', {
|
|||||||
|
|
||||||
window.app.component('username-password', {
|
window.app.component('username-password', {
|
||||||
template: '#username-password',
|
template: '#username-password',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
props: {
|
props: {
|
||||||
allowed_new_users: Boolean,
|
allowed_new_users: Boolean,
|
||||||
authMethods: Array,
|
authMethods: Array,
|
||||||
@@ -455,8 +432,10 @@ window.app.component('username-password', {
|
|||||||
username: String,
|
username: String,
|
||||||
password_1: String,
|
password_1: String,
|
||||||
password_2: String,
|
password_2: String,
|
||||||
|
invitationCode: String,
|
||||||
resetKey: String
|
resetKey: String
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
oauth: [
|
oauth: [
|
||||||
@@ -469,8 +448,10 @@ window.app.component('username-password', {
|
|||||||
password: this.password_1,
|
password: this.password_1,
|
||||||
passwordRepeat: this.password_2,
|
passwordRepeat: this.password_2,
|
||||||
reset_key: this.resetKey,
|
reset_key: this.resetKey,
|
||||||
keycloakOrg: LNBITS_AUTH_KEYCLOAK_ORG || 'Keycloak',
|
confirmationMethod: 'code',
|
||||||
keycloakIcon: LNBITS_AUTH_KEYCLOAK_ICON
|
confirmationEmail: '',
|
||||||
|
confirmationCode: this.invitationCode || '',
|
||||||
|
showConfirmationCode: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -483,6 +464,7 @@ window.app.component('username-password', {
|
|||||||
this.$emit('update:userName', this.username)
|
this.$emit('update:userName', this.username)
|
||||||
this.$emit('update:password_1', this.password)
|
this.$emit('update:password_1', this.password)
|
||||||
this.$emit('update:password_2', this.passwordRepeat)
|
this.$emit('update:password_2', this.passwordRepeat)
|
||||||
|
this.$emit('update:invitationCode', this.confirmationCode)
|
||||||
this.$emit('register')
|
this.$emit('register')
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
@@ -574,6 +556,25 @@ window.app.component('username-password', {
|
|||||||
computed: {
|
computed: {
|
||||||
showOauth() {
|
showOauth() {
|
||||||
return this.oauth.some(m => this.authMethods.includes(m))
|
return this.oauth.some(m => this.authMethods.includes(m))
|
||||||
|
},
|
||||||
|
disableRegister() {
|
||||||
|
const usernameOK = !!this.username
|
||||||
|
const passwordOK = !!this.password && this.password.length >= 8
|
||||||
|
const passwordsMatch = this.password === this.passwordRepeat
|
||||||
|
const codeOk =
|
||||||
|
this.confirmationMethodsCount === 0 ||
|
||||||
|
this.confirmationMethod !== 'code' ||
|
||||||
|
this.confirmationCode.length > 0
|
||||||
|
|
||||||
|
return !usernameOK || !passwordOK || !passwordsMatch || !codeOk
|
||||||
|
},
|
||||||
|
confirmationMethodsCount() {
|
||||||
|
const methods = [
|
||||||
|
this.g.settings.userActivationByEmail,
|
||||||
|
this.g.settings.userActivationByPayment,
|
||||||
|
this.g.settings.userActivationByInvitationCode
|
||||||
|
]
|
||||||
|
return methods.filter(Boolean).length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {}
|
created() {}
|
||||||
@@ -685,7 +686,6 @@ window.app.component('lnbits-stat', {
|
|||||||
|
|
||||||
window.app.component('lnbits-node-qrcode', {
|
window.app.component('lnbits-node-qrcode', {
|
||||||
props: ['info'],
|
props: ['info'],
|
||||||
mixins: [window.windowMixin],
|
|
||||||
template: `
|
template: `
|
||||||
<q-card class="my-card">
|
<q-card class="my-card">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@@ -708,7 +708,7 @@ window.app.component('lnbits-node-qrcode', {
|
|||||||
dense
|
dense
|
||||||
unelevated
|
unelevated
|
||||||
size="md"
|
size="md"
|
||||||
@click="copyText(info.id)"
|
@click="utils.copyText(info.id)"
|
||||||
>Public Key<q-tooltip> Click to copy </q-tooltip>
|
>Public Key<q-tooltip> Click to copy </q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
@@ -757,24 +757,6 @@ window.app.component('lnbits-channel-balance', {
|
|||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
window.app.component('lnbits-date', {
|
|
||||||
props: ['ts'],
|
|
||||||
computed: {
|
|
||||||
date() {
|
|
||||||
return LNbits.utils.formatDate(this.ts)
|
|
||||||
},
|
|
||||||
dateFrom() {
|
|
||||||
return moment.utc(this.date).local().fromNow()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<div>
|
|
||||||
<q-tooltip>{{ this.date }}</q-tooltip>
|
|
||||||
{{ this.dateFrom }}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
|
|
||||||
window.app.component('lnbits-node-info', {
|
window.app.component('lnbits-node-info', {
|
||||||
props: ['info'],
|
props: ['info'],
|
||||||
data() {
|
data() {
|
||||||
@@ -782,7 +764,6 @@ window.app.component('lnbits-node-info', {
|
|||||||
showDialog: false
|
showDialog: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mixins: [window.windowMixin],
|
|
||||||
methods: {
|
methods: {
|
||||||
shortenNodeId(nodeId) {
|
shortenNodeId(nodeId) {
|
||||||
return nodeId
|
return nodeId
|
||||||
@@ -807,7 +788,7 @@ window.app.component('lnbits-node-info', {
|
|||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
icon='content_paste'
|
icon='content_paste'
|
||||||
@click='copyText(info.id)'
|
@click='utils.copyText(info.id)'
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
size='xs'
|
size='xs'
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
window.app.component('lnbits-admin-assets-config', {
|
window.app.component('lnbits-admin-assets-config', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-assets-config',
|
template: '#lnbits-admin-assets-config',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
newAllowedAssetMimeType: '',
|
newAllowedAssetMimeType: '',
|
||||||
@@ -17,6 +16,7 @@ window.app.component('lnbits-admin-assets-config', {
|
|||||||
this.newAllowedAssetMimeType
|
this.newAllowedAssetMimeType
|
||||||
)
|
)
|
||||||
this.newAllowedAssetMimeType = ''
|
this.newAllowedAssetMimeType = ''
|
||||||
|
this.formData.touch = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeAllowedAssetMimeType(type) {
|
removeAllowedAssetMimeType(type) {
|
||||||
@@ -24,18 +24,21 @@ window.app.component('lnbits-admin-assets-config', {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.formData.lnbits_assets_allowed_mime_types.splice(index, 1)
|
this.formData.lnbits_assets_allowed_mime_types.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
this.formData.touch = null
|
||||||
},
|
},
|
||||||
addNewNoLimitUser() {
|
addNewNoLimitUser() {
|
||||||
if (this.newNoLimitUser) {
|
if (this.newNoLimitUser) {
|
||||||
this.removeNoLimitUser(this.newNoLimitUser)
|
this.removeNoLimitUser(this.newNoLimitUser)
|
||||||
this.formData.lnbits_assets_no_limit_users.push(this.newNoLimitUser)
|
this.formData.lnbits_assets_no_limit_users.push(this.newNoLimitUser)
|
||||||
this.newNoLimitUser = ''
|
this.newNoLimitUser = ''
|
||||||
|
this.formData.touch = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeNoLimitUser(user) {
|
removeNoLimitUser(user) {
|
||||||
if (user) {
|
if (user) {
|
||||||
this.formData.lnbits_assets_no_limit_users =
|
this.formData.lnbits_assets_no_limit_users =
|
||||||
this.formData.lnbits_assets_no_limit_users.filter(u => u !== user)
|
this.formData.lnbits_assets_no_limit_users.filter(u => u !== user)
|
||||||
|
this.formData.touch = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
window.app.component('lnbits-admin-audit', {
|
window.app.component('lnbits-admin-audit', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-audit',
|
template: '#lnbits-admin-audit',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddIncludePath: '',
|
formAddIncludePath: '',
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
window.app.component('lnbits-admin-exchange-providers', {
|
window.app.component('lnbits-admin-exchange-providers', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-exchange-providers',
|
template: '#lnbits-admin-exchange-providers',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
exchangeData: {
|
exchangeData: {
|
||||||
@@ -70,6 +69,11 @@ window.app.component('lnbits-admin-exchange-providers', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getDefaultSetting(fieldName) {
|
||||||
|
LNbits.api.getDefaultSetting(fieldName).then(response => {
|
||||||
|
this.formData[fieldName] = response.data.default_value
|
||||||
|
})
|
||||||
|
},
|
||||||
getExchangeRateHistory() {
|
getExchangeRateHistory() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
|
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
|
||||||
@@ -124,7 +128,7 @@ window.app.component('lnbits-admin-exchange-providers', {
|
|||||||
},
|
},
|
||||||
initExchangeChart(data) {
|
initExchangeChart(data) {
|
||||||
const xValues = data.map(d =>
|
const xValues = data.map(d =>
|
||||||
Quasar.date.formatDate(new Date(d.timestamp * 1000), 'HH:mm')
|
this.utils.formatTimestamp(d.timestamp, 'HH:mm')
|
||||||
)
|
)
|
||||||
const exchanges = [
|
const exchanges = [
|
||||||
...this.formData.lnbits_exchange_rate_providers,
|
...this.formData.lnbits_exchange_rate_providers,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
window.app.component('lnbits-admin-extensions', {
|
window.app.component('lnbits-admin-extensions', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-extensions',
|
template: '#lnbits-admin-extensions',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddExtensionsManifest: ''
|
formAddExtensionsManifest: ''
|
||||||
|
|||||||
@@ -1,14 +1,82 @@
|
|||||||
window.app.component('lnbits-admin-fiat-providers', {
|
window.app.component('lnbits-admin-fiat-providers', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-fiat-providers',
|
template: '#lnbits-admin-fiat-providers',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddStripeUser: '',
|
formAddStripeUser: '',
|
||||||
|
formAddPaypalUser: '',
|
||||||
hideInputToggle: true
|
hideInputToggle: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
stripeWebhookUrl() {
|
||||||
|
return (
|
||||||
|
this.formData?.stripe_payment_webhook_url ||
|
||||||
|
this.calculateWebhookUrl('stripe')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
paypalWebhookUrl() {
|
||||||
|
return (
|
||||||
|
this.formData?.paypal_payment_webhook_url ||
|
||||||
|
this.calculateWebhookUrl('paypal')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
formData: {
|
||||||
|
handler() {
|
||||||
|
this.syncWebhookUrls()
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
basePathFromLocation() {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const normalizedPath = window.location.pathname.replace(/\/+$/, '')
|
||||||
|
const adminIndex = normalizedPath.lastIndexOf('/admin')
|
||||||
|
const basePath =
|
||||||
|
adminIndex >= 0
|
||||||
|
? normalizedPath.slice(0, adminIndex)
|
||||||
|
: normalizedPath || ''
|
||||||
|
return basePath || ''
|
||||||
|
},
|
||||||
|
calculateWebhookUrl(provider) {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const basePath = this.basePathFromLocation()
|
||||||
|
const path = `${basePath}/api/v1/callback/${provider}`.replace(
|
||||||
|
/\/+/g,
|
||||||
|
'/'
|
||||||
|
)
|
||||||
|
const withLeadingSlash = path.startsWith('/') ? path : `/${path}`
|
||||||
|
return `${window.location.origin}${withLeadingSlash}`
|
||||||
|
},
|
||||||
|
syncWebhookUrls() {
|
||||||
|
this.maybeSetWebhookUrl('stripe_payment_webhook_url', 'stripe')
|
||||||
|
this.maybeSetWebhookUrl('paypal_payment_webhook_url', 'paypal')
|
||||||
|
},
|
||||||
|
maybeSetWebhookUrl(fieldName, provider) {
|
||||||
|
if (!this.formData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const calculated = this.calculateWebhookUrl(provider)
|
||||||
|
const current = this.formData[fieldName]
|
||||||
|
const hasPlaceholder =
|
||||||
|
!current || current.includes('your-lnbits-domain-here.com')
|
||||||
|
if (hasPlaceholder && calculated) {
|
||||||
|
this.formData[fieldName] = calculated
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyWebhookUrl(url) {
|
||||||
|
if (!url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.copyText(url)
|
||||||
|
},
|
||||||
addStripeAllowedUser() {
|
addStripeAllowedUser() {
|
||||||
const addUser = this.formAddStripeUser || ''
|
const addUser = this.formAddStripeUser || ''
|
||||||
if (
|
if (
|
||||||
@@ -26,6 +94,23 @@ window.app.component('lnbits-admin-fiat-providers', {
|
|||||||
this.formData.stripe_limits.allowed_users =
|
this.formData.stripe_limits.allowed_users =
|
||||||
this.formData.stripe_limits.allowed_users.filter(u => u !== user)
|
this.formData.stripe_limits.allowed_users.filter(u => u !== user)
|
||||||
},
|
},
|
||||||
|
addPaypalAllowedUser() {
|
||||||
|
const addUser = this.formAddPaypalUser || ''
|
||||||
|
if (
|
||||||
|
addUser.length &&
|
||||||
|
!this.formData.paypal_limits.allowed_users.includes(addUser)
|
||||||
|
) {
|
||||||
|
this.formData.paypal_limits.allowed_users = [
|
||||||
|
...this.formData.paypal_limits.allowed_users,
|
||||||
|
addUser
|
||||||
|
]
|
||||||
|
this.formAddPaypalUser = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removePaypalAllowedUser(user) {
|
||||||
|
this.formData.paypal_limits.allowed_users =
|
||||||
|
this.formData.paypal_limits.allowed_users.filter(u => u !== user)
|
||||||
|
},
|
||||||
checkFiatProvider(providerName) {
|
checkFiatProvider(providerName) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('PUT', `/api/v1/fiat/check/${providerName}`)
|
.request('PUT', `/api/v1/fiat/check/${providerName}`)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
window.app.component('lnbits-admin-funding-sources', {
|
window.app.component('lnbits-admin-funding-sources', {
|
||||||
template: '#lnbits-admin-funding-sources',
|
template: '#lnbits-admin-funding-sources',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
props: ['form-data', 'allowed-funding-sources'],
|
props: ['form-data', 'allowed-funding-sources'],
|
||||||
methods: {
|
methods: {
|
||||||
getFundingSourceLabel(item) {
|
getFundingSourceLabel(item) {
|
||||||
@@ -89,8 +88,14 @@ window.app.component('lnbits-admin-funding-sources', {
|
|||||||
lnd_rest_cert: 'Certificate',
|
lnd_rest_cert: 'Certificate',
|
||||||
lnd_rest_macaroon: 'Macaroon',
|
lnd_rest_macaroon: 'Macaroon',
|
||||||
lnd_rest_macaroon_encrypted: 'Encrypted Macaroon',
|
lnd_rest_macaroon_encrypted: 'Encrypted Macaroon',
|
||||||
lnd_rest_route_hints: 'Enable Route Hints',
|
lnd_rest_route_hints: {
|
||||||
lnd_rest_allow_self_payment: 'Allow Self Payment'
|
advanced: true,
|
||||||
|
label: 'Enable Route Hints'
|
||||||
|
},
|
||||||
|
lnd_rest_allow_self_payment: {
|
||||||
|
advanced: true,
|
||||||
|
label: 'Allow Self Payment'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -160,16 +165,27 @@ window.app.component('lnbits-admin-funding-sources', {
|
|||||||
'BoltzWallet',
|
'BoltzWallet',
|
||||||
'Boltz',
|
'Boltz',
|
||||||
{
|
{
|
||||||
boltz_client_endpoint: 'Endpoint',
|
boltz_client_endpoint: {
|
||||||
boltz_client_macaroon: 'Admin Macaroon path or hex',
|
label: 'Boltz client endpoint',
|
||||||
boltz_client_cert: 'Certificate path or hex',
|
value: '127.0.0.1:9002'
|
||||||
boltz_client_wallet: 'Wallet Name',
|
},
|
||||||
boltz_client_password: 'Wallet Password (can be empty)',
|
boltz_client_macaroon: {
|
||||||
|
label: 'Admin Macaroon path or hex',
|
||||||
|
value: '/home/ubuntu/.boltz/macaroons/admin.macaroon'
|
||||||
|
},
|
||||||
|
boltz_client_cert: {
|
||||||
|
label: 'Certificate path or hex',
|
||||||
|
value: '/home/ubuntu/.boltz/tls.cert'
|
||||||
|
},
|
||||||
boltz_mnemonic: {
|
boltz_mnemonic: {
|
||||||
label: 'Liquid mnemonic (copy into greenwallet)',
|
label: 'Liquid seed phrase',
|
||||||
readonly: true,
|
hint: 'Boltz will fetch once connected, but you can change later (can be opened in a liquid wallet) ',
|
||||||
copy: true,
|
copy: true,
|
||||||
qrcode: true
|
qrcode: true
|
||||||
|
},
|
||||||
|
boltz_client_password: {
|
||||||
|
label: 'Wallet Password (optional)',
|
||||||
|
advanced: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
window.app.component('lnbits-admin-funding', {
|
window.app.component('lnbits-admin-funding', {
|
||||||
props: ['is-super-user', 'form-data', 'settings'],
|
props: ['is-super-user', 'form-data', 'settings'],
|
||||||
template: '#lnbits-admin-funding',
|
template: '#lnbits-admin-funding',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
auditData: []
|
auditData: []
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
window.app.component('lnbits-admin-notifications', {
|
window.app.component('lnbits-admin-notifications', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-notifications',
|
template: '#lnbits-admin-notifications',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
nostrNotificationIdentifier: '',
|
nostrNotificationIdentifier: '',
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
window.app.component('lnbits-admin-security', {
|
window.app.component('lnbits-admin-security', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-security',
|
template: '#lnbits-admin-security',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
logs: [],
|
logs: [],
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
window.app.component('lnbits-admin-server', {
|
window.app.component('lnbits-admin-server', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-server',
|
template: '#lnbits-admin-server'
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currencies: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created() {
|
|
||||||
this.currencies = await LNbits.api.getCurrencies()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
window.app.component('lnbits-admin-site-customisation', {
|
window.app.component('lnbits-admin-site-customisation', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-site-customisation',
|
template: '#lnbits-admin-site-customisation',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lnbits_theme_options: [
|
lnbits_theme_options: [
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
window.app.component('lnbits-admin-users', {
|
window.app.component('lnbits-admin-users', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-users',
|
template: '#lnbits-admin-users',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddUser: '',
|
formAddUser: '',
|
||||||
formAddAdmin: ''
|
formAddAdmin: '',
|
||||||
|
formAddActivationCode: '',
|
||||||
|
showReusableActivationCode: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -32,6 +33,24 @@ window.app.component('lnbits-admin-users', {
|
|||||||
removeAdminUser(user) {
|
removeAdminUser(user) {
|
||||||
let admin_users = this.formData.lnbits_admin_users
|
let admin_users = this.formData.lnbits_admin_users
|
||||||
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
||||||
|
},
|
||||||
|
addOneTimeActivationCode() {
|
||||||
|
const code = this.formAddActivationCode
|
||||||
|
const activationCodes =
|
||||||
|
this.formData.lnbits_register_one_time_activation_codes
|
||||||
|
if (code?.length && !activationCodes.includes(code)) {
|
||||||
|
this.formData.lnbits_register_one_time_activation_codes = [
|
||||||
|
...activationCodes,
|
||||||
|
code
|
||||||
|
]
|
||||||
|
this.formAddActivationCode = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeOneTimeActivationCode(code) {
|
||||||
|
const codes = this.formData.lnbits_register_one_time_activation_codes
|
||||||
|
this.formData.lnbits_register_one_time_activation_codes = codes.filter(
|
||||||
|
u => u !== code
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
window.app.component('lnbits-disclaimer', {
|
window.app.component('lnbits-disclaimer', {
|
||||||
template: '#lnbits-disclaimer',
|
template: '#lnbits-disclaimer',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
computed: {
|
computed: {
|
||||||
showDisclaimer() {
|
showDisclaimer() {
|
||||||
return !g.disclaimerShown && g.isUserAuthorized
|
return !g.disclaimerShown && g.isUserAuthorized
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
window.app.component('lnbits-drawer', {
|
window.app.component('lnbits-drawer', {
|
||||||
template: '#lnbits-drawer',
|
template: '#lnbits-drawer'
|
||||||
mixins: [window.windowMixin]
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
window.app.component('lnbits-error', {
|
||||||
|
template: '#lnbits-error',
|
||||||
|
props: ['dynamic', 'code', 'message'],
|
||||||
|
computed: {
|
||||||
|
isExtension() {
|
||||||
|
if (this.code != 403) return false
|
||||||
|
if (this.message.startsWith('Extension ')) return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() {
|
||||||
|
window.history.back()
|
||||||
|
},
|
||||||
|
goHome() {
|
||||||
|
window.location = '/'
|
||||||
|
},
|
||||||
|
goToWallet() {
|
||||||
|
if (this.dynamic) {
|
||||||
|
this.$router.push('/wallet')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.location = '/wallet'
|
||||||
|
},
|
||||||
|
goToExtension() {
|
||||||
|
const extension = this.message.match(/'([^']+)'/)[1]
|
||||||
|
const url = `/extensions#${extension}`
|
||||||
|
if (this.dynamic) {
|
||||||
|
this.$router.push(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.location = url
|
||||||
|
},
|
||||||
|
async logOut() {
|
||||||
|
try {
|
||||||
|
await LNbits.api.logout()
|
||||||
|
window.location = '/'
|
||||||
|
} catch (e) {
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
// check if we have error from error.html
|
||||||
|
if (!this.dynamic) {
|
||||||
|
if (this.code == 401) {
|
||||||
|
console.warn(`Unauthorized: ${this.errorMessage}`)
|
||||||
|
this.logOut()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
window.app.component('lnbits-footer', {
|
window.app.component('lnbits-footer', {
|
||||||
template: '#lnbits-footer',
|
template: '#lnbits-footer',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
computed: {
|
computed: {
|
||||||
version() {
|
|
||||||
return this.LNBITS_VERSION || 'unknown version'
|
|
||||||
},
|
|
||||||
title() {
|
title() {
|
||||||
return `${this.SITE_TITLE}, ${this.SITE_TAGLINE}`
|
return `${this.g.settings.siteTitle}, ${this.g.settings.siteTagline}`
|
||||||
},
|
},
|
||||||
showFooter() {
|
version() {
|
||||||
return (
|
return this.$t('lnbits_version') + ': ' + this.g.settings.version
|
||||||
this.SITE_TITLE == 'LNbits' &&
|
|
||||||
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
window.app.component('lnbits-header-wallets', {
|
window.app.component('lnbits-header-wallets', {
|
||||||
template: '#lnbits-header-wallets',
|
template: '#lnbits-header-wallets'
|
||||||
mixins: [window.windowMixin]
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,57 +1,48 @@
|
|||||||
window.app.component('lnbits-header', {
|
window.app.component('lnbits-header', {
|
||||||
template: '#lnbits-header',
|
template: '#lnbits-header',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
computed: {
|
computed: {
|
||||||
denomination() {
|
|
||||||
return this.LNBITS_DENOMINATION || 'sat'
|
|
||||||
},
|
|
||||||
hasServiceFeeMax() {
|
|
||||||
return (
|
|
||||||
this.g.user &&
|
|
||||||
this.LNBITS_SERVICE_FEE_MAX &&
|
|
||||||
this.LNBITS_SERVICE_FEE_MAX > 0
|
|
||||||
)
|
|
||||||
},
|
|
||||||
serviceFeeMax() {
|
|
||||||
return this.LNBITS_SERVICE_FEE_MAX || 0
|
|
||||||
},
|
|
||||||
hasServiceFee() {
|
|
||||||
return (
|
|
||||||
this.g.user && this.LNBITS_SERVICE_FEE && this.LNBITS_SERVICE_FEE > 0
|
|
||||||
)
|
|
||||||
},
|
|
||||||
serviceFee() {
|
|
||||||
return this.LNBITS_SERVICE_FEE || 0
|
|
||||||
},
|
|
||||||
hasCustomBadge() {
|
|
||||||
return this.LNBITS_CUSTOM_BADGE && this.LNBITS_CUSTOM_BADGE != ''
|
|
||||||
},
|
|
||||||
customBadge() {
|
|
||||||
return this.LNBITS_CUSTOM_BADGE || ''
|
|
||||||
},
|
|
||||||
customBadgeColor() {
|
|
||||||
return this.LNBITS_CUSTOM_BADGE_COLOR || ''
|
|
||||||
},
|
|
||||||
title() {
|
|
||||||
return this.SITE_TITLE
|
|
||||||
},
|
|
||||||
titleIsLnbits() {
|
|
||||||
return this.SITE_TITLE == 'LNbits'
|
|
||||||
},
|
|
||||||
customLogoUrl() {
|
|
||||||
return this.USE_CUSTOM_LOGO || null
|
|
||||||
},
|
|
||||||
userPictureUrl() {
|
|
||||||
return this.g.user.config.picture
|
|
||||||
},
|
|
||||||
hasUserPicture() {
|
|
||||||
return this.g.user && this.g.user.config && this.g.user.config.picture
|
|
||||||
},
|
|
||||||
showAdmin() {
|
showAdmin() {
|
||||||
return this.g.user && (this.g.user.super_user || this.g.user.admin)
|
return this.g.user && (this.g.user.super_user || this.g.user.admin)
|
||||||
},
|
},
|
||||||
showVoidwallet() {
|
displayName() {
|
||||||
return this.g.user && this.VOIDWALLET == true
|
return (
|
||||||
|
this.g.user?.extra?.display_name ||
|
||||||
|
this.g.user.username ||
|
||||||
|
this.g.user?.extra?.first_name ||
|
||||||
|
'Anon'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
displayRole() {
|
||||||
|
if (this.g.user?.super_user) {
|
||||||
|
return 'Super User'
|
||||||
|
} else if (this.g.user?.admin) {
|
||||||
|
return 'Admin'
|
||||||
|
} else {
|
||||||
|
return 'User'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async stopImpersonation() {
|
||||||
|
try {
|
||||||
|
await LNbits.api.stopImpersonation()
|
||||||
|
LNbits.utils.restoreLocalStorage('impersonation')
|
||||||
|
window.location = '/users'
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleLanguageChanged(lang) {
|
||||||
|
try {
|
||||||
|
await LNbits.api.updateUiCustomization({locale: lang.locale})
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Language Updated',
|
||||||
|
caption: lang.locale
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
window.app.component('lnbits-home-logos', {
|
window.app.component('lnbits-home-logos', {
|
||||||
template: '#lnbits-home-logos',
|
template: '#lnbits-home-logos',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
logos: [
|
logos: [
|
||||||
@@ -95,9 +94,9 @@ window.app.component('lnbits-home-logos', {
|
|||||||
computed: {
|
computed: {
|
||||||
showLogos() {
|
showLogos() {
|
||||||
return (
|
return (
|
||||||
this.LNBITS_DENOMINATION == 'sats' &&
|
this.g.isSatsDenomination &&
|
||||||
this.SITE_TITLE == 'LNbits' &&
|
this.g.settings.siteTitle == 'LNbits' &&
|
||||||
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
this.g.settings.showHomePageElements == true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
window.app.component('lnbits-label-selector', {
|
window.app.component('lnbits-label-selector', {
|
||||||
template: '#lnbits-label-selector',
|
template: '#lnbits-label-selector',
|
||||||
props: ['labels'],
|
props: ['labels'],
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
labelFilter: '',
|
labelFilter: '',
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
window.app.component('lnbits-language-dropdown', {
|
window.app.component('lnbits-language-dropdown', {
|
||||||
template: '#lnbits-language-dropdown',
|
template: '#lnbits-language-dropdown',
|
||||||
mixins: [window.windowMixin],
|
computed: {
|
||||||
|
currentLanguage() {
|
||||||
|
return (
|
||||||
|
this.langs.find(lang => lang.value === window.i18n.global.locale) || {
|
||||||
|
value: 'en',
|
||||||
|
label: 'English',
|
||||||
|
display: '🇬🇧 EN'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
activeLanguage(lang) {
|
activeLanguage(lang) {
|
||||||
return window.i18n.global.locale === lang
|
return window.i18n.global.locale === lang
|
||||||
@@ -9,6 +19,7 @@ window.app.component('lnbits-language-dropdown', {
|
|||||||
this.g.locale = newValue
|
this.g.locale = newValue
|
||||||
window.i18n.global.locale = newValue
|
window.i18n.global.locale = newValue
|
||||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||||
|
this.$emit('language-changed', newValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
window.app.component('lnbits-manage-extension-list', {
|
window.app.component('lnbits-manage-extension-list', {
|
||||||
mixins: [window.windowMixin],
|
|
||||||
template: '#lnbits-manage-extension-list',
|
template: '#lnbits-manage-extension-list',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -9,27 +8,18 @@ window.app.component('lnbits-manage-extension-list', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'g.user.extensions': {
|
'g.user.extensions'() {
|
||||||
async handler() {
|
this.loadExtensions()
|
||||||
await this.loadExtensions()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
searchTerm() {
|
searchTerm() {
|
||||||
this.filterUserExtensionsByTerm()
|
this.filterUserExtensionsByTerm()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
map(data) {
|
|
||||||
const obj = {...data}
|
|
||||||
obj.url = ['/', obj.code, '/'].join('')
|
|
||||||
return obj
|
|
||||||
},
|
|
||||||
async loadExtensions() {
|
async loadExtensions() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
|
res = await LNbits.api.request('GET', '/api/v1/extension')
|
||||||
this.extensions = data
|
this.extensions = res.data.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map(extension => this.map(extension))
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
this.filterUserExtensionsByTerm()
|
this.filterUserExtensionsByTerm()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
@@ -45,10 +35,6 @@ window.app.component('lnbits-manage-extension-list', {
|
|||||||
.toLocaleLowerCase()
|
.toLocaleLowerCase()
|
||||||
.includes(this.searchTerm.toLocaleLowerCase())
|
.includes(this.searchTerm.toLocaleLowerCase())
|
||||||
})
|
})
|
||||||
.map(obj => {
|
|
||||||
obj.isActive = window.location.pathname.startsWith(obj.url)
|
|
||||||
return obj
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
|
|||||||
@@ -1,9 +1,91 @@
|
|||||||
window.app.component('lnbits-manage-wallet-list', {
|
window.app.component('lnbits-manage-wallet-list', {
|
||||||
template: '#lnbits-manage-wallet-list',
|
template: '#lnbits-manage-wallet-list',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
walletName: ''
|
activeWalletId: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
maxWallets() {
|
||||||
|
return this.g.user?.extra?.visible_wallet_count || 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
if (to.path.startsWith('/wallet/')) {
|
||||||
|
this.activeWalletId = to.params.id
|
||||||
|
} else {
|
||||||
|
this.activeWalletId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.g.user && this.g.walletEventListeners.length === 0) {
|
||||||
|
this.paymentEvents()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openNewWalletDialog() {
|
||||||
|
if (this.g.user.walletInvitesCount) {
|
||||||
|
this.g.newWalletType = 'lightning-shared'
|
||||||
|
} else {
|
||||||
|
this.g.newWalletType = 'lightning'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onWebsocketMessage(ev) {
|
||||||
|
const data = JSON.parse(ev.data)
|
||||||
|
if (!data.payment) {
|
||||||
|
console.error('ws message no payment', data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// update sidebar wallet balances
|
||||||
|
this.g.user.wallets.forEach(w => {
|
||||||
|
if (w.id === data.payment.wallet_id) {
|
||||||
|
w.sat = data.wallet_balance
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// if current wallet, update balance and payments
|
||||||
|
if (this.g.wallet.id === data.payment.wallet_id) {
|
||||||
|
this.g.wallet.sat = data.wallet_balance
|
||||||
|
// lnbits-payment-list is watching
|
||||||
|
this.g.updatePayments = !this.g.updatePayments
|
||||||
|
this.g.updatePaymentsHash = !this.g.updatePaymentsHash
|
||||||
|
}
|
||||||
|
// NOTE: react only on incoming payments for now
|
||||||
|
if (data.payment.amount > 0) {
|
||||||
|
eventReaction(data.wallet_balance * 1000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
paymentEvents() {
|
||||||
|
if (!this.g.user) return
|
||||||
|
let timeout
|
||||||
|
const wallets = this.g.user.wallets.slice(0, this.maxWallets)
|
||||||
|
wallets.forEach(wallet => {
|
||||||
|
if (!this.g.walletEventListeners.includes(wallet.id)) {
|
||||||
|
this.g.walletEventListeners.push(wallet.id)
|
||||||
|
const ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`)
|
||||||
|
ws.onmessage = this.onWebsocketMessage
|
||||||
|
ws.onopen = () => console.log('ws connected for wallet', wallet.id)
|
||||||
|
// onclose and onerror can both happen on their own or together,
|
||||||
|
// so we add a clearTimeout to avoid multiple reconnections
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('ws closed, reconnecting...', wallet.id)
|
||||||
|
this.g.walletEventListeners = this.g.walletEventListeners.filter(
|
||||||
|
id => id !== wallet.id
|
||||||
|
)
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(this.paymentEvents, 5000)
|
||||||
|
}
|
||||||
|
ws.onerror = () => {
|
||||||
|
console.warn('ws error, reconnecting...', wallet.id)
|
||||||
|
this.g.walletEventListeners = this.g.walletEventListeners.filter(
|
||||||
|
id => id !== wallet.id
|
||||||
|
)
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(this.paymentEvents, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
window.app.component('lnbits-payment-list', {
|
window.app.component('lnbits-payment-list', {
|
||||||
template: '#lnbits-payment-list',
|
template: '#lnbits-payment-list',
|
||||||
props: ['update', 'lazy', 'wallet', 'paymentFilter'],
|
props: ['wallet', 'paymentFilter'],
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
denomination: LNBITS_DENOMINATION,
|
|
||||||
payments: [],
|
payments: [],
|
||||||
paymentsTable: {
|
paymentsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
@@ -18,7 +16,7 @@ window.app.component('lnbits-payment-list', {
|
|||||||
{
|
{
|
||||||
name: 'amount',
|
name: 'amount',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
label: this.$t('amount'),
|
||||||
field: 'sat',
|
field: 'sat',
|
||||||
sortable: true
|
sortable: true
|
||||||
}
|
}
|
||||||
@@ -30,6 +28,13 @@ window.app.component('lnbits-payment-list', {
|
|||||||
descending: true,
|
descending: true,
|
||||||
rowsNumber: 10
|
rowsNumber: 10
|
||||||
},
|
},
|
||||||
|
sortFields: [
|
||||||
|
{name: 'amount', label: 'Amount'},
|
||||||
|
{name: 'fee', label: 'Fee'},
|
||||||
|
{name: 'memo', label: 'Memo'},
|
||||||
|
{name: 'time', label: 'Creation Date'},
|
||||||
|
{name: 'updated_at', label: 'Last Updated'}
|
||||||
|
],
|
||||||
search: '',
|
search: '',
|
||||||
loading: false
|
loading: false
|
||||||
},
|
},
|
||||||
@@ -46,10 +51,10 @@ window.app.component('lnbits-payment-list', {
|
|||||||
paymentsCSV: {
|
paymentsCSV: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'pending',
|
name: 'status',
|
||||||
align: 'left',
|
align: 'right',
|
||||||
label: 'Pending',
|
label: this.$t('status'),
|
||||||
field: 'pending'
|
field: 'status'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'memo',
|
name: 'memo',
|
||||||
@@ -67,14 +72,14 @@ window.app.component('lnbits-payment-list', {
|
|||||||
{
|
{
|
||||||
name: 'amount',
|
name: 'amount',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
label: this.$t('amount'),
|
||||||
field: 'sat',
|
field: 'sat',
|
||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'fee',
|
name: 'fee',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
|
label: this.$t('fee'),
|
||||||
field: 'fee'
|
field: 'fee'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -127,9 +132,6 @@ window.app.component('lnbits-payment-list', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentWallet() {
|
|
||||||
return this.wallet || this.g.wallet
|
|
||||||
},
|
|
||||||
filteredPayments() {
|
filteredPayments() {
|
||||||
const q = this.paymentsTable.search
|
const q = this.paymentsTable.search
|
||||||
if (!q || q === '') return this.payments
|
if (!q || q === '') return this.payments
|
||||||
@@ -147,6 +149,47 @@ window.app.component('lnbits-payment-list', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
mapPayment(data) {
|
||||||
|
const obj = {
|
||||||
|
checking_id: data.checking_id,
|
||||||
|
status: data.status,
|
||||||
|
amount: data.amount,
|
||||||
|
fee: data.fee,
|
||||||
|
memo: data.memo,
|
||||||
|
time: data.time,
|
||||||
|
bolt11: data.bolt11,
|
||||||
|
preimage: data.preimage,
|
||||||
|
payment_hash: data.payment_hash,
|
||||||
|
expiry: data.expiry,
|
||||||
|
extra: data.extra ?? {},
|
||||||
|
wallet_id: data.wallet_id,
|
||||||
|
webhook: data.webhook,
|
||||||
|
webhook_status: data.webhook_status,
|
||||||
|
fiat_amount: data.fiat_amount,
|
||||||
|
fiat_currency: data.fiat_currency,
|
||||||
|
labels: data.labels
|
||||||
|
}
|
||||||
|
obj.date = this.utils.formatDate(data.created_at)
|
||||||
|
obj.dateFrom = this.utils.formatDateFrom(data.created_at)
|
||||||
|
obj.expirydate = this.utils.formatDate(data.expiry)
|
||||||
|
obj.expirydateFrom = this.utils.formatDateFrom(data.expiry)
|
||||||
|
obj.msat = obj.amount
|
||||||
|
obj.sat = obj.msat / 1000
|
||||||
|
obj.tag = obj.extra?.tag
|
||||||
|
obj.fsat = this.utils.formatSat(obj.sat)
|
||||||
|
obj.isIn = obj.amount > 0
|
||||||
|
obj.isOut = obj.amount < 0
|
||||||
|
obj.isPending = obj.status === 'pending'
|
||||||
|
obj.isPaid = obj.status === 'success'
|
||||||
|
obj.isFailed = obj.status === 'failed'
|
||||||
|
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
|
||||||
|
try {
|
||||||
|
obj.details = JSON.parse(data.extra?.details || '{}')
|
||||||
|
} catch {
|
||||||
|
obj.details = {extraDetails: data.extra?.details}
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
},
|
||||||
searchByDate() {
|
searchByDate() {
|
||||||
if (typeof this.searchDate === 'string') {
|
if (typeof this.searchDate === 'string') {
|
||||||
this.searchDate = {
|
this.searchDate = {
|
||||||
@@ -160,7 +203,6 @@ window.app.component('lnbits-payment-list', {
|
|||||||
if (this.searchDate.to) {
|
if (this.searchDate.to) {
|
||||||
this.paymentFilter['time[le]'] = this.searchDate.to + 'T23:59:59'
|
this.paymentFilter['time[le]'] = this.searchDate.to + 'T23:59:59'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchPayments()
|
this.fetchPayments()
|
||||||
},
|
},
|
||||||
searchByLabels(labels) {
|
searchByLabels(labels) {
|
||||||
@@ -184,30 +226,39 @@ window.app.component('lnbits-payment-list', {
|
|||||||
this.fetchPayments()
|
this.fetchPayments()
|
||||||
},
|
},
|
||||||
fetchPayments(props) {
|
fetchPayments(props) {
|
||||||
|
this.paymentsTable.loading = true
|
||||||
const params = LNbits.utils.prepareFilterQuery(
|
const params = LNbits.utils.prepareFilterQuery(
|
||||||
this.paymentsTable,
|
this.paymentsTable,
|
||||||
props,
|
props,
|
||||||
this.paymentFilter
|
this.paymentFilter
|
||||||
)
|
)
|
||||||
this.paymentsTable.loading = true
|
|
||||||
return LNbits.api
|
return LNbits.api
|
||||||
.getPayments(this.currentWallet, params)
|
.getPayments(this.wallet, params)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.paymentsTable.loading = false
|
|
||||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||||
this.payments = response.data.data.map(obj => {
|
this.payments = response.data.data.map(this.mapPayment)
|
||||||
return LNbits.map.payment(obj)
|
this.paymentsTable.loading = false
|
||||||
})
|
this.recheckPendingPayments()
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
this.paymentsTable.loading = false
|
this.paymentsTable.loading = false
|
||||||
if (g.user.admin) {
|
if (g.user.admin) {
|
||||||
this.fetchPaymentsAsAdmin(this.currentWallet.id, params)
|
this.fetchPaymentsAsAdmin(this.wallet.id, params)
|
||||||
} else {
|
} else {
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
sortByColumn(columnName) {
|
||||||
|
if (this.paymentsTable.pagination.sortBy === columnName) {
|
||||||
|
this.paymentsTable.pagination.descending =
|
||||||
|
!this.paymentsTable.pagination.descending
|
||||||
|
} else {
|
||||||
|
this.paymentsTable.pagination.sortBy = columnName
|
||||||
|
this.paymentsTable.pagination.descending = false
|
||||||
|
}
|
||||||
|
this.fetchPayments()
|
||||||
|
},
|
||||||
fetchPaymentsAsAdmin(walletId, params) {
|
fetchPaymentsAsAdmin(walletId, params) {
|
||||||
params = (params || '') + '&wallet_id=' + walletId
|
params = (params || '') + '&wallet_id=' + walletId
|
||||||
return LNbits.api
|
return LNbits.api
|
||||||
@@ -215,9 +266,7 @@ window.app.component('lnbits-payment-list', {
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
this.paymentsTable.loading = false
|
this.paymentsTable.loading = false
|
||||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||||
this.payments = response.data.data.map(obj => {
|
this.payments = response.data.data.map(this.mapPayment)
|
||||||
return LNbits.map.payment(obj)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
this.paymentsTable.loading = false
|
this.paymentsTable.loading = false
|
||||||
@@ -226,7 +275,7 @@ window.app.component('lnbits-payment-list', {
|
|||||||
},
|
},
|
||||||
checkPayment(payment_hash) {
|
checkPayment(payment_hash) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.getPayment(this.g.wallet, payment_hash)
|
.getPayment(this.wallet, payment_hash)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.update = !this.update
|
this.update = !this.update
|
||||||
if (res.data.status == 'success') {
|
if (res.data.status == 'success') {
|
||||||
@@ -244,6 +293,41 @@ window.app.component('lnbits-payment-list', {
|
|||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
|
recheckPendingPayments() {
|
||||||
|
const pendingPayments = this.payments.filter(p => p.status === 'pending')
|
||||||
|
if (pendingPayments.length === 0) return
|
||||||
|
|
||||||
|
const params = [
|
||||||
|
'recheck_pending=true',
|
||||||
|
'checking_id[in]=' + pendingPayments.map(p => p.checking_id).join(',')
|
||||||
|
].join('&')
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.getPayments(this.wallet, params)
|
||||||
|
.then(response => {
|
||||||
|
let updatedPayments = 0
|
||||||
|
response.data.data.forEach(updatedPayment => {
|
||||||
|
if (updatedPayment.status !== 'pending') {
|
||||||
|
const index = this.payments.findIndex(
|
||||||
|
p => p.checking_id === updatedPayment.checking_id
|
||||||
|
)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.payments.splice(index, 1, this.mapPayment(updatedPayment))
|
||||||
|
updatedPayments += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (updatedPayments > 0) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: this.$t('payment_successful')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.warn(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
showHoldInvoiceDialog(payment) {
|
showHoldInvoiceDialog(payment) {
|
||||||
this.hodlInvoice.show = true
|
this.hodlInvoice.show = true
|
||||||
this.hodlInvoice.preimage = ''
|
this.hodlInvoice.preimage = ''
|
||||||
@@ -251,7 +335,7 @@ window.app.component('lnbits-payment-list', {
|
|||||||
},
|
},
|
||||||
cancelHoldInvoice(payment_hash) {
|
cancelHoldInvoice(payment_hash) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.cancelInvoice(this.g.wallet, payment_hash)
|
.cancelInvoice(this.wallet, payment_hash)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.update = !this.update
|
this.update = !this.update
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -263,7 +347,7 @@ window.app.component('lnbits-payment-list', {
|
|||||||
},
|
},
|
||||||
settleHoldInvoice(preimage) {
|
settleHoldInvoice(preimage) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.settleInvoice(this.g.wallet, preimage)
|
.settleInvoice(this.wallet, preimage)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.update = !this.update
|
this.update = !this.update
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -286,8 +370,8 @@ window.app.component('lnbits-payment-list', {
|
|||||||
direction: pagination.descending ? 'desc' : 'asc'
|
direction: pagination.descending ? 'desc' : 'asc'
|
||||||
}
|
}
|
||||||
const params = new URLSearchParams(query)
|
const params = new URLSearchParams(query)
|
||||||
LNbits.api.getPayments(this.g.wallet, params).then(response => {
|
LNbits.api.getPayments(this.wallet, params).then(response => {
|
||||||
let payments = response.data.data.map(LNbits.map.payment)
|
let payments = response.data.data.map(this.mapPayment)
|
||||||
let columns = this.paymentsCSV.columns
|
let columns = this.paymentsCSV.columns
|
||||||
|
|
||||||
if (detailed) {
|
if (detailed) {
|
||||||
@@ -314,7 +398,7 @@ window.app.component('lnbits-payment-list', {
|
|||||||
LNbits.utils.exportCSV(
|
LNbits.utils.exportCSV(
|
||||||
columns,
|
columns,
|
||||||
payments,
|
payments,
|
||||||
this.g.wallet.name + '-payments'
|
this.wallet.name + '-payments'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -383,7 +467,7 @@ window.app.component('lnbits-payment-list', {
|
|||||||
await LNbits.api.request(
|
await LNbits.api.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
`/api/v1/payments/${this.selectedPayment.payment_hash}/labels`,
|
`/api/v1/payments/${this.selectedPayment.payment_hash}/labels`,
|
||||||
this.g.wallet.adminkey,
|
this.wallet.adminkey,
|
||||||
{
|
{
|
||||||
labels: labels
|
labels: labels
|
||||||
}
|
}
|
||||||
@@ -423,23 +507,11 @@ window.app.component('lnbits-payment-list', {
|
|||||||
this.fetchPayments()
|
this.fetchPayments()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
lazy(newVal) {
|
|
||||||
if (newVal === true) this.fetchPayments()
|
|
||||||
},
|
|
||||||
update() {
|
|
||||||
this.fetchPayments()
|
|
||||||
},
|
|
||||||
'g.updatePayments'() {
|
'g.updatePayments'() {
|
||||||
this.fetchPayments()
|
this.fetchPayments()
|
||||||
},
|
|
||||||
'g.wallet': {
|
|
||||||
handler(newWallet) {
|
|
||||||
this.fetchPayments()
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.lazy === undefined) this.fetchPayments()
|
this.fetchPayments()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
window.app.component('lnbits-qrcode-lnurl', {
|
window.app.component('lnbits-qrcode-lnurl', {
|
||||||
template: '#lnbits-qrcode-lnurl',
|
template: '#lnbits-qrcode-lnurl',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
props: {
|
props: {
|
||||||
url: {
|
url: {
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
window.app.component('lnbits-qrcode-scanner', {
|
||||||
|
template: '#lnbits-qrcode-scanner',
|
||||||
|
props: ['callback'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showScanner: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
callback(newVal) {
|
||||||
|
if (newVal === null) {
|
||||||
|
return this.reset()
|
||||||
|
}
|
||||||
|
if (typeof newVal !== 'function') {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'QR code scanner callback is not a function.',
|
||||||
|
type: 'negative'
|
||||||
|
})
|
||||||
|
return this.reset()
|
||||||
|
}
|
||||||
|
if (this.g.hasCamera === false) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'No camera found on this device.',
|
||||||
|
type: 'negative'
|
||||||
|
})
|
||||||
|
return this.reset()
|
||||||
|
}
|
||||||
|
this.showScanner = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
reset() {
|
||||||
|
this.showScanner = false
|
||||||
|
this.g.scanner = null
|
||||||
|
},
|
||||||
|
detect(val) {
|
||||||
|
const rawValue = val[0].rawValue
|
||||||
|
console.log('Detected QR code value:', rawValue)
|
||||||
|
this.callback(rawValue)
|
||||||
|
this.$emit('detect', rawValue)
|
||||||
|
this.reset()
|
||||||
|
},
|
||||||
|
async onInitQR(promise) {
|
||||||
|
try {
|
||||||
|
await promise
|
||||||
|
} catch (error) {
|
||||||
|
const mapping = {
|
||||||
|
NotAllowedError: 'ERROR: you need to grant camera access permission',
|
||||||
|
NotFoundError: 'ERROR: no camera on this device',
|
||||||
|
NotSupportedError:
|
||||||
|
'ERROR: secure context required (HTTPS, localhost)',
|
||||||
|
NotReadableError: 'ERROR: is the camera already in use?',
|
||||||
|
OverconstrainedError: 'ERROR: installed cameras are not suitable',
|
||||||
|
StreamApiNotSupportedError:
|
||||||
|
'ERROR: Stream API is not supported in this browser',
|
||||||
|
InsecureContextError:
|
||||||
|
'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
|
||||||
|
}
|
||||||
|
const valid_error = Object.keys(mapping).filter(key => {
|
||||||
|
return error.name === key
|
||||||
|
})
|
||||||
|
const camera_error = valid_error
|
||||||
|
? mapping[valid_error]
|
||||||
|
: `ERROR: Camera error (${error.name})`
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: camera_error,
|
||||||
|
type: 'negative'
|
||||||
|
})
|
||||||
|
this.g.hasCamera = false
|
||||||
|
this.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
window.app.component('lnbits-qrcode', {
|
window.app.component('lnbits-qrcode', {
|
||||||
mixins: [window.windowMixin],
|
|
||||||
template: '#lnbits-qrcode',
|
template: '#lnbits-qrcode',
|
||||||
components: {
|
components: {
|
||||||
QrcodeVue
|
QrcodeVue: QrcodeVue.default
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
@@ -13,6 +12,10 @@ window.app.component('lnbits-qrcode', {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
print: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
showButtons: {
|
showButtons: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
@@ -31,7 +34,7 @@ window.app.component('lnbits-qrcode', {
|
|||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
type: String,
|
type: String,
|
||||||
default: LNBITS_QR_LOGO
|
default: window.g.settings.qrLogo || null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -41,9 +44,44 @@ window.app.component('lnbits-qrcode', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
printQrCode() {
|
||||||
|
const svg = this.$refs.qrCode.$el.outerHTML
|
||||||
|
const printWindow = window.open('', '_blank')
|
||||||
|
printWindow.document.write(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Print QR Code</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 51px;
|
||||||
|
width: 14%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: -1.2rem;
|
||||||
|
border-radius: -1.2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>${svg}<img src="${this.logo}"></body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
printWindow.document.close()
|
||||||
|
printWindow.focus()
|
||||||
|
printWindow.print()
|
||||||
|
printWindow.close()
|
||||||
|
},
|
||||||
clickQrCode(event) {
|
clickQrCode(event) {
|
||||||
if (this.href === '') {
|
if (this.href === '') {
|
||||||
this.copyText(this.value)
|
this.utils.copyText(this.value)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
window.app.component('lnbits-theme', {
|
window.app.component('lnbits-theme', {
|
||||||
mixins: [window.windowMixin],
|
|
||||||
watch: {
|
watch: {
|
||||||
|
'g.walletFlip'(val) {
|
||||||
|
this.$q.localStorage.setItem('lnbits.walletFlip', val)
|
||||||
|
if (val === true && this.$q.screen.lt.md) {
|
||||||
|
this.g.visibleDrawer = false
|
||||||
|
}
|
||||||
|
},
|
||||||
'g.disclaimerShown'(val) {
|
'g.disclaimerShown'(val) {
|
||||||
this.$q.localStorage.setItem('lnbits.disclaimerShown', val)
|
this.$q.localStorage.setItem('lnbits.disclaimerShown', val)
|
||||||
},
|
},
|
||||||
|
'g.locale'(val) {
|
||||||
|
this.$q.localStorage.setItem('lnbits.lang', val)
|
||||||
|
window.i18n.global.locale = val
|
||||||
|
},
|
||||||
'g.isFiatPriority'(val) {
|
'g.isFiatPriority'(val) {
|
||||||
this.$q.localStorage.setItem('lnbits.isFiatPriority', val)
|
this.$q.localStorage.setItem('lnbits.isFiatPriority', val)
|
||||||
},
|
},
|
||||||
@@ -118,6 +127,13 @@ window.app.component('lnbits-theme', {
|
|||||||
if (this.g.mobileSimple === true) {
|
if (this.g.mobileSimple === true) {
|
||||||
document.body.classList.add('mobile-simple')
|
document.body.classList.add('mobile-simple')
|
||||||
}
|
}
|
||||||
|
Object.entries(this.g.user?.uiCustomization || {}).forEach(
|
||||||
|
([key, value]) => {
|
||||||
|
if (key in this.g) {
|
||||||
|
this.g[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
this.checkUrlParams()
|
this.checkUrlParams()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
window.app.component('lnbits-wallet-api-docs', {
|
window.app.component('lnbits-wallet-api-docs', {
|
||||||
template: '#lnbits-wallet-api-docs',
|
template: '#lnbits-wallet-api-docs',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
methods: {
|
methods: {
|
||||||
resetKeys() {
|
resetKeys() {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
@@ -39,6 +38,7 @@ window.app.component('lnbits-wallet-api-docs', {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
origin: window.location.origin,
|
||||||
inkeyHidden: true,
|
inkeyHidden: true,
|
||||||
adminkeyHidden: true,
|
adminkeyHidden: true,
|
||||||
walletIdHidden: true
|
walletIdHidden: true
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
window.app.component('lnbits-wallet-charts', {
|
window.app.component('lnbits-wallet-charts', {
|
||||||
template: '#lnbits-wallet-charts',
|
template: '#lnbits-wallet-charts',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
props: ['paymentFilter', 'chartConfig'],
|
props: ['paymentFilter', 'chartConfig'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -102,63 +101,62 @@ window.app.component('lnbits-wallet-charts', {
|
|||||||
if (this.walletBalanceInOut) {
|
if (this.walletBalanceInOut) {
|
||||||
this.walletBalanceInOut.destroy()
|
this.walletBalanceInOut.destroy()
|
||||||
}
|
}
|
||||||
this.walletBalanceInOut = new Chart(
|
const ref = this.$refs.walletBalanceInOut
|
||||||
this.$refs.walletBalanceInOut.getContext('2d'),
|
if (!ref) return
|
||||||
{
|
this.walletBalanceInOut = new Chart(ref.getContext('2d'), {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
options: this.barOptions,
|
options: this.barOptions,
|
||||||
data: {
|
data: {
|
||||||
labels,
|
labels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Balance In',
|
label: 'Balance In',
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
data: data.map(s => s.balance_in),
|
data: data.map(s => s.balance_in),
|
||||||
backgroundColor: this.colorPrimary
|
backgroundColor: this.colorPrimary
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Balance Out',
|
label: 'Balance Out',
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
data: data.map(s => s.balance_out),
|
data: data.map(s => s.balance_out),
|
||||||
backgroundColor: this.colorSecondary
|
backgroundColor: this.colorSecondary
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
},
|
},
|
||||||
drawPaymentInOut(data, labels) {
|
drawPaymentInOut(data, labels) {
|
||||||
if (this.walletPaymentInOut) {
|
if (this.walletPaymentInOut) {
|
||||||
this.walletPaymentInOut.destroy()
|
this.walletPaymentInOut.destroy()
|
||||||
}
|
}
|
||||||
this.walletPaymentInOut = new Chart(
|
const ref = this.$refs.walletPaymentInOut
|
||||||
this.$refs.walletPaymentInOut.getContext('2d'),
|
if (!ref) return
|
||||||
{
|
this.walletPaymentInOut = new Chart(ref.getContext('2d'), {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
options: this.barOptions,
|
options: this.barOptions,
|
||||||
data: {
|
data: {
|
||||||
labels,
|
labels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Payments In',
|
label: 'Payments In',
|
||||||
data: data.map(s => s.count_in),
|
data: data.map(s => s.count_in),
|
||||||
backgroundColor: this.colorPrimary
|
backgroundColor: this.colorPrimary
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Payments Out',
|
label: 'Payments Out',
|
||||||
data: data.map(s => -s.count_out),
|
data: data.map(s => -s.count_out),
|
||||||
backgroundColor: this.colorSecondary
|
backgroundColor: this.colorSecondary
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
},
|
},
|
||||||
drawBalanceChart(data, labels) {
|
drawBalanceChart(data, labels) {
|
||||||
const ref = this.$refs.walletBalanceChart
|
|
||||||
if (this.walletBalanceChart) {
|
if (this.walletBalanceChart) {
|
||||||
this.walletBalanceChart.destroy()
|
this.walletBalanceChart.destroy()
|
||||||
}
|
}
|
||||||
|
const ref = this.$refs.walletBalanceChart
|
||||||
|
if (!ref) return
|
||||||
this.walletBalanceChart = new Chart(ref.getContext('2d'), {
|
this.walletBalanceChart = new Chart(ref.getContext('2d'), {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
window.app.component('lnbits-wallet-extra', {
|
||||||
|
template: '#lnbits-wallet-extra',
|
||||||
|
props: ['chartConfig'],
|
||||||
|
computed: {
|
||||||
|
exportUrl() {
|
||||||
|
return `${window.location.origin}/wallet?usr=${this.g.user.id}&wal=${this.g.wallet.id}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSendLnurl(lnurl) {
|
||||||
|
this.$emit('send-lnurl', lnurl)
|
||||||
|
},
|
||||||
|
updateWallet(wallet) {
|
||||||
|
this.$emit('update-wallet', wallet)
|
||||||
|
},
|
||||||
|
handleFiatTracking() {
|
||||||
|
this.g.fiatTracking = !this.g.fiatTracking
|
||||||
|
if (!this.g.fiatTracking) {
|
||||||
|
this.g.isFiatPriority = false
|
||||||
|
this.g.wallet.currency = ''
|
||||||
|
this.updateWallet({currency: ''})
|
||||||
|
} else {
|
||||||
|
this.updateWallet({currency: this.g.wallet.currency})
|
||||||
|
this.updateFiatBalance()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteWallet() {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||||
|
.onOk(() => {
|
||||||
|
LNbits.api
|
||||||
|
.deleteWallet(this.g.wallet)
|
||||||
|
.then(() => {
|
||||||
|
this.g.user.wallets = this.g.user.wallets.filter(
|
||||||
|
w => w.id !== this.g.wallet.id
|
||||||
|
)
|
||||||
|
this.g.lastActiveWallet = this.g.user.wallets[0].id
|
||||||
|
this.$router.push(`/wallet/${this.g.lastActiveWallet}`)
|
||||||
|
Quasar.Notify.create({
|
||||||
|
timeout: 3000,
|
||||||
|
message: `Wallet deleted!`,
|
||||||
|
spinner: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateFiatBalance() {
|
||||||
|
// set rate from local storage to avoid clunky api calls
|
||||||
|
if (
|
||||||
|
this.$q.localStorage.getItem(
|
||||||
|
'lnbits.exchangeRate.' + this.g.wallet.currency
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.g.exchangeRate = this.$q.localStorage.getItem(
|
||||||
|
'lnbits.exchangeRate.' + this.g.wallet.currency
|
||||||
|
)
|
||||||
|
this.g.fiatBalance =
|
||||||
|
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||||
|
}
|
||||||
|
LNbits.api
|
||||||
|
.request('GET', `/api/v1/rate/` + this.g.wallet.currency, null)
|
||||||
|
.then(response => {
|
||||||
|
this.g.fiatBalance =
|
||||||
|
(response.data.price / 100000000) * this.g.wallet.sat
|
||||||
|
this.g.exchangeRate = response.data.price.toFixed(2)
|
||||||
|
this.g.fiatTracking = true
|
||||||
|
this.$q.localStorage.set(
|
||||||
|
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
||||||
|
this.g.exchangeRate
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch(e => console.error(e))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.g.wallet.currency !== '' && this.g.isSatsDenomination) {
|
||||||
|
this.g.fiatTracking = true
|
||||||
|
this.updateFiatBalance()
|
||||||
|
} else {
|
||||||
|
this.g.fiatTracking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
window.app.component('lnbits-wallet-icon', {
|
window.app.component('lnbits-wallet-icon', {
|
||||||
template: '#lnbits-wallet-icon',
|
template: '#lnbits-wallet-icon',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
icon: {
|
icon: {
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
window.app.component('lnbits-wallet-new', {
|
window.app.component('lnbits-wallet-new', {
|
||||||
template: '#lnbits-wallet-new',
|
template: '#lnbits-wallet-new',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
walletTypes: [{label: 'Lightning Wallet', value: 'lightning'}],
|
walletTypes: [{label: 'Lightning Wallet', value: 'lightning'}],
|
||||||
newWallet: {name: '', sharedWalletId: ''}
|
wallet: {name: '', sharedWalletId: ''},
|
||||||
|
showNewWalletDialog: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'g.newWalletType'(val) {
|
||||||
|
if (val === null) return
|
||||||
|
this.showNewWalletDialog = true
|
||||||
|
},
|
||||||
|
showNewWalletDialog(val) {
|
||||||
|
if (val === true) return
|
||||||
|
this.reset()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isLightning() {
|
||||||
|
return this.g.newWalletType === 'lightning'
|
||||||
|
},
|
||||||
|
isLightningShared() {
|
||||||
|
return this.g.newWalletType === 'lightning-shared'
|
||||||
|
},
|
||||||
inviteWalletOptions() {
|
inviteWalletOptions() {
|
||||||
return (this.g.user?.extra?.wallet_invite_requests || []).map(i => ({
|
return (this.g.user?.extra?.wallet_invite_requests || []).map(i => ({
|
||||||
label: `${i.to_wallet_name} (from ${i.from_user_name})`,
|
label: `${i.to_wallet_name} (from ${i.from_user_name})`,
|
||||||
@@ -16,11 +32,16 @@ window.app.component('lnbits-wallet-new', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
reset() {
|
||||||
|
this.showNewWalletDialog = false
|
||||||
|
this.g.newWalletType = null
|
||||||
|
this.wallet = {name: '', sharedWalletId: ''}
|
||||||
|
},
|
||||||
async submitRejectWalletInvitation() {
|
async submitRejectWalletInvitation() {
|
||||||
try {
|
try {
|
||||||
const inviteRequests = this.g.user.extra.wallet_invite_requests || []
|
const inviteRequests = this.g.user.extra.wallet_invite_requests || []
|
||||||
const invite = inviteRequests.find(
|
const invite = inviteRequests.find(
|
||||||
invite => invite.to_wallet_id === this.newWallet.sharedWalletId
|
invite => invite.to_wallet_id === this.wallet.sharedWalletId
|
||||||
)
|
)
|
||||||
if (!invite) {
|
if (!invite) {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -46,8 +67,8 @@ window.app.component('lnbits-wallet-new', {
|
|||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submitAddWallet() {
|
submitAddWallet() {
|
||||||
const data = this.newWallet
|
const data = this.wallet
|
||||||
if (this.g.newWalletType === 'lightning' && !data.name) {
|
if (this.g.newWalletType === 'lightning' && !data.name) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: 'Please enter a name for the wallet',
|
message: 'Please enter a name for the wallet',
|
||||||
@@ -55,7 +76,6 @@ window.app.component('lnbits-wallet-new', {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.g.newWalletType === 'lightning-shared' && !data.sharedWalletId) {
|
if (this.g.newWalletType === 'lightning-shared' && !data.sharedWalletId) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: 'Missing a shared wallet ID',
|
message: 'Missing a shared wallet ID',
|
||||||
@@ -63,19 +83,21 @@ window.app.component('lnbits-wallet-new', {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
LNbits.api
|
||||||
await LNbits.api.createWallet(
|
.createWallet(data.name, this.g.newWalletType, {
|
||||||
this.g.user.wallets[0],
|
shared_wallet_id: data.sharedWalletId
|
||||||
data.name,
|
})
|
||||||
this.g.newWalletType,
|
.then(res => {
|
||||||
{
|
this.$q.notify({
|
||||||
shared_wallet_id: data.sharedWalletId
|
message: 'Wallet created successfully',
|
||||||
}
|
color: 'positive'
|
||||||
)
|
})
|
||||||
} catch (e) {
|
this.reset()
|
||||||
console.warn(e)
|
this.g.user.wallets.push(LNbits.map.wallet(res.data))
|
||||||
LNbits.utils.notifyApiError(e)
|
this.g.lastWalletId = res.data.id
|
||||||
}
|
this.$router.push(`/wallet/${res.data.id}`)
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
window.app.component('lnbits-wallet-paylinks', {
|
window.app.component('lnbits-wallet-paylinks', {
|
||||||
template: '#lnbits-wallet-paylinks',
|
template: '#lnbits-wallet-paylinks',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
storedPaylinks: []
|
storedPaylinks: []
|
||||||
@@ -15,10 +14,6 @@ window.app.component('lnbits-wallet-paylinks', {
|
|||||||
this.storedPaylinks = this.g.wallet.storedPaylinks
|
this.storedPaylinks = this.g.wallet.storedPaylinks
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
dateFromNow(unix) {
|
|
||||||
const date = new Date(unix * 1000)
|
|
||||||
return moment.utc(date).local().fromNow()
|
|
||||||
},
|
|
||||||
updatePaylinks() {
|
updatePaylinks() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
window.app.component('lnbits-wallet-share', {
|
window.app.component('lnbits-wallet-share', {
|
||||||
template: '#lnbits-wallet-share',
|
template: '#lnbits-wallet-share',
|
||||||
mixins: [window.windowMixin],
|
|
||||||
computed: {
|
computed: {
|
||||||
walletApprovedShares() {
|
walletApprovedShares() {
|
||||||
return this.g.wallet.extra.shared_with.filter(
|
return this.g.wallet.extra.shared_with.filter(
|
||||||
@@ -26,7 +25,7 @@ window.app.component('lnbits-wallet-share', {
|
|||||||
{label: 'Send', value: 'send-payments'}
|
{label: 'Send', value: 'send-payments'}
|
||||||
],
|
],
|
||||||
walletShareInvite: {
|
walletShareInvite: {
|
||||||
unsername: '',
|
username: '',
|
||||||
permissions: []
|
permissions: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -473,7 +473,6 @@ function confettiStars() {
|
|||||||
F = t.width * I.x,
|
F = t.width * I.x,
|
||||||
N = t.height * I.y;
|
N = t.height * I.y;
|
||||||
E--;
|
E--;
|
||||||
|
|
||||||
)
|
)
|
||||||
S.push(
|
S.push(
|
||||||
((o = {
|
((o = {
|
||||||
|
|||||||
+44
-36
@@ -9,48 +9,50 @@ const localStore = (key, defaultValue) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.g = Vue.reactive({
|
window.g = Vue.reactive({
|
||||||
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
// vars from server
|
||||||
offline: !navigator.onLine,
|
settings: SETTINGS,
|
||||||
visibleDrawer: false,
|
currencies: CURRENCIES,
|
||||||
extensions: [],
|
extensions: SETTINGS.extensions,
|
||||||
user: null,
|
allowedCurrencies: SETTINGS.allowedCurrencies,
|
||||||
wallet: {},
|
denomination: SETTINGS.denomination,
|
||||||
fiatBalance: 0,
|
isSatsDenomination: SETTINGS.denomination == 'sats',
|
||||||
exchangeRate: 0,
|
// local storage vars
|
||||||
fiatTracking: false,
|
themeChoice: localStore('lnbits.theme', SETTINGS.defaultTheme),
|
||||||
wallets: [],
|
borderChoice: localStore('lnbits.border', SETTINGS.defaultBorder),
|
||||||
payments: [],
|
gradientChoice: localStore('lnbits.gradientBg', SETTINGS.defaultGradient),
|
||||||
walletEventListeners: [],
|
reactionChoice: localStore('lnbits.reactions', SETTINGS.defaultReaction),
|
||||||
showNewWalletDialog: false,
|
bgimageChoice: localStore(
|
||||||
newWalletType: 'lightning',
|
'lnbits.backgroundImage',
|
||||||
updatePayments: false,
|
SETTINGS.defaultBgimage || ''
|
||||||
updatePaymentsHash: '',
|
),
|
||||||
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
|
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
|
||||||
disclaimerShown: localStore('lnbits.disclaimerShown', false),
|
disclaimerShown: localStore('lnbits.disclaimerShown', false),
|
||||||
isFiatPriority: localStore('lnbits.isFiatPriority', false),
|
isFiatPriority: localStore('lnbits.isFiatPriority', false),
|
||||||
mobileSimple: localStore('lnbits.mobileSimple', true),
|
mobileSimple: localStore('lnbits.mobileSimple', true),
|
||||||
walletFlip: localStore('lnbits.walletFlip', false),
|
walletFlip: localStore('lnbits.walletFlip', false),
|
||||||
|
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
|
||||||
darkChoice: localStore('lnbits.darkMode', true),
|
darkChoice: localStore('lnbits.darkMode', true),
|
||||||
themeChoice: localStore('lnbits.theme', WINDOW_SETTINGS.LNBITS_DEFAULT_THEME),
|
// cookie vars
|
||||||
borderChoice: localStore(
|
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
||||||
'lnbits.border',
|
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
|
||||||
WINDOW_SETTINGS.LNBITS_DEFAULT_BORDER || 'hard-border'
|
// frontend vars
|
||||||
),
|
errorCode: null,
|
||||||
gradientChoice: localStore(
|
errorMessage: null,
|
||||||
'lnbits.gradientBg',
|
user: null,
|
||||||
WINDOW_SETTINGS.LNBITS_DEFAULT_GRADIENT || false
|
wallet: null,
|
||||||
),
|
isPublicPage: true,
|
||||||
reactionChoice: localStore(
|
offline: !navigator.onLine,
|
||||||
'lnbits.reactions',
|
hasCamera: false,
|
||||||
WINDOW_SETTINGS.LNBITS_DEFAULT_REACTION || 'confettiBothSides'
|
visibleDrawer: false,
|
||||||
),
|
fiatBalance: 0,
|
||||||
bgimageChoice: localStore(
|
exchangeRate: 0,
|
||||||
'lnbits.backgroundImage',
|
fiatTracking: false,
|
||||||
WINDOW_SETTINGS.LNBITS_DEFAULT_BGIMAGE || ''
|
payments: [],
|
||||||
),
|
walletEventListeners: [],
|
||||||
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
|
updatePayments: false, // used for updating the lnbits-payment-list
|
||||||
denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION,
|
updatePaymentsHash: false, // used for closing the receive dialog
|
||||||
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats'
|
scanner: null,
|
||||||
|
newWalletType: null
|
||||||
})
|
})
|
||||||
|
|
||||||
window.dateFormat = 'YYYY-MM-DD HH:mm'
|
window.dateFormat = 'YYYY-MM-DD HH:mm'
|
||||||
@@ -76,3 +78,9 @@ if (navigator.serviceWorker != null) {
|
|||||||
console.log('Registered events at scope: ', registration.scope)
|
console.log('Registered events at scope: ', registration.scope)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
|
||||||
|
navigator.mediaDevices.enumerateDevices().then(devices => {
|
||||||
|
window.g.hasCamera = devices.some(device => device.kind === 'videoinput')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user