Compare commits
104
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0ff997ec1 | ||
|
|
eb55170905 | ||
|
|
dc466ae738 | ||
|
|
dd5fe8b7a2 | ||
|
|
5196ee6a15 | ||
|
|
3498cfb5b8 | ||
|
|
30ffe437a0 | ||
|
|
574641d432 | ||
|
|
90d14629d9 | ||
|
|
3be06269c2 | ||
|
|
d7723696d8 | ||
|
|
91f7b7730d | ||
|
|
b2aaf97cf3 | ||
|
|
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 |
@@ -23,3 +23,10 @@ mypy.ini
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
package.json
|
package.json
|
||||||
pytest.ini
|
pytest.ini
|
||||||
|
|
||||||
|
.mypy_cache
|
||||||
|
.github
|
||||||
|
.pytest_cache
|
||||||
|
.vscode
|
||||||
|
bin
|
||||||
|
dist
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ ENABLE_LOG_TO_FILE=true
|
|||||||
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
|
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
|
||||||
LOG_ROTATION="100 MB"
|
LOG_ROTATION="100 MB"
|
||||||
LOG_RETENTION="3 months"
|
LOG_RETENTION="3 months"
|
||||||
|
|
||||||
# for database cleanup commands
|
# for database cleanup commands
|
||||||
# CLEANUP_WALLETS_DAYS=90
|
# CLEANUP_WALLETS_DAYS=90
|
||||||
|
|
||||||
@@ -187,7 +186,6 @@ BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
|
|||||||
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
||||||
# HEXSTRING instead of path also possible
|
# HEXSTRING instead of path also possible
|
||||||
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
||||||
BOLTZ_CLIENT_WALLET="lnbits"
|
|
||||||
|
|
||||||
# StrikeWallet
|
# StrikeWallet
|
||||||
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
||||||
@@ -333,4 +331,3 @@ LNBITS_RESERVE_FEE_PERCENT=1.0
|
|||||||
######################################
|
######################################
|
||||||
###### Logging and Development #######
|
###### Logging and Development #######
|
||||||
######################################
|
######################################
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
|
|
||||||
|
test-frontend:
|
||||||
|
needs: [ lint ]
|
||||||
|
uses: ./.github/workflows/make.yml
|
||||||
|
with:
|
||||||
|
make: test-frontend
|
||||||
|
npm: true
|
||||||
|
|
||||||
test-api:
|
test-api:
|
||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
@@ -75,7 +82,14 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10"]
|
python-version: ["3.10"]
|
||||||
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
|
backend-wallet-class:
|
||||||
|
- BoltzWallet
|
||||||
|
- LndRestWallet
|
||||||
|
- LndWallet
|
||||||
|
- CoreLightningWallet
|
||||||
|
- CoreLightningRestWallet
|
||||||
|
- LNbitsWallet
|
||||||
|
- EclairWallet
|
||||||
with:
|
with:
|
||||||
custom-pytest: "uv run pytest tests/regtest"
|
custom-pytest: "uv run pytest tests/regtest"
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|||||||
@@ -62,3 +62,4 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
build-args: LNBITS_TAG=${{ inputs.tag }}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||||
cd docker
|
cd docker
|
||||||
chmod +x ./tests
|
chmod +x ./start-regtest
|
||||||
./tests
|
./start-regtest
|
||||||
sudo chmod -R a+rwx .
|
sudo chmod -R a+rwx .
|
||||||
|
|
||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
@@ -63,6 +63,8 @@ jobs:
|
|||||||
LNBITS_ENDPOINT: http://localhost:5001
|
LNBITS_ENDPOINT: http://localhost:5001
|
||||||
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
|
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
|
||||||
ECLAIR_URL: http://127.0.0.1:8082
|
ECLAIR_URL: http://127.0.0.1:8082
|
||||||
|
BOLTZ_CLIENT_ENDPOINT: 127.0.0.1:9002
|
||||||
|
BOLTZ_MNEMONIC: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
|
||||||
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
|
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
|
||||||
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
|
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
|
||||||
ECLAIR_PASS: lnbits
|
ECLAIR_PASS: lnbits
|
||||||
|
|||||||
@@ -10,7 +10,29 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
upload_url: ${{ steps.get_upload_url.outputs.upload_url }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Create github pre-release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
gh release create "$tag" --prerelease --generate-notes --draft
|
||||||
|
- id: get_upload_url
|
||||||
|
name: Get upload url of Github release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
upload_url=$(gh release view "$tag" --json uploadUrl -q ".uploadUrl")
|
||||||
|
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
|
if: github.repository == 'lnbits/lnbits'
|
||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.ref_name }}
|
tag: ${{ github.ref_name }}
|
||||||
@@ -19,6 +41,7 @@ jobs:
|
|||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
pypi:
|
pypi:
|
||||||
|
if: github.repository == 'lnbits/lnbits'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies for building secp256k1
|
- name: Install dependencies for building secp256k1
|
||||||
@@ -30,3 +53,10 @@ jobs:
|
|||||||
uses: JRubics/poetry-publish@v1.15
|
uses: JRubics/poetry-publish@v1.15
|
||||||
with:
|
with:
|
||||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||||
|
|
||||||
|
appimage:
|
||||||
|
needs: [ release ]
|
||||||
|
uses: ./.github/workflows/appimage.yml
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||||
|
|||||||
@@ -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,6 +53,7 @@ jobs:
|
|||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
pypi:
|
pypi:
|
||||||
|
if: github.repository == 'lnbits/lnbits'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies for building secp256k1
|
- name: Install dependencies for building secp256k1
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: tests
|
name: tests pytest
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ dev:
|
|||||||
docker:
|
docker:
|
||||||
docker build -t lnbits/lnbits .
|
docker build -t lnbits/lnbits .
|
||||||
|
|
||||||
|
test-frontend:
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
|
|
||||||
test-wallets:
|
test-wallets:
|
||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -33,9 +33,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 +46,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 +69,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 +124,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 +133,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)
|
||||||
|
|
||||||
@@ -326,6 +325,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 +361,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
|
||||||
|
|||||||
+17
-13
@@ -30,9 +30,9 @@ async def create_account(
|
|||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def update_account(account: Account) -> Account:
|
async def update_account(account: Account, conn: Connection | None = None) -> Account:
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
await db.update("accounts", account)
|
await (conn or db).update("accounts", account)
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
@@ -171,21 +171,23 @@ async def get_account_by_username_or_email(
|
|||||||
|
|
||||||
|
|
||||||
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
||||||
account = await get_account(user_id, conn)
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
if not account:
|
account = await get_account(user_id, conn=conn)
|
||||||
return None
|
if not account:
|
||||||
return await get_user_from_account(account, conn)
|
return None
|
||||||
|
return await get_user_from_account(account, conn=conn)
|
||||||
|
|
||||||
|
|
||||||
async def get_user_from_account(
|
async def get_user_from_account(
|
||||||
account: Account, conn: Connection | None = None
|
account: Account, conn: Connection | None = None
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
extensions = await get_user_active_extensions_ids(account.id, conn=conn)
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
wallets = await get_wallets(account.id, deleted=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:
|
if len(wallets) == 0:
|
||||||
wallet = await create_wallet(user_id=account.id, conn=conn)
|
wallet = await create_wallet(user_id=account.id, conn=conn)
|
||||||
wallets.append(wallet)
|
wallets.append(wallet)
|
||||||
|
|
||||||
return User(
|
return User(
|
||||||
id=account.id,
|
id=account.id,
|
||||||
@@ -205,9 +207,11 @@ async def get_user_from_account(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def update_user_access_control_list(user_acls: UserAcls):
|
async def update_user_access_control_list(
|
||||||
|
user_acls: UserAcls, conn: Connection | None = None
|
||||||
|
):
|
||||||
user_acls.updated_at = datetime.now(timezone.utc)
|
user_acls.updated_at = datetime.now(timezone.utc)
|
||||||
await db.update("accounts", user_acls)
|
await (conn or db).update("accounts", user_acls)
|
||||||
|
|
||||||
|
|
||||||
async def get_user_access_control_lists(
|
async def get_user_access_control_lists(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -51,6 +52,11 @@ async def delete_wallet(
|
|||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
now = int(time())
|
now = int(time())
|
||||||
|
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}")
|
||||||
|
|
||||||
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"""
|
||||||
@@ -240,6 +246,24 @@ 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 id, "user", wallet_type, adminkey, inkey FROM wallets
|
||||||
|
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
||||||
|
""",
|
||||||
|
{"key": key},
|
||||||
|
BaseWallet,
|
||||||
|
)
|
||||||
|
if not wallet:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
async def get_source_wallet(
|
async def get_source_wallet(
|
||||||
wallet: Wallet, conn: Connection | None = None
|
wallet: Wallet, conn: Connection | None = None
|
||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
|
|||||||
@@ -786,3 +786,65 @@ 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}");
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|||||||
@@ -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,6 +6,7 @@ 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
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ from lnbits.helpers import (
|
|||||||
version_parse,
|
version_parse,
|
||||||
)
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
|
|
||||||
class ExplicitRelease(BaseModel):
|
class ExplicitRelease(BaseModel):
|
||||||
@@ -606,6 +608,37 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_installable_extensions(
|
async def get_installable_extensions(
|
||||||
|
cls, post_refresh_cache: bool = False
|
||||||
|
) -> list[InstallableExtension]:
|
||||||
|
extension_list: list[InstallableExtension] = []
|
||||||
|
|
||||||
|
cache_key = "extensions:installable"
|
||||||
|
cache_value = cache.value(cache_key)
|
||||||
|
if not cache_value:
|
||||||
|
extension_list = await cls._get_installable_extensions()
|
||||||
|
cache.set(cache_key, extension_list, expiry=3600) # one hour
|
||||||
|
return extension_list
|
||||||
|
|
||||||
|
if cache_value.older_than(10 * 60) or post_refresh_cache:
|
||||||
|
# refresh cache in background if older than 10 minutes or requested
|
||||||
|
create_task(cls._refresh_installable_extensions_cache())
|
||||||
|
|
||||||
|
extension_list = cache_value.value # type: ignore
|
||||||
|
return extension_list
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _refresh_installable_extensions_cache(
|
||||||
|
cls,
|
||||||
|
) -> None:
|
||||||
|
cache_key = "extensions:installable"
|
||||||
|
extension_list: list[InstallableExtension] = (
|
||||||
|
await cls._get_installable_extensions()
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.set(cache_key, extension_list, expiry=3600)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _get_installable_extensions(
|
||||||
cls,
|
cls,
|
||||||
) -> list[InstallableExtension]:
|
) -> list[InstallableExtension]:
|
||||||
extension_list: list[InstallableExtension] = []
|
extension_list: list[InstallableExtension] = []
|
||||||
|
|||||||
@@ -196,7 +196,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,8 +172,15 @@ class UserAcls(BaseModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Account(BaseModel):
|
class AccountId(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_admin_id(self) -> bool:
|
||||||
|
return settings.is_admin_user(self.id)
|
||||||
|
|
||||||
|
|
||||||
|
class Account(AccountId):
|
||||||
external_id: str | None = None # for external account linking
|
external_id: str | None = None # for external account linking
|
||||||
username: str | None = None
|
username: str | None = None
|
||||||
password_hash: str | None = None
|
password_hash: str | None = None
|
||||||
@@ -194,6 +201,10 @@ class Account(BaseModel):
|
|||||||
self.is_admin = settings.is_admin_user(self.id)
|
self.is_admin = settings.is_admin_user(self.id)
|
||||||
self.fiat_providers = settings.get_fiat_providers_for_user(self.id)
|
self.fiat_providers = settings.get_fiat_providers_for_user(self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_password(self) -> bool:
|
||||||
|
return self.password_hash is not None
|
||||||
|
|
||||||
def hash_password(self, password: str) -> str:
|
def hash_password(self, password: str) -> str:
|
||||||
"""sets and returns the hashed password"""
|
"""sets and returns the hashed password"""
|
||||||
salt = gensalt()
|
salt = gensalt()
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,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(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from lnbits.core.crud.extensions import (
|
|||||||
update_installed_extension,
|
update_installed_extension,
|
||||||
)
|
)
|
||||||
from lnbits.core.helpers import migrate_extension_database
|
from lnbits.core.helpers import migrate_extension_database
|
||||||
|
from lnbits.db import Connection
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
||||||
@@ -149,9 +150,9 @@ async def start_extension_background_work(ext_id: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def get_valid_extensions(
|
async def get_valid_extensions(
|
||||||
include_deactivated: bool | None = True,
|
include_deactivated: bool | None = True, conn: Connection | None = None
|
||||||
) -> list[Extension]:
|
) -> list[Extension]:
|
||||||
installed_extensions = await get_installed_extensions()
|
installed_extensions = await get_installed_extensions(conn=conn)
|
||||||
valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions]
|
valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions]
|
||||||
|
|
||||||
if include_deactivated:
|
if include_deactivated:
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
@@ -70,6 +72,63 @@ def check_stripe_signature(
|
|||||||
raise ValueError("Stripe signature verification failed.")
|
raise ValueError("Stripe signature verification failed.")
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_paypal_webhook(headers, payload: bytes):
|
||||||
|
"""
|
||||||
|
Validate PayPal webhook signatures using the PayPal verify API.
|
||||||
|
"""
|
||||||
|
webhook_id = settings.paypal_webhook_id
|
||||||
|
if not webhook_id:
|
||||||
|
logger.warning("PayPal webhook ID not set; skipping verification.")
|
||||||
|
return
|
||||||
|
|
||||||
|
required_headers = {
|
||||||
|
"PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"),
|
||||||
|
"PAYPAL-TRANSMISSION-TIME": headers.get("PAYPAL-TRANSMISSION-TIME"),
|
||||||
|
"PAYPAL-TRANSMISSION-SIG": headers.get("PAYPAL-TRANSMISSION-SIG"),
|
||||||
|
"PAYPAL-CERT-URL": headers.get("PAYPAL-CERT-URL"),
|
||||||
|
"PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"),
|
||||||
|
}
|
||||||
|
if not all(required_headers.values()):
|
||||||
|
logger.warning("Missing PayPal webhook headers; skipping verification.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client:
|
||||||
|
token_resp = await client.post(
|
||||||
|
"/v1/oauth2/token",
|
||||||
|
data={"grant_type": "client_credentials"},
|
||||||
|
auth=(
|
||||||
|
settings.paypal_client_id or "",
|
||||||
|
settings.paypal_client_secret or "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
token_resp.raise_for_status()
|
||||||
|
access_token = token_resp.json().get("access_token")
|
||||||
|
if not access_token:
|
||||||
|
raise ValueError("PayPal token missing in verification flow.")
|
||||||
|
|
||||||
|
verify_resp = await client.post(
|
||||||
|
"/v1/notifications/verify-webhook-signature",
|
||||||
|
json={
|
||||||
|
"auth_algo": required_headers["PAYPAL-AUTH-ALGO"],
|
||||||
|
"cert_url": required_headers["PAYPAL-CERT-URL"],
|
||||||
|
"transmission_id": required_headers["PAYPAL-TRANSMISSION-ID"],
|
||||||
|
"transmission_sig": required_headers["PAYPAL-TRANSMISSION-SIG"],
|
||||||
|
"transmission_time": required_headers["PAYPAL-TRANSMISSION-TIME"],
|
||||||
|
"webhook_id": webhook_id,
|
||||||
|
"webhook_event": json.loads(payload.decode()),
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
|
)
|
||||||
|
verify_resp.raise_for_status()
|
||||||
|
verification_status = verify_resp.json().get("verification_status")
|
||||||
|
if verification_status != "SUCCESS":
|
||||||
|
raise ValueError("PayPal webhook verification failed.")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
raise ValueError("PayPal webhook cannot be verified.") from exc
|
||||||
|
|
||||||
|
|
||||||
async def test_connection(provider: str) -> SimpleStatus:
|
async def test_connection(provider: str) -> SimpleStatus:
|
||||||
"""
|
"""
|
||||||
Test the connection to Stripe by checking if the API key is valid.
|
Test the connection to Stripe by checking if the API key is valid.
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ from ..models import (
|
|||||||
PaymentState,
|
PaymentState,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from .notifications import send_payment_notification
|
from .notifications import send_payment_notification_in_background
|
||||||
|
|
||||||
payment_lock = asyncio.Lock()
|
payment_lock = asyncio.Lock()
|
||||||
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
||||||
@@ -67,12 +67,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 +97,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 +201,22 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
|||||||
# do not save memo if description_hash or unhashed_description is set
|
# do not save memo if description_hash or unhashed_description is set
|
||||||
memo = ""
|
memo = ""
|
||||||
|
|
||||||
payment = await create_invoice(
|
async with db.connect() as conn:
|
||||||
wallet_id=wallet_id,
|
payment = await create_invoice(
|
||||||
amount=data.amount,
|
wallet_id=wallet_id,
|
||||||
memo=memo,
|
amount=data.amount,
|
||||||
currency=data.unit,
|
memo=memo,
|
||||||
description_hash=description_hash,
|
currency=data.unit,
|
||||||
unhashed_description=unhashed_description,
|
description_hash=description_hash,
|
||||||
expiry=data.expiry,
|
unhashed_description=unhashed_description,
|
||||||
extra=data.extra,
|
expiry=data.expiry,
|
||||||
webhook=data.webhook,
|
extra=data.extra,
|
||||||
internal=data.internal,
|
webhook=data.webhook,
|
||||||
payment_hash=data.payment_hash,
|
internal=data.internal,
|
||||||
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 +361,15 @@ async def update_pending_payments(wallet_id: str):
|
|||||||
await update_pending_payment(payment)
|
await update_pending_payment(payment)
|
||||||
|
|
||||||
|
|
||||||
async def update_pending_payment(payment: Payment) -> Payment:
|
async def update_pending_payment(
|
||||||
|
payment: Payment, conn: Connection | None = None
|
||||||
|
) -> Payment:
|
||||||
status = await payment.check_status()
|
status = await payment.check_status()
|
||||||
if status.failed:
|
if status.failed:
|
||||||
payment.status = PaymentState.FAILED
|
payment.status = PaymentState.FAILED
|
||||||
await update_payment(payment)
|
await update_payment(payment, conn=conn)
|
||||||
elif status.success:
|
elif status.success:
|
||||||
payment = await update_payment_success_status(payment, status)
|
payment = await update_payment_success_status(payment, status, conn=conn)
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
|
||||||
@@ -698,6 +706,7 @@ async def _pay_internal_invoice(
|
|||||||
internal_payment = await check_internal(
|
internal_payment = await check_internal(
|
||||||
create_payment_model.payment_hash, conn=conn
|
create_payment_model.payment_hash, conn=conn
|
||||||
)
|
)
|
||||||
|
|
||||||
if not internal_payment:
|
if not internal_payment:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -706,6 +715,7 @@ async def _pay_internal_invoice(
|
|||||||
internal_invoice = await get_standalone_payment(
|
internal_invoice = await get_standalone_payment(
|
||||||
internal_payment.checking_id, incoming=True, conn=conn
|
internal_payment.checking_id, incoming=True, conn=conn
|
||||||
)
|
)
|
||||||
|
|
||||||
if not internal_invoice:
|
if not internal_invoice:
|
||||||
raise PaymentError("Internal payment not found.", status="failed")
|
raise PaymentError("Internal payment not found.", status="failed")
|
||||||
|
|
||||||
@@ -727,6 +737,7 @@ async def _pay_internal_invoice(
|
|||||||
|
|
||||||
internal_id = f"internal_{create_payment_model.payment_hash}"
|
internal_id = f"internal_{create_payment_model.payment_hash}"
|
||||||
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
||||||
|
|
||||||
payment = await create_payment(
|
payment = await create_payment(
|
||||||
checking_id=internal_id,
|
checking_id=internal_id,
|
||||||
data=create_payment_model,
|
data=create_payment_model,
|
||||||
@@ -741,7 +752,7 @@ async def _pay_internal_invoice(
|
|||||||
await update_payment(internal_payment, conn=conn)
|
await update_payment(internal_payment, conn=conn)
|
||||||
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
||||||
|
|
||||||
await send_payment_notification(wallet, payment)
|
send_payment_notification_in_background(wallet, payment)
|
||||||
|
|
||||||
# notify receiver asynchronously
|
# notify receiver asynchronously
|
||||||
from lnbits.tasks import internal_invoice_queue
|
from lnbits.tasks import internal_invoice_queue
|
||||||
@@ -814,7 +825,7 @@ 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)
|
send_payment_notification_in_background(wallet, payment)
|
||||||
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
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.extensions import UserExtension
|
from lnbits.core.models.extensions import UserExtension
|
||||||
|
from lnbits.db import Connection
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
SuperSettings,
|
SuperSettings,
|
||||||
@@ -48,37 +50,43 @@ async def create_user_account_no_ckeck(
|
|||||||
account: Account | None = None,
|
account: Account | None = None,
|
||||||
wallet_name: str | None = None,
|
wallet_name: str | None = None,
|
||||||
default_exts: list[str] | None = None,
|
default_exts: list[str] | None = None,
|
||||||
|
conn: Connection | None = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
|
if account:
|
||||||
|
account.validate_fields()
|
||||||
|
if account.username and await get_account_by_username(
|
||||||
|
account.username, conn=conn
|
||||||
|
):
|
||||||
|
raise ValueError("Username already exists.")
|
||||||
|
|
||||||
if account:
|
if account.email and await get_account_by_email(account.email, conn=conn):
|
||||||
account.validate_fields()
|
raise ValueError("Email already exists.")
|
||||||
if account.username and await get_account_by_username(account.username):
|
|
||||||
raise ValueError("Username already exists.")
|
|
||||||
|
|
||||||
if account.email and await get_account_by_email(account.email):
|
if account.pubkey and await get_account_by_pubkey(
|
||||||
raise ValueError("Email already exists.")
|
account.pubkey, conn=conn
|
||||||
|
):
|
||||||
|
raise ValueError("Pubkey already exists.")
|
||||||
|
|
||||||
if account.pubkey and await get_account_by_pubkey(account.pubkey):
|
if not account.id:
|
||||||
raise ValueError("Pubkey already exists.")
|
account.id = uuid4().hex
|
||||||
|
|
||||||
if not account.id:
|
account = await create_account(account, conn=conn)
|
||||||
account.id = uuid4().hex
|
await create_wallet(
|
||||||
|
user_id=account.id,
|
||||||
|
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
|
||||||
account = await create_account(account)
|
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
|
||||||
await create_wallet(
|
for ext_id in user_extensions:
|
||||||
user_id=account.id,
|
try:
|
||||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
|
||||||
)
|
await create_user_extension(user_ext, conn=conn)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error enabeling default extension {ext_id}: {e}")
|
||||||
|
|
||||||
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
|
user = await get_user_from_account(account, conn=conn)
|
||||||
for ext_id in user_extensions:
|
|
||||||
try:
|
|
||||||
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
|
|
||||||
await create_user_extension(user_ext)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error enabeling default extension {ext_id}: {e}")
|
|
||||||
|
|
||||||
user = await get_user_from_account(account)
|
|
||||||
if not user:
|
if not user:
|
||||||
raise ValueError("Cannot find user for account.")
|
raise ValueError("Cannot find user for account.")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,9 +48,9 @@ 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
|
||||||
@@ -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.")
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ from lnbits.core.models.users import (
|
|||||||
)
|
)
|
||||||
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 update_user_account
|
||||||
from lnbits.decorators import access_token_payload, check_user_exists
|
from lnbits.decorators import (
|
||||||
|
access_token_payload,
|
||||||
|
check_account_exists,
|
||||||
|
check_user_exists,
|
||||||
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
decrypt_internal_message,
|
decrypt_internal_message,
|
||||||
@@ -119,11 +123,11 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
|||||||
@auth_router.get("/acl")
|
@auth_router.get("/acl")
|
||||||
async def api_get_user_acls(
|
async def api_get_user_acls(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: User = Depends(check_user_exists),
|
account: Account = Depends(check_account_exists),
|
||||||
) -> UserAcls:
|
) -> UserAcls:
|
||||||
api_routes = get_api_routes(request.app.router.routes)
|
api_routes = get_api_routes(request.app.router.routes)
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
acls = await get_user_access_control_lists(account.id)
|
||||||
|
|
||||||
# Add missing/new endpoints to the ACLs
|
# Add missing/new endpoints to the ACLs
|
||||||
for acl in acls.access_control_list:
|
for acl in acls.access_control_list:
|
||||||
@@ -136,7 +140,7 @@ async def api_get_user_acls(
|
|||||||
acl.endpoints.append(EndpointAccess(path=path, name=name))
|
acl.endpoints.append(EndpointAccess(path=path, name=name))
|
||||||
acl.endpoints.sort(key=lambda e: e.name.lower())
|
acl.endpoints.sort(key=lambda e: e.name.lower())
|
||||||
|
|
||||||
return UserAcls(id=user.id, access_control_list=acls.access_control_list)
|
return UserAcls(id=account.id, access_control_list=acls.access_control_list)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/acl")
|
@auth_router.put("/acl")
|
||||||
@@ -144,13 +148,13 @@ async def api_get_user_acls(
|
|||||||
async def api_update_user_acl(
|
async def api_update_user_acl(
|
||||||
request: Request,
|
request: Request,
|
||||||
data: UpdateAccessControlList,
|
data: UpdateAccessControlList,
|
||||||
user: User = Depends(check_user_exists),
|
account: Account = Depends(check_account_exists),
|
||||||
) -> UserAcls:
|
) -> UserAcls:
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
user_acls = await get_user_access_control_lists(user.id)
|
user_acls = await get_user_access_control_lists(account.id)
|
||||||
acl = user_acls.get_acl_by_id(data.id)
|
acl = user_acls.get_acl_by_id(data.id)
|
||||||
if acl:
|
if acl:
|
||||||
user_acls.access_control_list.remove(acl)
|
user_acls.access_control_list.remove(acl)
|
||||||
@@ -175,33 +179,30 @@ async def api_update_user_acl(
|
|||||||
|
|
||||||
@auth_router.delete("/acl")
|
@auth_router.delete("/acl")
|
||||||
async def api_delete_user_acl(
|
async def api_delete_user_acl(
|
||||||
data: DeleteAccessControlList,
|
data: DeleteAccessControlList, account: Account = Depends(check_account_exists)
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
):
|
):
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
user_acls = await get_user_access_control_lists(user.id)
|
user_acls = await get_user_access_control_lists(account.id)
|
||||||
user_acls.delete_acl_by_id(data.id)
|
user_acls.delete_acl_by_id(data.id)
|
||||||
await update_user_access_control_list(user_acls)
|
await update_user_access_control_list(user_acls)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.post("/acl/token")
|
@auth_router.post("/acl/token")
|
||||||
async def api_create_user_api_token(
|
async def api_create_user_api_token(
|
||||||
data: ApiTokenRequest,
|
data: ApiTokenRequest, account: Account = Depends(check_account_exists)
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
) -> ApiTokenResponse:
|
) -> ApiTokenResponse:
|
||||||
if not data.expiration_time_minutes > 0:
|
if not data.expiration_time_minutes > 0:
|
||||||
raise ValueError("Expiration time must be in the future.")
|
raise ValueError("Expiration time must be in the future.")
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
if not account.username:
|
if not account.username:
|
||||||
raise ValueError("Username must be configured.")
|
raise ValueError("Username must be configured.")
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
acls = await get_user_access_control_lists(account.id)
|
||||||
acl = acls.get_acl_by_id(data.acl_id)
|
acl = acls.get_acl_by_id(data.acl_id)
|
||||||
if not acl:
|
if not acl:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
||||||
@@ -218,18 +219,16 @@ async def api_create_user_api_token(
|
|||||||
|
|
||||||
@auth_router.delete("/acl/token")
|
@auth_router.delete("/acl/token")
|
||||||
async def api_delete_user_api_token(
|
async def api_delete_user_api_token(
|
||||||
data: DeleteTokenRequest,
|
data: DeleteTokenRequest, account: Account = Depends(check_account_exists)
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
):
|
):
|
||||||
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
if not account.username:
|
if not account.username:
|
||||||
raise ValueError("Username must be configured.")
|
raise ValueError("Username must be configured.")
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
acls = await get_user_access_control_lists(account.id)
|
||||||
acl = acls.get_acl_by_id(data.acl_id)
|
acl = acls.get_acl_by_id(data.acl_id)
|
||||||
if not acl:
|
if not acl:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
||||||
@@ -318,24 +317,20 @@ async def register(data: RegisterUser) -> JSONResponse:
|
|||||||
@auth_router.put("/pubkey")
|
@auth_router.put("/pubkey")
|
||||||
async def update_pubkey(
|
async def update_pubkey(
|
||||||
data: UpdateUserPubkey,
|
data: UpdateUserPubkey,
|
||||||
user: User = Depends(check_user_exists),
|
account: Account = Depends(check_account_exists),
|
||||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
if data.user_id != user.id:
|
if data.user_id != account.id:
|
||||||
raise ValueError("Invalid user ID.")
|
raise ValueError("Invalid user ID.")
|
||||||
|
|
||||||
_validate_auth_timeout(payload.auth_time)
|
_validate_auth_timeout(payload.auth_time)
|
||||||
if (
|
if (
|
||||||
data.pubkey
|
data.pubkey
|
||||||
and data.pubkey != user.pubkey
|
and data.pubkey != account.pubkey
|
||||||
and await get_account_by_pubkey(data.pubkey)
|
and await get_account_by_pubkey(data.pubkey)
|
||||||
):
|
):
|
||||||
raise ValueError("Public key already in use.")
|
raise ValueError("Public key already in use.")
|
||||||
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account:
|
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Account not found.")
|
|
||||||
|
|
||||||
account.pubkey = normalize_public_key(data.pubkey)
|
account.pubkey = normalize_public_key(data.pubkey)
|
||||||
await update_account(account)
|
await update_account(account)
|
||||||
return await get_user_from_account(account)
|
return await get_user_from_account(account)
|
||||||
@@ -344,23 +339,19 @@ async def update_pubkey(
|
|||||||
@auth_router.put("/password")
|
@auth_router.put("/password")
|
||||||
async def update_password(
|
async def update_password(
|
||||||
data: UpdateUserPassword,
|
data: UpdateUserPassword,
|
||||||
user: User = Depends(check_user_exists),
|
account: Account = Depends(check_account_exists),
|
||||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
_validate_auth_timeout(payload.auth_time)
|
_validate_auth_timeout(payload.auth_time)
|
||||||
if data.user_id != user.id:
|
if data.user_id != account.id:
|
||||||
raise ValueError("Invalid user ID.")
|
raise ValueError("Invalid user ID.")
|
||||||
if (
|
if (
|
||||||
data.username
|
data.username
|
||||||
and user.username != data.username
|
and account.username != data.username
|
||||||
and await get_account_by_username(data.username)
|
and await get_account_by_username(data.username)
|
||||||
):
|
):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||||
|
|
||||||
account = await get_account(user.id)
|
|
||||||
if not account:
|
|
||||||
raise ValueError("Account not found.")
|
|
||||||
|
|
||||||
# old accounts do not have a password
|
# old accounts do not have a password
|
||||||
if account.password_hash:
|
if account.password_hash:
|
||||||
if not data.password_old:
|
if not data.password_old:
|
||||||
@@ -419,15 +410,11 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
|||||||
|
|
||||||
@auth_router.put("/update")
|
@auth_router.put("/update")
|
||||||
async def update(
|
async def update(
|
||||||
data: UpdateUser, user: User = Depends(check_user_exists)
|
data: UpdateUser, account: Account = Depends(check_account_exists)
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
if data.user_id != user.id:
|
if data.user_id != account.id:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from lnbits.core.models.misc import SimpleStatus
|
|||||||
from lnbits.core.models.payments import CreateInvoice
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
from lnbits.core.services.fiat_providers import (
|
from lnbits.core.services.fiat_providers import (
|
||||||
check_stripe_signature,
|
check_stripe_signature,
|
||||||
|
verify_paypal_webhook,
|
||||||
)
|
)
|
||||||
from lnbits.core.services.payments import create_fiat_invoice
|
from lnbits.core.services.payments import create_fiat_invoice
|
||||||
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
|
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
|
||||||
@@ -37,6 +38,17 @@ async def api_generic_webhook_handler(
|
|||||||
message=f"Callback received successfully from '{provider_name}'.",
|
message=f"Callback received successfully from '{provider_name}'.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if provider_name.lower() == "paypal":
|
||||||
|
payload = await request.body()
|
||||||
|
await verify_paypal_webhook(request.headers, payload)
|
||||||
|
event = await request.json()
|
||||||
|
await handle_paypal_event(event)
|
||||||
|
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True,
|
||||||
|
message=f"Callback received successfully from '{provider_name}'.",
|
||||||
|
)
|
||||||
|
|
||||||
return SimpleStatus(
|
return SimpleStatus(
|
||||||
success=False,
|
success=False,
|
||||||
message=f"Unknown fiat provider '{provider_name}'.",
|
message=f"Unknown fiat provider '{provider_name}'.",
|
||||||
@@ -48,6 +60,8 @@ async def handle_stripe_event(event: dict):
|
|||||||
event_type = event.get("type")
|
event_type = event.get("type")
|
||||||
if event_type == "checkout.session.completed":
|
if event_type == "checkout.session.completed":
|
||||||
await _handle_stripe_checkout_session_completed(event)
|
await _handle_stripe_checkout_session_completed(event)
|
||||||
|
elif event_type == "payment_intent.succeeded":
|
||||||
|
await _handle_stripe_intent_session_completed(event)
|
||||||
elif event_type == "invoice.paid":
|
elif event_type == "invoice.paid":
|
||||||
await _handle_stripe_subscription_invoice_paid(event)
|
await _handle_stripe_subscription_invoice_paid(event)
|
||||||
else:
|
else:
|
||||||
@@ -56,18 +70,38 @@ async def handle_stripe_event(event: dict):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_stripe_intent_session_completed(event: dict):
|
||||||
|
event_id = event.get("id")
|
||||||
|
event_object = event.get("data", {}).get("object", {})
|
||||||
|
object_type = event_object.get("object")
|
||||||
|
payment_hash = event_object.get("metadata", {}).get("payment_hash")
|
||||||
|
logger.debug(
|
||||||
|
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
|
||||||
|
f" Payment hash: '{payment_hash}'."
|
||||||
|
)
|
||||||
|
if not payment_hash:
|
||||||
|
logger.warning("Stripe event does not contain a payment hash.")
|
||||||
|
return
|
||||||
|
|
||||||
|
payment = await get_standalone_payment(payment_hash)
|
||||||
|
if not payment:
|
||||||
|
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||||
|
return
|
||||||
|
await payment.check_fiat_status()
|
||||||
|
|
||||||
|
|
||||||
async def _handle_stripe_checkout_session_completed(event: dict):
|
async def _handle_stripe_checkout_session_completed(event: dict):
|
||||||
event_id = event.get("id")
|
event_id = event.get("id")
|
||||||
event_object = event.get("data", {}).get("object", {})
|
event_object = event.get("data", {}).get("object", {})
|
||||||
object_type = event_object.get("object")
|
object_type = event_object.get("object")
|
||||||
payment_hash = event_object.get("metadata", {}).get("payment_hash")
|
payment_hash = event_object.get("metadata", {}).get("payment_hash")
|
||||||
lnbits_action = event_object.get("metadata", {}).get("lnbits_action")
|
alan_action = event_object.get("metadata", {}).get("alan_action")
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
|
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
|
||||||
f" Payment hash: '{payment_hash}'."
|
f" Payment hash: '{payment_hash}'."
|
||||||
)
|
)
|
||||||
if lnbits_action != "invoice":
|
if alan_action != "invoice":
|
||||||
logger.warning(f"Stripe event is not an invoice: '{lnbits_action}'.")
|
logger.warning(f"Stripe event is not an invoice: '{alan_action}'.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not payment_hash:
|
if not payment_hash:
|
||||||
@@ -132,7 +166,7 @@ async def _get_stripe_subscription_payment_options(
|
|||||||
|
|
||||||
metadata = parent.get("subscription_details", {}).get("metadata", {})
|
metadata = parent.get("subscription_details", {}).get("metadata", {})
|
||||||
|
|
||||||
if metadata.get("lnbits_action") != "subscription":
|
if metadata.get("alan_action") != "subscription":
|
||||||
raise ValueError("Stripe invoice.paid metadata action is not 'subscription'.")
|
raise ValueError("Stripe invoice.paid metadata action is not 'subscription'.")
|
||||||
|
|
||||||
if "extra" in metadata:
|
if "extra" in metadata:
|
||||||
@@ -143,3 +177,84 @@ async def _get_stripe_subscription_payment_options(
|
|||||||
metadata["extra"] = {}
|
metadata["extra"] = {}
|
||||||
|
|
||||||
return FiatSubscriptionPaymentOptions(**metadata)
|
return FiatSubscriptionPaymentOptions(**metadata)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_paypal_event(event: dict):
|
||||||
|
event_type = event.get("event_type", "")
|
||||||
|
resource = event.get("resource", {})
|
||||||
|
|
||||||
|
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
|
||||||
|
payment_hash = _paypal_extract_payment_hash(resource)
|
||||||
|
if not payment_hash:
|
||||||
|
logger.warning("PayPal event missing payment hash.")
|
||||||
|
return
|
||||||
|
payment = await get_standalone_payment(payment_hash)
|
||||||
|
if not payment:
|
||||||
|
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||||
|
return
|
||||||
|
await payment.check_fiat_status()
|
||||||
|
return
|
||||||
|
|
||||||
|
if event_type in (
|
||||||
|
"PAYMENT.SALE.COMPLETED",
|
||||||
|
"BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED",
|
||||||
|
):
|
||||||
|
await _handle_paypal_subscription_payment(resource)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Unhandled PayPal event type: '{event_type}'.")
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_paypal_subscription_payment(resource: dict):
|
||||||
|
amount_info = resource.get("amount") or {}
|
||||||
|
currency = (amount_info.get("currency_code") or "").upper()
|
||||||
|
total = amount_info.get("value")
|
||||||
|
if not currency or total is None:
|
||||||
|
raise ValueError("PayPal subscription event missing amount.")
|
||||||
|
|
||||||
|
custom_id = resource.get("custom_id") or resource.get("custom")
|
||||||
|
if not custom_id:
|
||||||
|
raise ValueError("PayPal subscription event missing custom metadata.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata = json.loads(custom_id)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
payment_options = FiatSubscriptionPaymentOptions(**metadata)
|
||||||
|
if not payment_options.wallet_id:
|
||||||
|
raise ValueError("PayPal subscription event missing wallet_id.")
|
||||||
|
|
||||||
|
memo = payment_options.memo or ""
|
||||||
|
extra = {
|
||||||
|
**(payment_options.extra or {}),
|
||||||
|
"fiat_method": "subscription",
|
||||||
|
"tag": payment_options.tag,
|
||||||
|
"subscription": {
|
||||||
|
"checking_id": resource.get("id") or resource.get("billing_agreement_id"),
|
||||||
|
"payment_request": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
payment = await create_fiat_invoice(
|
||||||
|
wallet_id=payment_options.wallet_id,
|
||||||
|
invoice_data=CreateInvoice(
|
||||||
|
unit=currency,
|
||||||
|
amount=float(total),
|
||||||
|
memo=memo,
|
||||||
|
extra=extra,
|
||||||
|
fiat_provider="paypal",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await payment.check_fiat_status()
|
||||||
|
|
||||||
|
|
||||||
|
def _paypal_extract_payment_hash(resource: dict) -> str | None:
|
||||||
|
purchase_units = resource.get("purchase_units") or []
|
||||||
|
for pu in purchase_units:
|
||||||
|
if pu.get("invoice_id"):
|
||||||
|
return pu.get("invoice_id")
|
||||||
|
if pu.get("custom_id"):
|
||||||
|
return pu.get("custom_id")
|
||||||
|
return None
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
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,
|
||||||
@@ -23,6 +24,7 @@ from lnbits.core.models.extensions import (
|
|||||||
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,
|
||||||
@@ -33,8 +35,9 @@ from lnbits.core.services.extensions import (
|
|||||||
uninstall_extension,
|
uninstall_extension,
|
||||||
)
|
)
|
||||||
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 +145,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 +167,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 +181,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 +223,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 +380,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 +428,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 +441,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 +462,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 +507,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 +530,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 +548,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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ async def extensions_builder_preview(
|
|||||||
request,
|
request,
|
||||||
"error.html",
|
"error.html",
|
||||||
{
|
{
|
||||||
"err": f"Extension {ext_id} not found",
|
"status_code": 404,
|
||||||
"message": "Please 'Refresh Preview' first.",
|
"message": f"Extension {ext_id} not found, refresh Preview.",
|
||||||
},
|
},
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
)
|
)
|
||||||
@@ -87,7 +87,6 @@ async def extensions_builder_preview(
|
|||||||
html_file_path.as_posix(),
|
html_file_path.as_posix(),
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"ajax": _is_ajax_request(request),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,6 +95,7 @@ async def extensions_builder_preview(
|
|||||||
"style-src 'self' 'unsafe-inline'; "
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -197,6 +197,7 @@ 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")
|
||||||
|
@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")
|
||||||
@@ -223,7 +224,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}")
|
||||||
@@ -284,7 +285,3 @@ async def lnurlwallet(request: Request, lightning: str = ""):
|
|||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/wallet?usr={account.id}&wal={wallet.id}",
|
f"/wallet?usr={account.id}&wal={wallet.id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _is_ajax_request(request: Request):
|
|
||||||
return request.headers.get("X-Requested-With", None) == "XMLHttpRequest"
|
|
||||||
|
|||||||
@@ -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,24 +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(
|
recheck_pending: bool = Query(
|
||||||
False, description="Force check and update of pending payments."
|
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,
|
||||||
if not recheck_pending:
|
conn=conn,
|
||||||
return page
|
)
|
||||||
|
if not recheck_pending:
|
||||||
|
return page
|
||||||
|
|
||||||
for payment in page.data:
|
payments = []
|
||||||
if payment.pending:
|
for payment in page.data:
|
||||||
await update_pending_payment(payment)
|
if payment.pending:
|
||||||
|
refreshed_payment = await update_pending_payment(payment, conn=conn)
|
||||||
|
payments.append(refreshed_payment)
|
||||||
|
else:
|
||||||
|
payments.append(payment)
|
||||||
|
|
||||||
return page
|
return Page(data=payments, total=page.total)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
@@ -208,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(
|
||||||
@@ -243,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,
|
||||||
@@ -274,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.")
|
||||||
@@ -332,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}
|
||||||
@@ -341,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."
|
||||||
|
|||||||
@@ -115,12 +115,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,7 +154,7 @@ 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:
|
if len(wallets) > 0:
|
||||||
@@ -169,7 +169,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.",
|
||||||
@@ -295,7 +295,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 +304,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.",
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ from lnbits.core.crud.wallets import (
|
|||||||
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,12 +30,14 @@ 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,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import generate_filter_params_openapi
|
from lnbits.helpers import generate_filter_params_openapi
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
delete_wallet,
|
delete_wallet,
|
||||||
@@ -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,17 @@ async def api_update_wallet_name(
|
|||||||
|
|
||||||
@wallet_router.put("/reset/{wallet_id}")
|
@wallet_router.put("/reset/{wallet_id}")
|
||||||
async def api_reset_wallet_keys(
|
async def api_reset_wallet_keys(
|
||||||
wallet_id: str, user: User = Depends(check_user_exists)
|
wallet_id: str,
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
wallet = await get_wallet(wallet_id)
|
wallet = await get_wallet(wallet_id)
|
||||||
if not wallet or wallet.user != user.id:
|
if not wallet or wallet.user != account_id.id:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||||
|
|
||||||
|
cache.pop(f"auth:wallet:{wallet.id}")
|
||||||
|
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
||||||
|
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
||||||
|
|
||||||
wallet.adminkey = uuid4().hex
|
wallet.adminkey = uuid4().hex
|
||||||
wallet.inkey = uuid4().hex
|
wallet.inkey = uuid4().hex
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
@@ -175,10 +183,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 +197,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 +213,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)
|
||||||
|
|||||||
+221
-53
@@ -19,6 +19,8 @@ from lnbits.core.crud import (
|
|||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import get_user_access_control_lists
|
from lnbits.core.crud.users import get_user_access_control_lists
|
||||||
|
from lnbits.core.crud.wallets import get_base_wallet_for_key
|
||||||
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccessTokenPayload,
|
AccessTokenPayload,
|
||||||
Account,
|
Account,
|
||||||
@@ -27,9 +29,12 @@ from lnbits.core.models import (
|
|||||||
User,
|
User,
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.models.users import AccountId
|
||||||
|
from lnbits.core.models.wallets import BaseWallet, BaseWalletTypeInfo
|
||||||
from lnbits.db import Connection, Filter, Filters, TFilterModel
|
from lnbits.db import Connection, Filter, Filters, TFilterModel
|
||||||
from lnbits.helpers import normalize_path, path_segments
|
from lnbits.helpers import normalize_path, path_segments, sha256s
|
||||||
from lnbits.settings import AuthMethods, settings
|
from lnbits.settings import AuthMethods, settings
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(
|
oauth2_scheme = OAuth2PasswordBearer(
|
||||||
tokenUrl="api/v1/auth",
|
tokenUrl="api/v1/auth",
|
||||||
@@ -52,7 +57,7 @@ api_key_query = APIKeyQuery(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class KeyChecker(SecurityBase):
|
class BaseKeyChecker(SecurityBase):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
@@ -77,8 +82,7 @@ class KeyChecker(SecurityBase):
|
|||||||
)
|
)
|
||||||
self.model: APIKey = openapi_model # type: ignore
|
self.model: APIKey = openapi_model # type: ignore
|
||||||
|
|
||||||
async def __call__(self, request: Request) -> WalletTypeInfo:
|
def _extract_key_value(self, request):
|
||||||
|
|
||||||
key_value = (
|
key_value = (
|
||||||
self._api_key
|
self._api_key
|
||||||
if self._api_key
|
if self._api_key
|
||||||
@@ -91,27 +95,88 @@ class KeyChecker(SecurityBase):
|
|||||||
detail="No Api Key provided.",
|
detail="No Api Key provided.",
|
||||||
)
|
)
|
||||||
|
|
||||||
wallet = await get_wallet_for_key(key_value)
|
return key_value
|
||||||
|
|
||||||
if not wallet:
|
async def _extract_key_type(self, key_value: str, wallet: BaseWallet) -> KeyType:
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
detail="Wallet not found.",
|
|
||||||
)
|
|
||||||
|
|
||||||
request.scope["user_id"] = wallet.user
|
|
||||||
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
detail="Invalid adminkey.",
|
detail="Invalid adminkey.",
|
||||||
)
|
)
|
||||||
|
|
||||||
await _check_user_extension_access(wallet.user, request["path"])
|
|
||||||
|
|
||||||
key_type = KeyType.admin if wallet.adminkey == key_value else KeyType.invoice
|
key_type = KeyType.admin if wallet.adminkey == key_value else KeyType.invoice
|
||||||
|
return key_type
|
||||||
|
|
||||||
|
|
||||||
|
class KeyChecker(BaseKeyChecker):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str | None = None,
|
||||||
|
expected_key_type: KeyType | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(api_key, expected_key_type)
|
||||||
|
|
||||||
|
async def __call__(self, request: Request) -> WalletTypeInfo:
|
||||||
|
key_value = self._extract_key_value(request)
|
||||||
|
|
||||||
|
async with db.connect() as conn:
|
||||||
|
wallet = await get_wallet_for_key(key_value, conn=conn)
|
||||||
|
|
||||||
|
if not wallet:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="Wallet not found.",
|
||||||
|
)
|
||||||
|
|
||||||
|
request.scope["user_id"] = wallet.user
|
||||||
|
await _check_user_access(request, wallet.user, conn=conn)
|
||||||
|
|
||||||
|
key_type = await self._extract_key_type(key_value, wallet)
|
||||||
return WalletTypeInfo(key_type, wallet)
|
return WalletTypeInfo(key_type, wallet)
|
||||||
|
|
||||||
|
|
||||||
|
class LightKeyChecker(BaseKeyChecker):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str | None = None,
|
||||||
|
expected_key_type: KeyType | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(api_key, expected_key_type)
|
||||||
|
|
||||||
|
async def __call__(self, request: Request) -> BaseWalletTypeInfo:
|
||||||
|
key_value = self._extract_key_value(request)
|
||||||
|
cache_key = f"auth:x-api-key:{key_value}"
|
||||||
|
cache_time = settings.auth_authentication_cache_minutes * 60
|
||||||
|
|
||||||
|
async with db.connect() as conn:
|
||||||
|
if cache_time > 0:
|
||||||
|
key_info: BaseWalletTypeInfo | None = cache.get(cache_key)
|
||||||
|
if key_info:
|
||||||
|
request.scope["user_id"] = key_info.wallet.user
|
||||||
|
await _check_user_access(request, key_info.wallet.user, conn=conn)
|
||||||
|
return key_info
|
||||||
|
|
||||||
|
wallet = await get_base_wallet_for_key(key_value, conn=conn)
|
||||||
|
|
||||||
|
if not wallet:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="Wallet not found.",
|
||||||
|
)
|
||||||
|
request.scope["user_id"] = wallet.user
|
||||||
|
await _check_user_access(request, wallet.user, conn=conn)
|
||||||
|
|
||||||
|
key_type = await self._extract_key_type(key_value, wallet)
|
||||||
|
key_info = BaseWalletTypeInfo(key_type, wallet)
|
||||||
|
|
||||||
|
if cache_time > 0:
|
||||||
|
cache.set(cache_key, key_info, expiry=cache_time)
|
||||||
|
cache.set(f"auth:wallet:{wallet.id}", wallet, expiry=cache_time)
|
||||||
|
return key_info
|
||||||
|
|
||||||
|
|
||||||
async def require_admin_key(
|
async def require_admin_key(
|
||||||
request: Request,
|
request: Request,
|
||||||
api_key_header: str = Security(api_key_header),
|
api_key_header: str = Security(api_key_header),
|
||||||
@@ -124,6 +189,18 @@ async def require_admin_key(
|
|||||||
return await check(request)
|
return await check(request)
|
||||||
|
|
||||||
|
|
||||||
|
async def require_base_admin_key(
|
||||||
|
request: Request,
|
||||||
|
api_key_header: str = Security(api_key_header),
|
||||||
|
api_key_query: str = Security(api_key_query),
|
||||||
|
) -> BaseWalletTypeInfo:
|
||||||
|
check: LightKeyChecker = LightKeyChecker(
|
||||||
|
api_key=api_key_header or api_key_query,
|
||||||
|
expected_key_type=KeyType.admin,
|
||||||
|
)
|
||||||
|
return await check(request)
|
||||||
|
|
||||||
|
|
||||||
async def require_invoice_key(
|
async def require_invoice_key(
|
||||||
request: Request,
|
request: Request,
|
||||||
api_key_header: str = Security(api_key_header),
|
api_key_header: str = Security(api_key_header),
|
||||||
@@ -136,6 +213,18 @@ async def require_invoice_key(
|
|||||||
return await check(request)
|
return await check(request)
|
||||||
|
|
||||||
|
|
||||||
|
async def require_base_invoice_key(
|
||||||
|
request: Request,
|
||||||
|
api_key_header: str = Security(api_key_header),
|
||||||
|
api_key_query: str = Security(api_key_query),
|
||||||
|
) -> BaseWalletTypeInfo:
|
||||||
|
check: LightKeyChecker = LightKeyChecker(
|
||||||
|
api_key=api_key_header or api_key_query,
|
||||||
|
expected_key_type=KeyType.invoice,
|
||||||
|
)
|
||||||
|
return await check(request)
|
||||||
|
|
||||||
|
|
||||||
async def check_access_token(
|
async def check_access_token(
|
||||||
header_access_token: Annotated[str | None, Depends(oauth2_scheme)],
|
header_access_token: Annotated[str | None, Depends(oauth2_scheme)],
|
||||||
cookie_access_token: Annotated[str | None, Cookie()] = None,
|
cookie_access_token: Annotated[str | None, Cookie()] = None,
|
||||||
@@ -144,33 +233,95 @@ async def check_access_token(
|
|||||||
return header_access_token or cookie_access_token or bearer_access_token
|
return header_access_token or cookie_access_token or bearer_access_token
|
||||||
|
|
||||||
|
|
||||||
|
async def check_account_id_exists(
|
||||||
|
r: Request,
|
||||||
|
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||||
|
usr: UUID4 | None = None,
|
||||||
|
) -> AccountId:
|
||||||
|
cache_key: str | None = None
|
||||||
|
if access_token:
|
||||||
|
cache_key = f"auth:access_token:{sha256s(access_token)}"
|
||||||
|
elif usr:
|
||||||
|
cache_key = f"auth:user_id:{sha256s(usr.hex)}"
|
||||||
|
|
||||||
|
async with db.connect() as conn:
|
||||||
|
if cache_key and settings.auth_authentication_cache_minutes > 0:
|
||||||
|
account_id = cache.get(cache_key)
|
||||||
|
if account_id:
|
||||||
|
r.scope["user_id"] = account_id.id
|
||||||
|
await _check_user_access(r, account_id.id, conn=conn)
|
||||||
|
return account_id
|
||||||
|
|
||||||
|
account = await _check_account_exists(r, access_token, usr, conn=conn)
|
||||||
|
account_id = AccountId(id=account.id)
|
||||||
|
|
||||||
|
if cache_key and settings.auth_authentication_cache_minutes > 0:
|
||||||
|
cache.set(
|
||||||
|
cache_key,
|
||||||
|
account_id,
|
||||||
|
expiry=settings.auth_authentication_cache_minutes * 60,
|
||||||
|
)
|
||||||
|
|
||||||
|
return account_id
|
||||||
|
|
||||||
|
|
||||||
|
async def check_account_exists(
|
||||||
|
r: Request,
|
||||||
|
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||||
|
usr: UUID4 | None = None,
|
||||||
|
) -> Account:
|
||||||
|
return await _check_account_exists(r, access_token, usr)
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_account_exists(
|
||||||
|
r: Request,
|
||||||
|
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||||
|
usr: UUID4 | None = None,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Account:
|
||||||
|
"""
|
||||||
|
Check that the account exists based on access token or user id.
|
||||||
|
More performant version of `check_user_exists`.
|
||||||
|
Unlike `check_user_exists`, this function:
|
||||||
|
- does not fetch the user wallets
|
||||||
|
- caches the account info based on settings cache time
|
||||||
|
"""
|
||||||
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
|
if access_token:
|
||||||
|
account = await _get_account_from_token(
|
||||||
|
access_token, r["path"], r["method"], conn=new_conn
|
||||||
|
)
|
||||||
|
elif usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||||
|
account = await get_account(usr.hex, conn=new_conn)
|
||||||
|
if account and account.is_admin:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.FORBIDDEN, "User id only access for admins is forbidden."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.UNAUTHORIZED, "Missing user ID or access token."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not found.")
|
||||||
|
|
||||||
|
r.scope["user_id"] = account.id
|
||||||
|
await _check_user_access(r, account.id, conn=new_conn)
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def check_user_exists(
|
async def check_user_exists(
|
||||||
r: Request,
|
r: Request,
|
||||||
access_token: Annotated[str | None, Depends(check_access_token)],
|
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||||
usr: UUID4 | None = None,
|
usr: UUID4 | None = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
if access_token:
|
async with db.connect() as conn:
|
||||||
account = await _get_account_from_token(access_token, r["path"], r["method"])
|
account = await _check_account_exists(r, access_token, usr, conn=conn)
|
||||||
elif usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
user = await get_user_from_account(account, conn=conn)
|
||||||
account = await get_account(usr.hex)
|
|
||||||
if account and account.is_admin:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.FORBIDDEN, "User id only access for admins is forbidden."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing user ID or access token.")
|
|
||||||
|
|
||||||
if not account:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not found.")
|
|
||||||
|
|
||||||
r.scope["user_id"] = account.id
|
|
||||||
if not settings.is_user_allowed(account.id):
|
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "User not allowed.")
|
|
||||||
|
|
||||||
user = await get_user_from_account(account)
|
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not found.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not found.")
|
||||||
await _check_user_extension_access(user.id, r["path"])
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -198,29 +349,36 @@ async def access_token_payload(
|
|||||||
return AccessTokenPayload(**payload)
|
return AccessTokenPayload(**payload)
|
||||||
|
|
||||||
|
|
||||||
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
async def check_admin(
|
||||||
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
|
account: Annotated[Account, Depends(check_account_exists)],
|
||||||
|
) -> Account:
|
||||||
|
if (
|
||||||
|
account.id != settings.super_user
|
||||||
|
and account.id not in settings.lnbits_admin_users
|
||||||
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN, "User not authorized. No admin privileges."
|
HTTPStatus.FORBIDDEN, "User not authorized. No admin privileges."
|
||||||
)
|
)
|
||||||
if not user.has_password:
|
if not account.has_password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN, "Admin users must have credentials configured."
|
HTTPStatus.FORBIDDEN, "Admin users must have credentials configured."
|
||||||
)
|
)
|
||||||
|
|
||||||
return user
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
async def check_super_user(
|
||||||
if user.id != settings.super_user:
|
account: Annotated[Account, Depends(check_admin)],
|
||||||
|
) -> Account:
|
||||||
|
if account.id != settings.super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN, "User not authorized. No super user privileges."
|
HTTPStatus.FORBIDDEN, "User not authorized. No super user privileges."
|
||||||
)
|
)
|
||||||
if not user.has_password:
|
if not account.has_password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN, "Super user must have credentials configured."
|
HTTPStatus.FORBIDDEN, "Super user must have credentials configured."
|
||||||
)
|
)
|
||||||
return user
|
return account
|
||||||
|
|
||||||
|
|
||||||
def parse_filters(model: type[TFilterModel]):
|
def parse_filters(model: type[TFilterModel]):
|
||||||
@@ -280,9 +438,17 @@ async def check_user_extension_access(
|
|||||||
return SimpleStatus(success=True, message="OK")
|
return SimpleStatus(success=True, message="OK")
|
||||||
|
|
||||||
|
|
||||||
async def _check_user_extension_access(user_id: str, path: str):
|
async def _check_user_access(r: Request, user_id: str, conn: Connection | None = None):
|
||||||
|
if not settings.is_user_allowed(user_id):
|
||||||
|
raise HTTPException(HTTPStatus.FORBIDDEN, "User not allowed.")
|
||||||
|
await _check_user_extension_access(user_id, r["path"], conn=conn)
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_user_extension_access(
|
||||||
|
user_id: str, path: str, conn: Connection | None = None
|
||||||
|
):
|
||||||
ext_id = path_segments(path)[0]
|
ext_id = path_segments(path)[0]
|
||||||
status = await check_user_extension_access(user_id, ext_id)
|
status = await check_user_extension_access(user_id, ext_id, conn=conn)
|
||||||
if not status.success:
|
if not status.success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
@@ -291,12 +457,12 @@ async def _check_user_extension_access(user_id: str, path: str):
|
|||||||
|
|
||||||
|
|
||||||
async def _get_account_from_token(
|
async def _get_account_from_token(
|
||||||
access_token: str, path: str, method: str
|
access_token: str, path: str, method: str, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
try:
|
try:
|
||||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
||||||
return await _get_account_from_jwt_payload(
|
return await _get_account_from_jwt_payload(
|
||||||
AccessTokenPayload(**payload), path, method
|
AccessTokenPayload(**payload), path, method, conn=conn
|
||||||
)
|
)
|
||||||
|
|
||||||
except jwt.ExpiredSignatureError as exc:
|
except jwt.ExpiredSignatureError as exc:
|
||||||
@@ -309,33 +475,35 @@ async def _get_account_from_token(
|
|||||||
|
|
||||||
|
|
||||||
async def _get_account_from_jwt_payload(
|
async def _get_account_from_jwt_payload(
|
||||||
payload: AccessTokenPayload, path: str, method: str
|
payload: AccessTokenPayload, path: str, method: str, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
account = None
|
account = None
|
||||||
if payload.sub:
|
if payload.sub:
|
||||||
account = await get_account_by_username(payload.sub)
|
account = await get_account_by_username(payload.sub, conn=conn)
|
||||||
elif payload.usr:
|
elif payload.usr:
|
||||||
account = await get_account(payload.usr)
|
account = await get_account(payload.usr, conn=conn)
|
||||||
elif payload.email:
|
elif payload.email:
|
||||||
account = await get_account_by_email(payload.email)
|
account = await get_account_by_email(payload.email, conn=conn)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if payload.api_token_id:
|
if payload.api_token_id:
|
||||||
await _check_account_api_access(account.id, payload.api_token_id, path, method)
|
await _check_account_api_access(
|
||||||
|
account.id, payload.api_token_id, path, method, conn=conn
|
||||||
|
)
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def _check_account_api_access(
|
async def _check_account_api_access(
|
||||||
user_id: str, token_id: str, path: str, method: str
|
user_id: str, token_id: str, path: str, method: str, conn: Connection | None = None
|
||||||
):
|
):
|
||||||
segments = path.split("/")
|
segments = path.split("/")
|
||||||
if len(segments) < 3:
|
if len(segments) < 3:
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Not an API endpoint.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Not an API endpoint.")
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user_id)
|
acls = await get_user_access_control_lists(user_id, conn=conn)
|
||||||
acl = acls.get_acl_by_token_id(token_id)
|
acl = acls.get_acl_by_token_id(token_id)
|
||||||
if not acl:
|
if not acl:
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Invalid token id.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Invalid token id.")
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
folders.extend(additional_folders)
|
folders.extend(additional_folders)
|
||||||
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
||||||
t.env.globals["static_url_for"] = static_url_for
|
t.env.globals["static_url_for"] = static_url_for
|
||||||
|
t.env.globals["normalize_path"] = normalize_path
|
||||||
|
|
||||||
window_settings = {
|
window_settings = {
|
||||||
"AD_SPACE": settings.lnbits_ad_space,
|
"AD_SPACE": settings.lnbits_ad_space,
|
||||||
@@ -91,6 +92,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
||||||
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
||||||
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
||||||
|
"LNBITS_APPLE_TOUCH_ICON": settings.lnbits_apple_touch_icon,
|
||||||
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
||||||
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
||||||
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
||||||
@@ -112,6 +114,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
|
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
|
||||||
"LNBITS_CURRENCIES": list(currencies.keys()),
|
"LNBITS_CURRENCIES": list(currencies.keys()),
|
||||||
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
|
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
|
||||||
|
"CACHE_KEY": settings.server_startup_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||||
|
|||||||
+31
-4
@@ -270,6 +270,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")
|
||||||
@@ -307,6 +308,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 +607,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 +647,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 +696,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 +705,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 +720,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 +782,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
|
||||||
|
|||||||
+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
@@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-center q-pa-md flex flex-center">
|
||||||
|
<div>
|
||||||
|
<div class="error-code" v-text="code"></div>
|
||||||
|
<div class="error-message" v-text="message"></div>
|
||||||
|
<div class="q-mx-auto q-mt-lg justify-center" style="width: max-content">
|
||||||
|
<q-btn
|
||||||
|
v-if="isExtension"
|
||||||
|
color="primary"
|
||||||
|
@click="goToExtension()"
|
||||||
|
label="Go To Extension"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-else-if="g.isUserAuthorized"
|
||||||
|
color="primary"
|
||||||
|
@click="goToWallet()"
|
||||||
|
label="Go to Wallet"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn v-else color="primary" @click="goBack()" label="Go Back"></q-btn>
|
||||||
|
<span class="q-mx-md">OR</span>
|
||||||
|
<q-btn color="secondary" @click="goHome()" label="Go Home"></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import LnbitsError from './lnbits-error.vue'
|
||||||
|
|
||||||
|
window.LnbitsError = LnbitsError
|
||||||
@@ -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',
|
||||||
@@ -237,6 +239,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.',
|
||||||
@@ -428,6 +431,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',
|
||||||
@@ -574,6 +578,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',
|
||||||
@@ -638,6 +645,8 @@ window.localisation.en = {
|
|||||||
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
||||||
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
||||||
ui_qr_code_logo_hint: 'QR code and favicon logo url',
|
ui_qr_code_logo_hint: 'QR code and favicon logo url',
|
||||||
|
ui_apple_touch_icon: 'Apple Touch Icon',
|
||||||
|
ui_apple_touch_icon_hint: 'Apple touch icon url',
|
||||||
ui_custom_image: 'Custom Image',
|
ui_custom_image: 'Custom Image',
|
||||||
ui_custom_image_label: 'URL to custom image',
|
ui_custom_image_label: 'URL to custom image',
|
||||||
ui_custom_image_hint: 'Image showed at homepage/login',
|
ui_custom_image_hint: 'Image showed at homepage/login',
|
||||||
@@ -699,10 +708,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',
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
+16
-11
@@ -89,6 +89,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',
|
||||||
@@ -123,14 +129,12 @@ window._lnbitsApi = {
|
|||||||
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 => {
|
}).catch(LNbits.utils.notifyApiError)
|
||||||
window.location = '/wallet?wal=' + res.data.id
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
updateWallet(name, wallet) {
|
updateWallet(name, wallet) {
|
||||||
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
||||||
@@ -145,11 +149,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 +171,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ window.LNbits = {
|
|||||||
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,
|
||||||
@@ -50,9 +51,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')
|
||||||
|
|||||||
@@ -58,11 +58,6 @@ window.app.component('lnbits-payment-details', {
|
|||||||
template: '#lnbits-payment-details',
|
template: '#lnbits-payment-details',
|
||||||
props: ['payment'],
|
props: ['payment'],
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
hasPreimage() {
|
hasPreimage() {
|
||||||
return (
|
return (
|
||||||
@@ -116,11 +111,11 @@ 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
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -364,11 +359,8 @@ window.app.component('lnbits-update-balance', {
|
|||||||
mixins: [window.windowMixin],
|
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() {
|
||||||
@@ -708,7 +700,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 +749,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() {
|
||||||
@@ -807,7 +781,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'
|
||||||
|
|||||||
@@ -70,6 +70,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 +129,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,
|
||||||
|
|||||||
@@ -5,10 +5,79 @@ window.app.component('lnbits-admin-fiat-providers', {
|
|||||||
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 +95,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}`)
|
||||||
|
|||||||
@@ -89,8 +89,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 +166,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ window.app.component('lnbits-header', {
|
|||||||
template: '#lnbits-header',
|
template: '#lnbits-header',
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
computed: {
|
computed: {
|
||||||
denomination() {
|
|
||||||
return this.LNBITS_DENOMINATION || 'sat'
|
|
||||||
},
|
|
||||||
hasServiceFeeMax() {
|
hasServiceFeeMax() {
|
||||||
return (
|
return (
|
||||||
this.g.user &&
|
this.g.user &&
|
||||||
@@ -41,17 +38,28 @@ window.app.component('lnbits-header', {
|
|||||||
customLogoUrl() {
|
customLogoUrl() {
|
||||||
return this.USE_CUSTOM_LOGO || null
|
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() {
|
showVoidwallet() {
|
||||||
return this.g.user && this.VOIDWALLET == true
|
return this.g.user && this.VOIDWALLET == true
|
||||||
|
},
|
||||||
|
displayName() {
|
||||||
|
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'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ window.app.component('lnbits-home-logos', {
|
|||||||
computed: {
|
computed: {
|
||||||
showLogos() {
|
showLogos() {
|
||||||
return (
|
return (
|
||||||
this.LNBITS_DENOMINATION == 'sats' &&
|
this.g.isSatsDenomination &&
|
||||||
this.SITE_TITLE == 'LNbits' &&
|
this.SITE_TITLE == 'LNbits' &&
|
||||||
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,27 +9,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 +36,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() {
|
||||||
|
|||||||
@@ -3,7 +3,90 @@ window.app.component('lnbits-manage-wallet-list', {
|
|||||||
mixins: [window.windowMixin],
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ window.app.component('lnbits-payment-list', {
|
|||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
denomination: LNBITS_DENOMINATION,
|
|
||||||
payments: [],
|
payments: [],
|
||||||
paymentsTable: {
|
paymentsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
@@ -18,7 +17,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 +29,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 +52,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 +73,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 +133,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
|
||||||
@@ -167,16 +170,14 @@ window.app.component('lnbits-payment-list', {
|
|||||||
fiat_currency: data.fiat_currency,
|
fiat_currency: data.fiat_currency,
|
||||||
labels: data.labels
|
labels: data.labels
|
||||||
}
|
}
|
||||||
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
|
obj.date = this.utils.formatDate(data.created_at)
|
||||||
obj.dateFrom = moment.utc(data.created_at).local().fromNow()
|
obj.dateFrom = this.utils.formatDateFrom(data.created_at)
|
||||||
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
|
obj.expirydate = this.utils.formatDate(data.expiry)
|
||||||
obj.expirydateFrom = moment.utc(obj.expiry).local().fromNow()
|
obj.expirydateFrom = this.utils.formatDateFrom(data.expiry)
|
||||||
obj.msat = obj.amount
|
obj.msat = obj.amount
|
||||||
obj.sat = obj.msat / 1000
|
obj.sat = obj.msat / 1000
|
||||||
obj.tag = obj.extra?.tag
|
obj.tag = obj.extra?.tag
|
||||||
obj.fsat = new Intl.NumberFormat(window.i18n.global.locale).format(
|
obj.fsat = this.utils.formatSat(obj.sat)
|
||||||
obj.sat
|
|
||||||
)
|
|
||||||
obj.isIn = obj.amount > 0
|
obj.isIn = obj.amount > 0
|
||||||
obj.isOut = obj.amount < 0
|
obj.isOut = obj.amount < 0
|
||||||
obj.isPending = obj.status === 'pending'
|
obj.isPending = obj.status === 'pending'
|
||||||
@@ -203,7 +204,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) {
|
||||||
@@ -227,29 +227,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(this.mapPayment)
|
this.payments = response.data.data.map(this.mapPayment)
|
||||||
|
this.paymentsTable.loading = false
|
||||||
this.recheckPendingPayments()
|
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
|
||||||
@@ -266,7 +276,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') {
|
||||||
@@ -294,7 +304,7 @@ window.app.component('lnbits-payment-list', {
|
|||||||
].join('&')
|
].join('&')
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.getPayments(this.currentWallet, params)
|
.getPayments(this.wallet, params)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let updatedPayments = 0
|
let updatedPayments = 0
|
||||||
response.data.data.forEach(updatedPayment => {
|
response.data.data.forEach(updatedPayment => {
|
||||||
@@ -326,7 +336,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({
|
||||||
@@ -338,7 +348,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({
|
||||||
@@ -361,7 +371,7 @@ 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(this.mapPayment)
|
let payments = response.data.data.map(this.mapPayment)
|
||||||
let columns = this.paymentsCSV.columns
|
let columns = this.paymentsCSV.columns
|
||||||
|
|
||||||
@@ -389,7 +399,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'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -458,7 +468,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -2,7 +2,7 @@ window.app.component('lnbits-qrcode', {
|
|||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
template: '#lnbits-qrcode',
|
template: '#lnbits-qrcode',
|
||||||
components: {
|
components: {
|
||||||
QrcodeVue
|
QrcodeVue: QrcodeVue.default
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
@@ -13,6 +13,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
|
||||||
@@ -41,9 +45,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
|
||||||
|
|||||||
@@ -39,6 +39,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
|
||||||
|
|||||||
@@ -102,63 +102,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: {
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ window.app.component('lnbits-wallet-extra', {
|
|||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.deleteWallet(this.g.wallet)
|
.deleteWallet(this.g.wallet)
|
||||||
.then(_ => {
|
.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({
|
Quasar.Notify.create({
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
message: `Wallet deleted!`,
|
message: `Wallet deleted!`,
|
||||||
|
|||||||
@@ -4,10 +4,27 @@ window.app.component('lnbits-wallet-new', {
|
|||||||
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 +33,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 +68,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 +77,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 +84,20 @@ 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}`)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|||||||
@@ -15,10 +15,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(
|
||||||
|
|||||||
@@ -26,7 +26,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 = {
|
||||||
|
|||||||
@@ -9,20 +9,23 @@ const localStore = (key, defaultValue) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.g = Vue.reactive({
|
window.g = Vue.reactive({
|
||||||
|
errorCode: null,
|
||||||
|
errorMessage: null,
|
||||||
|
user: null,
|
||||||
|
wallet: null,
|
||||||
|
isPublicPage: true,
|
||||||
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
||||||
offline: !navigator.onLine,
|
offline: !navigator.onLine,
|
||||||
|
hasCamera: false,
|
||||||
visibleDrawer: false,
|
visibleDrawer: false,
|
||||||
extensions: [],
|
extensions: WINDOW_SETTINGS.EXTENSIONS,
|
||||||
user: null,
|
|
||||||
wallet: {},
|
|
||||||
fiatBalance: 0,
|
fiatBalance: 0,
|
||||||
exchangeRate: 0,
|
exchangeRate: 0,
|
||||||
fiatTracking: false,
|
fiatTracking: false,
|
||||||
payments: [],
|
payments: [],
|
||||||
walletEventListeners: [],
|
walletEventListeners: [],
|
||||||
showNewWalletDialog: false,
|
updatePayments: false, // used for updating the lnbits-payment-list
|
||||||
newWalletType: 'lightning',
|
updatePaymentsHash: false, // used for closing the receive dialog
|
||||||
updatePayments: false,
|
|
||||||
currencies: WINDOW_SETTINGS.LNBITS_CURRENCIES ?? [],
|
currencies: WINDOW_SETTINGS.LNBITS_CURRENCIES ?? [],
|
||||||
allowedCurrencies: WINDOW_SETTINGS.LNBITS_ALLOWED_CURRENCIES ?? [],
|
allowedCurrencies: WINDOW_SETTINGS.LNBITS_ALLOWED_CURRENCIES ?? [],
|
||||||
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
|
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
|
||||||
@@ -51,7 +54,9 @@ window.g = Vue.reactive({
|
|||||||
),
|
),
|
||||||
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
|
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
|
||||||
denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION,
|
denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION,
|
||||||
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats'
|
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats',
|
||||||
|
scanner: null,
|
||||||
|
newWalletType: null
|
||||||
})
|
})
|
||||||
|
|
||||||
window.dateFormat = 'YYYY-MM-DD HH:mm'
|
window.dateFormat = 'YYYY-MM-DD HH:mm'
|
||||||
@@ -77,3 +82,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')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,51 @@ const quasarConfig = {
|
|||||||
config: {
|
config: {
|
||||||
loading: {
|
loading: {
|
||||||
spinner: Quasar.QSpinnerBars
|
spinner: Quasar.QSpinnerBars
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
rowsPerPageOptions: [5, 10, 20, 50, 100, 200, 500, 0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DynamicComponent = {
|
||||||
|
async created() {
|
||||||
|
const name = this.$route.path.split('/')[1]
|
||||||
|
const path = `/${name}/`
|
||||||
|
const routesPath = `/${name}/static/routes.json`
|
||||||
|
if (this.$router.getRoutes().some(r => r.path === path)) return
|
||||||
|
if (this.$route.fullPath.startsWith('/extensions/builder/preview')) return
|
||||||
|
fetch(routesPath)
|
||||||
|
.then(async res => {
|
||||||
|
if (!res.ok) throw new Error('No dynamic routes found')
|
||||||
|
const routes = await res.json()
|
||||||
|
routes.forEach(r => {
|
||||||
|
console.log('Adding dynamic route:', r.path)
|
||||||
|
window.router.addRoute({
|
||||||
|
path: r.path,
|
||||||
|
name: r.name,
|
||||||
|
component: async () => {
|
||||||
|
await LNbits.utils.loadTemplate(r.template)
|
||||||
|
await LNbits.utils.loadScript(r.component)
|
||||||
|
return window[r.name]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.router.push(this.$route.fullPath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
let route = RENDERED_ROUTE
|
||||||
|
// append trailing slash only on the root path `/path` -> `/path/`
|
||||||
|
if (route.split('/').length === 2) route += '/'
|
||||||
|
if (route !== this.$route.path) {
|
||||||
|
console.log('Redirecting to non-vue route:', this.$route.fullPath)
|
||||||
|
window.location = this.$route.fullPath
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/node',
|
path: '/node',
|
||||||
@@ -29,6 +70,14 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/wallet',
|
path: '/wallet',
|
||||||
|
redirect: to => {
|
||||||
|
const walletId =
|
||||||
|
window.g?.lastActiveWallet || window.g?.user?.wallets[0].id
|
||||||
|
return `/wallet/${to.query.wal || walletId || 'default'}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/wallet/:id',
|
||||||
name: 'Wallet',
|
name: 'Wallet',
|
||||||
component: PageWallet
|
component: PageWallet
|
||||||
},
|
},
|
||||||
@@ -71,6 +120,16 @@ const routes = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
name: 'PageHome',
|
name: 'PageHome',
|
||||||
component: PageHome
|
component: PageHome
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/error',
|
||||||
|
name: 'PageError',
|
||||||
|
component: PageError
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'DynamicComponent',
|
||||||
|
component: DynamicComponent
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -79,6 +138,9 @@ window.router = VueRouter.createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// BACKWARDS compatibility extensions
|
||||||
|
window.LOCALE = window.g.locale
|
||||||
|
|
||||||
window.i18n = new VueI18n.createI18n({
|
window.i18n = new VueI18n.createI18n({
|
||||||
locale: window.g.locale,
|
locale: window.g.locale,
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
@@ -88,17 +150,16 @@ window.i18n = new VueI18n.createI18n({
|
|||||||
window.app.mixin({
|
window.app.mixin({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
api: window._lnbitsApi,
|
||||||
|
utils: window._lnbitsUtils,
|
||||||
g: window.g,
|
g: window.g,
|
||||||
...WINDOW_SETTINGS
|
...WINDOW_SETTINGS
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
// backwards compatibility for extensions, should not be used in the future
|
||||||
isVueRoute() {
|
methods: {
|
||||||
const currentPath = window.location.pathname
|
copyText: window._lnbitsUtils.copyText,
|
||||||
const matchedRoute = window.router.resolve(currentPath)
|
formatBalance: window._lnbitsUtils.formatBalance
|
||||||
const isVueRoute = matchedRoute?.matched?.length > 0
|
|
||||||
return isVueRoute
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -107,4 +168,8 @@ window.app.use(Quasar, quasarConfig)
|
|||||||
|
|
||||||
window.app.use(window.i18n)
|
window.app.use(window.i18n)
|
||||||
window.app.use(window.router)
|
window.app.use(window.router)
|
||||||
|
|
||||||
|
window.app.component(QrcodeVue)
|
||||||
|
window.app.component('lnbits-error', window.LnbitsError)
|
||||||
|
|
||||||
window.app.mount('#vue')
|
window.app.mount('#vue')
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
window.decryptLnurlPayAES = (success_action, preimage) => {
|
|
||||||
let keyb = new Uint8Array(
|
|
||||||
preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
|
|
||||||
)
|
|
||||||
|
|
||||||
return crypto.subtle
|
|
||||||
.importKey('raw', keyb, {name: 'AES-CBC', length: 256}, false, ['decrypt'])
|
|
||||||
.then(key => {
|
|
||||||
let ivb = Uint8Array.from(window.atob(success_action.iv), c =>
|
|
||||||
c.charCodeAt(0)
|
|
||||||
)
|
|
||||||
let ciphertextb = Uint8Array.from(
|
|
||||||
window.atob(success_action.ciphertext),
|
|
||||||
c => c.charCodeAt(0)
|
|
||||||
)
|
|
||||||
|
|
||||||
return crypto.subtle.decrypt({name: 'AES-CBC', iv: ivb}, key, ciphertextb)
|
|
||||||
})
|
|
||||||
.then(valueb => {
|
|
||||||
let decoder = new TextDecoder('utf-8')
|
|
||||||
return decoder.decode(valueb)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ window.PageAccount = {
|
|||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: null,
|
|
||||||
untouchedUser: null,
|
untouchedUser: null,
|
||||||
hasUsername: false,
|
hasUsername: false,
|
||||||
showUserId: false,
|
showUserId: false,
|
||||||
@@ -191,6 +190,14 @@ window.PageAccount = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
tab(tab) {
|
||||||
|
this.$router.push(`/account#${tab}`)
|
||||||
|
},
|
||||||
|
$route(to) {
|
||||||
|
if (to.hash.length > 1) {
|
||||||
|
this.tab = to.hash.replace('#', '')
|
||||||
|
}
|
||||||
|
},
|
||||||
'assetsTable.search': {
|
'assetsTable.search': {
|
||||||
handler() {
|
handler() {
|
||||||
const props = {}
|
const props = {}
|
||||||
@@ -203,7 +210,7 @@ window.PageAccount = {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isUserTouched() {
|
isUserTouched() {
|
||||||
return JSON.stringify(this.user) !== JSON.stringify(this.untouchedUser)
|
return !_.isEqual(this.g.user, this.untouchedUser)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -221,14 +228,13 @@ window.PageAccount = {
|
|||||||
'/api/v1/auth/update',
|
'/api/v1/auth/update',
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
user_id: this.user.id,
|
user_id: this.g.user.id,
|
||||||
username: this.user.username,
|
username: this.g.user.username,
|
||||||
email: this.user.email,
|
email: this.g.user.email,
|
||||||
extra: this.user.extra
|
extra: this.g.user.extra
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
this.user = data
|
this.untouchedUser = JSON.parse(JSON.stringify(this.g.user))
|
||||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
|
||||||
this.hasUsername = !!data.username
|
this.hasUsername = !!data.username
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
@@ -260,14 +266,13 @@ window.PageAccount = {
|
|||||||
'/api/v1/auth/password',
|
'/api/v1/auth/password',
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
user_id: this.user.id,
|
user_id: this.g.user.id,
|
||||||
username: this.credentialsData.username,
|
username: this.credentialsData.username,
|
||||||
password_old: this.credentialsData.oldPassword,
|
password_old: this.credentialsData.oldPassword,
|
||||||
password: this.credentialsData.newPassword,
|
password: this.credentialsData.newPassword,
|
||||||
password_repeat: this.credentialsData.newPasswordRepeat
|
password_repeat: this.credentialsData.newPasswordRepeat
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
this.user = data
|
|
||||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
||||||
this.hasUsername = !!data.username
|
this.hasUsername = !!data.username
|
||||||
this.credentialsData.show = false
|
this.credentialsData.show = false
|
||||||
@@ -286,11 +291,10 @@ window.PageAccount = {
|
|||||||
'/api/v1/auth/pubkey',
|
'/api/v1/auth/pubkey',
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
user_id: this.user.id,
|
user_id: this.g.user.id,
|
||||||
pubkey: this.credentialsData.pubkey
|
pubkey: this.credentialsData.pubkey
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
this.user = data
|
|
||||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
||||||
this.hasUsername = !!data.username
|
this.hasUsername = !!data.username
|
||||||
this.credentialsData.show = false
|
this.credentialsData.show = false
|
||||||
@@ -306,8 +310,8 @@ window.PageAccount = {
|
|||||||
this.credentialsData = {
|
this.credentialsData = {
|
||||||
show: true,
|
show: true,
|
||||||
oldPassword: null,
|
oldPassword: null,
|
||||||
username: this.user.username,
|
username: this.g.user.username,
|
||||||
pubkey: this.user.pubkey,
|
pubkey: this.g.user.pubkey,
|
||||||
newPassword: null,
|
newPassword: null,
|
||||||
newPasswordRepeat: null
|
newPasswordRepeat: null
|
||||||
}
|
}
|
||||||
@@ -423,7 +427,7 @@ window.PageAccount = {
|
|||||||
'/api/v1/auth/acl',
|
'/api/v1/auth/acl',
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
id: this.user.id,
|
id: this.g.user.id,
|
||||||
password: this.apiAcl.password,
|
password: this.apiAcl.password,
|
||||||
...this.selectedApiAcl
|
...this.selectedApiAcl
|
||||||
}
|
}
|
||||||
@@ -607,7 +611,7 @@ window.PageAccount = {
|
|||||||
},
|
},
|
||||||
copyAssetLinkToClipboard(asset) {
|
copyAssetLinkToClipboard(asset) {
|
||||||
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/binary`
|
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/binary`
|
||||||
this.copyText(assetUrl)
|
this.utils.copyText(assetUrl)
|
||||||
},
|
},
|
||||||
addUserLabel() {
|
addUserLabel() {
|
||||||
if (!this.labelsDialog.data.name) {
|
if (!this.labelsDialog.data.name) {
|
||||||
@@ -624,8 +628,8 @@ window.PageAccount = {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.user.extra.labels = this.user.extra.labels || []
|
this.g.user.extra.labels = this.g.user.extra.labels || []
|
||||||
const duplicate = this.user.extra.labels.find(
|
const duplicate = this.g.user.extra.labels.find(
|
||||||
label => label.name === this.labelsDialog.data.name
|
label => label.name === this.labelsDialog.data.name
|
||||||
)
|
)
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
@@ -635,7 +639,7 @@ window.PageAccount = {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.user.extra.labels.unshift({...this.labelsDialog.data})
|
this.g.user.extra.labels.unshift({...this.labelsDialog.data})
|
||||||
this.labelsDialog.show = false
|
this.labelsDialog.show = false
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
@@ -657,13 +661,15 @@ window.PageAccount = {
|
|||||||
},
|
},
|
||||||
updateUserLabel() {
|
updateUserLabel() {
|
||||||
const label = this.labelsDialog.data
|
const label = this.labelsDialog.data
|
||||||
const existingLabels = JSON.parse(JSON.stringify(this.user.extra.labels))
|
const existingLabels = JSON.parse(
|
||||||
this.user.extra.labels = this.user.extra.labels.filter(
|
JSON.stringify(this.g.user.extra.labels)
|
||||||
|
)
|
||||||
|
this.g.user.extra.labels = this.g.user.extra.labels.filter(
|
||||||
l => l.name !== label.name
|
l => l.name !== label.name
|
||||||
)
|
)
|
||||||
const labelUpdated = this.addUserLabel()
|
const labelUpdated = this.addUserLabel()
|
||||||
if (!labelUpdated) {
|
if (!labelUpdated) {
|
||||||
this.user.extra.labels = existingLabels
|
this.g.user.extra.labels = existingLabels
|
||||||
}
|
}
|
||||||
this.labelsDialog.show = false
|
this.labelsDialog.show = false
|
||||||
},
|
},
|
||||||
@@ -671,7 +677,7 @@ window.PageAccount = {
|
|||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this label?')
|
.confirmDialog('Are you sure you want to delete this label?')
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
this.user.extra.labels = this.user.extra.labels.filter(
|
this.g.user.extra.labels = this.g.user.extra.labels.filter(
|
||||||
l => l.name !== label.name
|
l => l.name !== label.name
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -679,22 +685,13 @@ window.PageAccount = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
this.untouchedUser = JSON.parse(JSON.stringify(this.g.user))
|
||||||
const {data} = await LNbits.api.getAuthenticatedUser()
|
this.hasUsername = !!this.g.user.username
|
||||||
this.user = data
|
if (this.$route.hash.length > 1) {
|
||||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
this.tab = this.$route.hash.replace('#', '')
|
||||||
this.hasUsername = !!data.username
|
|
||||||
if (!this.user.extra) this.user.extra = {}
|
|
||||||
} catch (e) {
|
|
||||||
LNbits.utils.notifyApiError(e)
|
|
||||||
}
|
|
||||||
const hash = window.location.hash.replace('#', '')
|
|
||||||
if (hash) {
|
|
||||||
this.tab = hash
|
|
||||||
}
|
}
|
||||||
await this.getApiACLs()
|
await this.getApiACLs()
|
||||||
await this.getUserAssets()
|
await this.getUserAssets()
|
||||||
|
|
||||||
// filter out themes that are not allowed
|
// filter out themes that are not allowed
|
||||||
this.themeOptions = this.themeOptions.filter(theme =>
|
this.themeOptions = this.themeOptions.filter(theme =>
|
||||||
this.LNBITS_THEME_OPTIONS.includes(theme.name)
|
this.LNBITS_THEME_OPTIONS.includes(theme.name)
|
||||||
|
|||||||
@@ -15,13 +15,22 @@ window.PageAdmin = {
|
|||||||
needsRestart: false
|
needsRestart: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
watch: {
|
||||||
await this.getSettings()
|
tab(tab) {
|
||||||
const hash = window.location.hash.replace('#', '')
|
this.$router.push(`/admin#${tab}`)
|
||||||
if (hash) {
|
},
|
||||||
this.tab = hash
|
$route(to) {
|
||||||
|
if (to.hash.length > 1) {
|
||||||
|
this.tab = to.hash.replace('#', '')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async created() {
|
||||||
|
if (this.$route.hash.length > 1) {
|
||||||
|
this.tab = this.$route.hash.replace('#', '')
|
||||||
|
}
|
||||||
|
await this.getSettings()
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
checkChanges() {
|
checkChanges() {
|
||||||
return !_.isEqual(this.settings, this.formData)
|
return !_.isEqual(this.settings, this.formData)
|
||||||
@@ -29,17 +38,9 @@ window.PageAdmin = {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getDefaultSetting(fieldName) {
|
getDefaultSetting(fieldName) {
|
||||||
LNbits.api
|
LNbits.api.getDefaultSetting(fieldName).then(response => {
|
||||||
.request(
|
this.formData[fieldName] = response.data.default_value
|
||||||
'GET',
|
})
|
||||||
`/admin/api/v1/settings/default?field_name=${fieldName}`
|
|
||||||
)
|
|
||||||
.then(response => {
|
|
||||||
this.formData[fieldName] = response.data.default_value
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
restartServer() {
|
restartServer() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
|
|||||||
@@ -106,9 +106,6 @@ window.PageAudit = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
formatDate(dateString) {
|
|
||||||
return LNbits.utils.formatDateString(dateString)
|
|
||||||
},
|
|
||||||
async fetchAudit(props) {
|
async fetchAudit(props) {
|
||||||
try {
|
try {
|
||||||
const params = LNbits.utils.prepareFilterQuery(this.auditTable, props)
|
const params = LNbits.utils.prepareFilterQuery(this.auditTable, props)
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
window.PageError = {
|
||||||
|
template: '#page-error',
|
||||||
|
mixins: [window.windowMixin]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
window.PageExtensions = {
|
window.PageExtensions = {
|
||||||
template: '#page-extensions',
|
template: '#page-extensions',
|
||||||
mixins: [windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
extbuilderEnabled: false,
|
extbuilderEnabled: false,
|
||||||
@@ -8,9 +8,9 @@ window.PageExtensions = {
|
|||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
tab: 'all',
|
tab: 'installed',
|
||||||
manageExtensionTab: 'releases',
|
manageExtensionTab: 'releases',
|
||||||
filteredExtensions: null,
|
filteredExtensions: [],
|
||||||
updatableExtensions: [],
|
updatableExtensions: [],
|
||||||
showUninstallDialog: false,
|
showUninstallDialog: false,
|
||||||
showManageExtensionDialog: false,
|
showManageExtensionDialog: false,
|
||||||
@@ -26,19 +26,18 @@ window.PageExtensions = {
|
|||||||
selectedRelease: null,
|
selectedRelease: null,
|
||||||
uninstallAndDropDb: false,
|
uninstallAndDropDb: false,
|
||||||
maxStars: 5,
|
maxStars: 5,
|
||||||
paylinkWebsocket: null,
|
paylinkWebsocket: null
|
||||||
user: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
searchTerm(term) {
|
searchTerm(term) {
|
||||||
this.filterExtensions(term, this.tab)
|
this.filterExtensions(term, this.tab)
|
||||||
|
},
|
||||||
|
tab(val) {
|
||||||
|
this.filterExtensions(this.searchTerm, val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleTabChanged(tab) {
|
|
||||||
this.filterExtensions(this.searchTerm, tab)
|
|
||||||
},
|
|
||||||
filterExtensions(term, tab) {
|
filterExtensions(term, tab) {
|
||||||
// Filter the extensions list
|
// Filter the extensions list
|
||||||
function extensionNameContains(searchTerm) {
|
function extensionNameContains(searchTerm) {
|
||||||
@@ -65,7 +64,6 @@ window.PageExtensions = {
|
|||||||
details_link:
|
details_link:
|
||||||
e.installedRelease?.details_link || e.latestRelease?.details_link
|
e.installedRelease?.details_link || e.latestRelease?.details_link
|
||||||
}))
|
}))
|
||||||
this.tab = tab
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async installExtension(release) {
|
async installExtension(release) {
|
||||||
@@ -74,65 +72,66 @@ window.PageExtensions = {
|
|||||||
this.unsubscribeFromPaylinkWs()
|
this.unsubscribeFromPaylinkWs()
|
||||||
|
|
||||||
const extension = this.selectedExtension
|
const extension = this.selectedExtension
|
||||||
extension.inProgress = true
|
this.selectedExtension.inProgress = true
|
||||||
this.showManageExtensionDialog = false
|
this.showManageExtensionDialog = false
|
||||||
release.payment_hash =
|
release.payment_hash =
|
||||||
release.payment_hash || this.getPaylinkHash(release.pay_link)
|
release.payment_hash || this.getPaylinkHash(release.pay_link)
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('POST', `/api/v1/extension`, this.g.user.wallets[0].adminkey, {
|
.request('POST', `/api/v1/extension`, this.g.user.wallets[0].adminkey, {
|
||||||
ext_id: extension.id,
|
ext_id: this.selectedExtension.id,
|
||||||
archive: release.archive,
|
archive: release.archive,
|
||||||
source_repo: release.source_repo,
|
source_repo: release.source_repo,
|
||||||
payment_hash: release.payment_hash,
|
payment_hash: release.payment_hash,
|
||||||
version: release.version
|
version: release.version
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
this.selectedExtension.inProgress = false
|
||||||
|
const extension = this.extensions.find(
|
||||||
|
ext => ext.id === this.selectedExtension.id
|
||||||
|
)
|
||||||
extension.isAvailable = true
|
extension.isAvailable = true
|
||||||
extension.isInstalled = true
|
extension.isInstalled = true
|
||||||
extension.installedRelease = release
|
extension.installedRelease = release
|
||||||
this.toggleExtension(extension)
|
this.toggleExtension(extension)
|
||||||
extension.inProgress = false
|
extension.inProgress = false
|
||||||
this.filteredExtensions = this.extensions.concat([])
|
this.selectedExtension = extension
|
||||||
this.handleTabChanged('installed')
|
this.extensions = this.extensions.concat([])
|
||||||
this.tab = 'installed'
|
this.tab = 'installed'
|
||||||
this.refreshRoute()
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.warn(err)
|
console.warn(err)
|
||||||
extension.inProgress = false
|
this.selectedExtension.inProgress = false
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async uninstallExtension() {
|
async uninstallExtension() {
|
||||||
const extension = this.selectedExtension
|
|
||||||
this.showManageExtensionDialog = false
|
this.showManageExtensionDialog = false
|
||||||
this.showUninstallDialog = false
|
this.showUninstallDialog = false
|
||||||
extension.inProgress = true
|
this.selectedExtension.inProgress = true
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
`/api/v1/extension/${extension.id}`,
|
`/api/v1/extension/${this.selectedExtension.id}`,
|
||||||
this.g.user.wallets[0].adminkey
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
const extension = this.extensions.find(
|
||||||
|
ext => ext.id === this.selectedExtension.id
|
||||||
|
)
|
||||||
extension.isAvailable = false
|
extension.isAvailable = false
|
||||||
extension.isInstalled = false
|
extension.isInstalled = false
|
||||||
extension.inProgress = false
|
extension.inProgress = false
|
||||||
extension.installedRelease = null
|
extension.installedRelease = null
|
||||||
this.filteredExtensions = this.extensions.concat([])
|
this.filteredExtensions = this.filteredExtensions.filter(
|
||||||
this.handleTabChanged('installed')
|
ext => ext.id !== extension.id
|
||||||
this.tab = 'installed'
|
)
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Extension uninstalled!'
|
message: 'Extension uninstalled!'
|
||||||
})
|
})
|
||||||
if (this.uninstallAndDropDb) {
|
if (this.uninstallAndDropDb) {
|
||||||
this.showDropDb()
|
this.showDropDb()
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.refreshRoute()
|
|
||||||
}, 300)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -160,9 +159,6 @@ window.PageExtensions = {
|
|||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Extension DB deleted!'
|
message: 'Extension DB deleted!'
|
||||||
})
|
})
|
||||||
setTimeout(() => {
|
|
||||||
this.refreshRoute()
|
|
||||||
}, 300)
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
@@ -626,9 +622,6 @@ window.PageExtensions = {
|
|||||||
message: `${count ? count : 'No'} extensions updated!`
|
message: `${count ? count : 'No'} extensions updated!`
|
||||||
})
|
})
|
||||||
this.showUpdateAllDialog = false
|
this.showUpdateAllDialog = false
|
||||||
setTimeout(() => {
|
|
||||||
this.refreshRoute()
|
|
||||||
}, 2000)
|
|
||||||
},
|
},
|
||||||
async fetchAllExtensions() {
|
async fetchAllExtensions() {
|
||||||
try {
|
try {
|
||||||
@@ -643,21 +636,23 @@ window.PageExtensions = {
|
|||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.extensions = await this.fetchAllExtensions()
|
this.extensions = await this.fetchAllExtensions()
|
||||||
this.extbuilderEnabled = user.admin || this.LNBITS_EXT_BUILDER
|
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
|
||||||
this.filteredExtensions = this.extensions.concat([])
|
|
||||||
const hash = window.location.hash.replace('#', '')
|
if (this.g.user.extensions.length === 0) {
|
||||||
const ext = this.filteredExtensions.find(ext => ext.id === hash)
|
this.tab = 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
const extId = window.location.hash.replace('#', '')
|
||||||
|
const ext = this.extensions.find(ext => ext.id === extId)
|
||||||
|
|
||||||
if (ext) {
|
if (ext) {
|
||||||
this.searchTerm = ext.id
|
this.searchTerm = ext.id
|
||||||
this.handleTabChanged(ext.isInstalled ? 'installed' : 'all')
|
if (ext.isInstalled) this.tab = 'installed'
|
||||||
this.tab = ext.isInstalled ? 'installed' : 'all'
|
|
||||||
} else {
|
|
||||||
const hasInstalled = this.filteredExtensions.some(ext => ext.isInstalled)
|
|
||||||
this.handleTabChanged(hasInstalled ? 'installed' : 'all')
|
|
||||||
this.tab = hasInstalled ? 'installed' : 'all'
|
|
||||||
}
|
}
|
||||||
this.updatableExtensions = this.extensions.filter(ext =>
|
this.updatableExtensions = this.extensions.filter(ext =>
|
||||||
this.hasNewVersion(ext)
|
this.hasNewVersion(ext)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.filterExtensions(this.searchTerm, this.tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ window.PageHome = {
|
|||||||
return this.LNBITS_CUSTOM_IMAGE
|
return this.LNBITS_CUSTOM_IMAGE
|
||||||
},
|
},
|
||||||
showHomepageElements() {
|
showHomepageElements() {
|
||||||
return this.HOMEPAGE_ELEMENTS_ENABLED
|
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS === true
|
||||||
},
|
},
|
||||||
siteTitle() {
|
siteTitle() {
|
||||||
return this.SITE_TITLE || ''
|
return this.SITE_TITLE || ''
|
||||||
@@ -54,13 +54,6 @@ window.PageHome = {
|
|||||||
},
|
},
|
||||||
ads() {
|
ads() {
|
||||||
return this.AD_SPACE.map(ad => ad.split(';'))
|
return this.AD_SPACE.map(ad => ad.split(';'))
|
||||||
},
|
|
||||||
lnbitsBannerEnabled() {
|
|
||||||
return (
|
|
||||||
this.LNBITS_DENOMINATION == 'sats' &&
|
|
||||||
this.SITE_TITLE == 'LNbits' &&
|
|
||||||
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -86,7 +79,7 @@ window.PageHome = {
|
|||||||
this.password,
|
this.password,
|
||||||
this.passwordRepeat
|
this.passwordRepeat
|
||||||
)
|
)
|
||||||
window.location.href = '/wallet'
|
this.refreshAuthUser()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
@@ -98,7 +91,7 @@ window.PageHome = {
|
|||||||
this.password,
|
this.password,
|
||||||
this.passwordRepeat
|
this.passwordRepeat
|
||||||
)
|
)
|
||||||
window.location.href = '/wallet'
|
this.refreshAuthUser()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
@@ -106,7 +99,7 @@ window.PageHome = {
|
|||||||
async login() {
|
async login() {
|
||||||
try {
|
try {
|
||||||
await LNbits.api.login(this.username, this.password)
|
await LNbits.api.login(this.username, this.password)
|
||||||
window.location.href = '/wallet'
|
this.refreshAuthUser()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
@@ -114,8 +107,18 @@ window.PageHome = {
|
|||||||
async loginUsr() {
|
async loginUsr() {
|
||||||
try {
|
try {
|
||||||
await LNbits.api.loginUsr(this.usr)
|
await LNbits.api.loginUsr(this.usr)
|
||||||
this.usr = ''
|
this.refreshAuthUser()
|
||||||
window.location.href = '/wallet'
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async refreshAuthUser() {
|
||||||
|
try {
|
||||||
|
const res = await LNbits.api.getAuthUser()
|
||||||
|
this.g.user = LNbits.map.user(res.data)
|
||||||
|
this.g.isPublicPage = false
|
||||||
|
this.$router.push(`/wallet/${this.g.user.wallets[0].id}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
@@ -123,7 +126,7 @@ window.PageHome = {
|
|||||||
},
|
},
|
||||||
createWallet() {
|
createWallet() {
|
||||||
LNbits.api.createAccount(this.walletName).then(res => {
|
LNbits.api.createAccount(this.walletName).then(res => {
|
||||||
window.location = '/wallet?usr=' + res.data.user + '&wal=' + res.data.id
|
this.$router.push(`/wallet/${res.data.id}`)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
processing() {
|
processing() {
|
||||||
@@ -136,7 +139,7 @@ window.PageHome = {
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.g.isUserAuthorized) {
|
if (this.g.isUserAuthorized) {
|
||||||
window.location.href = '/wallet'
|
return this.refreshAuthUser()
|
||||||
}
|
}
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
this.reset_key = urlParams.get('reset_key')
|
this.reset_key = urlParams.get('reset_key')
|
||||||
|
|||||||
@@ -91,14 +91,14 @@ window.PageNode = {
|
|||||||
{
|
{
|
||||||
name: 'sat',
|
name: 'sat',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
label: this.$t('amount'),
|
||||||
field: row => this.formatMsat(row.amount),
|
field: row => this.formatMsat(row.amount),
|
||||||
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'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -144,7 +144,7 @@ window.PageNode = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'amount',
|
name: 'amount',
|
||||||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
label: this.$t('amount'),
|
||||||
field: row => this.formatMsat(row.amount),
|
field: row => this.formatMsat(row.amount),
|
||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -256,9 +256,6 @@ window.PagePayments = {
|
|||||||
this.paymentDetails = payment
|
this.paymentDetails = payment
|
||||||
return (this.showDetails = !this.showDetails)
|
return (this.showDetails = !this.showDetails)
|
||||||
},
|
},
|
||||||
formatDate(dateString) {
|
|
||||||
return LNbits.utils.formatDateString(dateString)
|
|
||||||
},
|
|
||||||
formatCurrency(amount, currency) {
|
formatCurrency(amount, currency) {
|
||||||
try {
|
try {
|
||||||
return LNbits.utils.formatCurrency(amount, currency)
|
return LNbits.utils.formatCurrency(amount, currency)
|
||||||
|
|||||||
@@ -176,9 +176,6 @@ window.PageUsers = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
formatDate(date) {
|
|
||||||
return LNbits.utils.formatDateString(date)
|
|
||||||
},
|
|
||||||
formatSat(value) {
|
formatSat(value) {
|
||||||
return LNbits.utils.formatSat(Math.floor(value / 1000))
|
return LNbits.utils.formatSat(Math.floor(value / 1000))
|
||||||
},
|
},
|
||||||
@@ -201,7 +198,7 @@ window.PageUsers = {
|
|||||||
)
|
)
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
const url = window.location.origin + '?reset_key=' + res.data
|
const url = window.location.origin + '?reset_key=' + res.data
|
||||||
this.copyText(url)
|
this.utils.copyText(url)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
@@ -350,7 +347,7 @@ window.PageUsers = {
|
|||||||
},
|
},
|
||||||
copyWalletLink(walletId) {
|
copyWalletLink(walletId) {
|
||||||
const url = `${window.location.origin}/wallet?usr=${this.activeWallet.userId}&wal=${walletId}`
|
const url = `${window.location.origin}/wallet?usr=${this.activeWallet.userId}&wal=${walletId}`
|
||||||
this.copyText(url)
|
this.utils.copyText(url)
|
||||||
},
|
},
|
||||||
fetchUsers(props) {
|
fetchUsers(props) {
|
||||||
this.relaxFilterForFields(['username', 'email'])
|
this.relaxFilterForFields(['username', 'email'])
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ window.PageWallet = {
|
|||||||
amountMsat: null,
|
amountMsat: null,
|
||||||
minMax: [0, 2100000000000000],
|
minMax: [0, 2100000000000000],
|
||||||
lnurl: null,
|
lnurl: null,
|
||||||
units: ['sat', ...(this.currencies || [])],
|
units: [],
|
||||||
unit: 'sat',
|
unit: 'sat',
|
||||||
fiatProvider: '',
|
fiatProvider: '',
|
||||||
data: {
|
data: {
|
||||||
@@ -49,7 +49,6 @@ window.PageWallet = {
|
|||||||
hasNfc: false,
|
hasNfc: false,
|
||||||
nfcReaderAbortController: null,
|
nfcReaderAbortController: null,
|
||||||
formattedFiatAmount: 0,
|
formattedFiatAmount: 0,
|
||||||
formattedExchange: null,
|
|
||||||
paymentFilter: {
|
paymentFilter: {
|
||||||
'status[ne]': 'failed'
|
'status[ne]': 'failed'
|
||||||
},
|
},
|
||||||
@@ -75,12 +74,10 @@ window.PageWallet = {
|
|||||||
return this.parse.invoice.sat <= this.g.wallet.sat
|
return this.parse.invoice.sat <= this.g.wallet.sat
|
||||||
},
|
},
|
||||||
formattedAmount() {
|
formattedAmount() {
|
||||||
if (this.receive.unit != 'sat' || LNBITS_DENOMINATION != 'sats') {
|
if (this.receive.unit != 'sat' || !this.g.isSatsDenomination) {
|
||||||
return LNbits.utils.formatCurrency(
|
return LNbits.utils.formatCurrency(
|
||||||
Number(this.receive.data.amount).toFixed(2),
|
Number(this.receive.data.amount).toFixed(2),
|
||||||
LNBITS_DENOMINATION != 'sats'
|
!this.g.isSatsDenomination ? this.g.denomination : this.receive.unit
|
||||||
? LNBITS_DENOMINATION
|
|
||||||
: this.receive.unit
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
|
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
|
||||||
@@ -99,12 +96,6 @@ window.PageWallet = {
|
|||||||
msatoshiFormat(value) {
|
msatoshiFormat(value) {
|
||||||
return LNbits.utils.formatSat(value / 1000)
|
return LNbits.utils.formatSat(value / 1000)
|
||||||
},
|
},
|
||||||
closeCamera() {
|
|
||||||
this.parse.camera.show = false
|
|
||||||
},
|
|
||||||
showCamera() {
|
|
||||||
this.parse.camera.show = true
|
|
||||||
},
|
|
||||||
showReceiveDialog() {
|
showReceiveDialog() {
|
||||||
this.receive.show = true
|
this.receive.show = true
|
||||||
this.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
@@ -114,6 +105,12 @@ window.PageWallet = {
|
|||||||
this.receive.data.memo = null
|
this.receive.data.memo = null
|
||||||
this.receive.data.internalMemo = null
|
this.receive.data.internalMemo = null
|
||||||
this.receive.data.payment_hash = null
|
this.receive.data.payment_hash = null
|
||||||
|
this.receive.units = [
|
||||||
|
'sat',
|
||||||
|
...(this.g.allowedCurrencies.length > 0
|
||||||
|
? this.g.allowedCurrencies
|
||||||
|
: this.g.currencies)
|
||||||
|
]
|
||||||
this.receive.unit = this.g.isFiatPriority
|
this.receive.unit = this.g.isFiatPriority
|
||||||
? this.g.wallet.currency || 'sat'
|
? this.g.wallet.currency || 'sat'
|
||||||
: 'sat'
|
: 'sat'
|
||||||
@@ -148,7 +145,7 @@ window.PageWallet = {
|
|||||||
},
|
},
|
||||||
createInvoice() {
|
createInvoice() {
|
||||||
this.receive.status = 'loading'
|
this.receive.status = 'loading'
|
||||||
if (LNBITS_DENOMINATION != 'sats') {
|
if (!this.g.isSatsDenomination) {
|
||||||
this.receive.data.amount = this.receive.data.amount * 100
|
this.receive.data.amount = this.receive.data.amount * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,35 +204,6 @@ window.PageWallet = {
|
|||||||
this.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
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})`
|
|
||||||
this.parse.camera.show = false
|
|
||||||
Quasar.Notify.create({
|
|
||||||
message: camera_error,
|
|
||||||
type: 'negative'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lnurlScan() {
|
lnurlScan() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('POST', '/api/v1/lnurlscan', this.g.wallet.adminkey, {
|
.request('POST', '/api/v1/lnurlscan', this.g.wallet.adminkey, {
|
||||||
@@ -283,8 +251,8 @@ window.PageWallet = {
|
|||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
decodeQR(res) {
|
decodeQR(val) {
|
||||||
this.parse.data.request = res[0].rawValue
|
this.parse.data.request = val
|
||||||
this.decodeRequest()
|
this.decodeRequest()
|
||||||
this.parse.camera.show = false
|
this.parse.camera.show = false
|
||||||
},
|
},
|
||||||
@@ -458,7 +426,7 @@ window.PageWallet = {
|
|||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'aes':
|
case 'aes':
|
||||||
decryptLnurlPayAES(action, response.data.preimage)
|
this.utils.decryptLnurlPayAES(action, response.data.preimage)
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
message: value,
|
message: value,
|
||||||
caption: extra.success_action.description,
|
caption: extra.success_action.description,
|
||||||
@@ -641,23 +609,21 @@ window.PageWallet = {
|
|||||||
this.decodeRequest()
|
this.decodeRequest()
|
||||||
this.parse.show = true
|
this.parse.show = true
|
||||||
}
|
}
|
||||||
if (urlParams.has('wal')) {
|
const wallet = this.g.user.wallets.find(w => w.id === this.$route.params.id)
|
||||||
const wallet = g.user.wallets.find(w => w.id === urlParams.get('wal'))
|
if (wallet) {
|
||||||
if (wallet) {
|
this.g.wallet = wallet
|
||||||
this.selectWallet(wallet)
|
this.g.lastActiveWallet = wallet.id
|
||||||
}
|
this.$q.localStorage.setItem('lnbits.lastActiveWallet', wallet.id)
|
||||||
|
this.$router.replace(`/wallet/${wallet.id}`)
|
||||||
} else {
|
} else {
|
||||||
const wallet = g.user.wallets.find(w => w.id === this.g.lastActiveWallet)
|
this.g.errorCode = 404
|
||||||
if (wallet) {
|
this.g.errorMessage = 'Wallet not found.'
|
||||||
this.selectWallet(wallet)
|
this.$router.push('/error')
|
||||||
} else if (g.user.wallets.length > 0) {
|
|
||||||
this.selectWallet(g.user.wallets[0])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'g.lastActiveWallet'(val) {
|
'g.updatePaymentsHash'() {
|
||||||
this.$q.localStorage.setItem('lnbits.lastActiveWallet', val)
|
this.receive.show = false
|
||||||
},
|
},
|
||||||
'g.updatePayments'() {
|
'g.updatePayments'() {
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
@@ -697,10 +663,6 @@ window.PageWallet = {
|
|||||||
if (this.g.fiatTracking && this.g.wallet.currency) {
|
if (this.g.fiatTracking && this.g.wallet.currency) {
|
||||||
this.g.fiatBalance =
|
this.g.fiatBalance =
|
||||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||||
this.formattedExchange = LNbits.utils.formatCurrency(
|
|
||||||
this.g.exchangeRate,
|
|
||||||
this.g.wallet.currency
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,44 @@
|
|||||||
window._lnbitsUtils = {
|
window._lnbitsUtils = {
|
||||||
|
url_for(url) {
|
||||||
|
const _url = new URL(url, window.location.origin)
|
||||||
|
_url.searchParams.set('v', WINDOW_SETTINGS.CACHE_KEY)
|
||||||
|
return _url.toString()
|
||||||
|
},
|
||||||
|
loadScript(src) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = this.url_for(src)
|
||||||
|
script.onload = () => {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
script.onerror = () => {
|
||||||
|
reject(new Error(`Failed to load script ${src}`))
|
||||||
|
}
|
||||||
|
document.body.appendChild(script)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async loadTemplate(url) {
|
||||||
|
return fetch(this.url_for(url))
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load template from ${url}`)
|
||||||
|
}
|
||||||
|
return response.text()
|
||||||
|
})
|
||||||
|
.then(html => {
|
||||||
|
const template = document.createElement('div')
|
||||||
|
template.innerHTML = html.trim()
|
||||||
|
document.body.appendChild(template)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
copyText(text, message, position) {
|
||||||
|
Quasar.copyToClipboard(text).then(() => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: message || 'Copied to clipboard!',
|
||||||
|
position: position || 'bottom'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
confirmDialog(msg) {
|
confirmDialog(msg) {
|
||||||
return Quasar.Dialog.create({
|
return Quasar.Dialog.create({
|
||||||
message: msg,
|
message: msg,
|
||||||
@@ -34,11 +74,33 @@ window._lnbitsUtils = {
|
|||||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
return hashHex
|
return hashHex
|
||||||
},
|
},
|
||||||
formatDate(timestamp) {
|
formatTimestamp(timestamp, format = null) {
|
||||||
return Quasar.date.formatDate(new Date(timestamp * 1000), window.dateFormat)
|
format = format || window.dateFormat
|
||||||
|
return Quasar.date.formatDate(new Date(timestamp * 1000), format)
|
||||||
},
|
},
|
||||||
|
// backwards compatibility for extensions
|
||||||
formatDateString(isoDateString) {
|
formatDateString(isoDateString) {
|
||||||
return Quasar.date.formatDate(new Date(isoDateString), window.dateFormat)
|
this.formatDate(isoDateString)
|
||||||
|
},
|
||||||
|
formatDate(isoDateString, format = null) {
|
||||||
|
format = format || window.dateFormat
|
||||||
|
return Quasar.date.formatDate(new Date(isoDateString), format)
|
||||||
|
},
|
||||||
|
formatTimestampFrom(timestamp) {
|
||||||
|
return moment
|
||||||
|
.utc(timestamp * 1000)
|
||||||
|
.local()
|
||||||
|
.fromNow()
|
||||||
|
},
|
||||||
|
formatDateFrom(isoDateString) {
|
||||||
|
const timestampMs = new Date(isoDateString).getTime()
|
||||||
|
return moment.utc(timestampMs).local().fromNow()
|
||||||
|
},
|
||||||
|
formatBalance(amount, denomination = 'sats') {
|
||||||
|
if (denomination === 'sats') {
|
||||||
|
return LNbits.utils.formatSat(amount) + ' sats'
|
||||||
|
}
|
||||||
|
return LNbits.utils.formatCurrency(amount / 100, denomination)
|
||||||
},
|
},
|
||||||
formatCurrency(value, currency) {
|
formatCurrency(value, currency) {
|
||||||
return new Intl.NumberFormat(window.i18n.global.locale, {
|
return new Intl.NumberFormat(window.i18n.global.locale, {
|
||||||
@@ -159,5 +221,34 @@ window._lnbitsUtils = {
|
|||||||
converter.setFlavor('github')
|
converter.setFlavor('github')
|
||||||
converter.setOption('simpleLineBreaks', true)
|
converter.setOption('simpleLineBreaks', true)
|
||||||
return converter.makeHtml(text)
|
return converter.makeHtml(text)
|
||||||
|
},
|
||||||
|
async decryptLnurlPayAES(success_action, preimage) {
|
||||||
|
let keyb = new Uint8Array(
|
||||||
|
preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
|
||||||
|
)
|
||||||
|
|
||||||
|
return crypto.subtle
|
||||||
|
.importKey('raw', keyb, {name: 'AES-CBC', length: 256}, false, [
|
||||||
|
'decrypt'
|
||||||
|
])
|
||||||
|
.then(key => {
|
||||||
|
let ivb = Uint8Array.from(window.atob(success_action.iv), c =>
|
||||||
|
c.charCodeAt(0)
|
||||||
|
)
|
||||||
|
let ciphertextb = Uint8Array.from(
|
||||||
|
window.atob(success_action.ciphertext),
|
||||||
|
c => c.charCodeAt(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
return crypto.subtle.decrypt(
|
||||||
|
{name: 'AES-CBC', iv: ivb},
|
||||||
|
key,
|
||||||
|
ciphertextb
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.then(valueb => {
|
||||||
|
let decoder = new TextDecoder('utf-8')
|
||||||
|
return decoder.decode(valueb)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1 @@
|
|||||||
window.windowMixin = {
|
window.windowMixin = {}
|
||||||
methods: {
|
|
||||||
openNewWalletDialog(walletType = 'lightning') {
|
|
||||||
this.g.newWalletType = walletType
|
|
||||||
this.g.showNewWalletDialog = true
|
|
||||||
},
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: react only on incoming payments for now
|
|
||||||
if (data.payment.amount > 0) {
|
|
||||||
eventReaction(data.wallet_balance * 1000)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
paymentEvents() {
|
|
||||||
let timeout
|
|
||||||
this.g.user.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectWallet(wallet) {
|
|
||||||
this.g.wallet = wallet
|
|
||||||
this.g.lastActiveWallet = wallet.id
|
|
||||||
this.g.updatePayments = !this.g.updatePayments
|
|
||||||
this.balance = parseInt(wallet.balance_msat / 1000)
|
|
||||||
const currentPath = this.$route.path
|
|
||||||
if (currentPath !== '/wallet') {
|
|
||||||
this.$router.push({
|
|
||||||
path: '/wallet',
|
|
||||||
query: {wal: this.g.wallet.id}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.$router.replace({
|
|
||||||
path: '/wallet',
|
|
||||||
query: {wal: this.g.wallet.id}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formatDate(date) {
|
|
||||||
return moment
|
|
||||||
.utc(date * 1000)
|
|
||||||
.local()
|
|
||||||
.fromNow()
|
|
||||||
},
|
|
||||||
formatBalance(amount) {
|
|
||||||
if (LNBITS_DENOMINATION != 'sats') {
|
|
||||||
return LNbits.utils.formatCurrency(amount / 100, LNBITS_DENOMINATION)
|
|
||||||
} else {
|
|
||||||
return LNbits.utils.formatSat(amount) + ' sats'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
copyText(text, message, position) {
|
|
||||||
Quasar.copyToClipboard(text).then(() => {
|
|
||||||
Quasar.Notify.create({
|
|
||||||
message: message || 'Copied to clipboard!',
|
|
||||||
position: position || 'bottom'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
refreshRoute() {
|
|
||||||
const path = window.location.pathname
|
|
||||||
console.log(path)
|
|
||||||
|
|
||||||
this.$router.push('/temp').then(() => {
|
|
||||||
this.$router.replace({path})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
// map jinja variable once on pageload
|
|
||||||
if (window.user && !this.g.user) {
|
|
||||||
this.g.user = window.LNbits.map.user(window.user)
|
|
||||||
this.paymentEvents()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
@use 'sass:color';
|
@use 'sass:color';
|
||||||
|
@use 'themes';
|
||||||
|
|
||||||
@each $theme, $colors in $themes {
|
@each $theme, $colors in themes.$themes {
|
||||||
body[data-theme='#{$theme}'] {
|
body[data-theme='#{$theme}'] {
|
||||||
@each $name, $color in $colors {
|
@each $name, $color in $colors {
|
||||||
@if $name== 'dark' {
|
@if $name== 'dark' {
|
||||||
@@ -62,6 +63,6 @@ body.body--dark {
|
|||||||
.q-drawer {
|
.q-drawer {
|
||||||
--q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)};
|
--q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)};
|
||||||
background-color: var(--q-dark);
|
background-color: var(--q-dark);
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px) brightness(0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@import 'themes';
|
@use 'themes';
|
||||||
@import 'borders';
|
@use 'borders';
|
||||||
@import 'background';
|
@use 'background';
|
||||||
@import 'home';
|
@use 'home';
|
||||||
|
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -197,3 +197,12 @@ video {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
font-size: clamp(15vh, 20vw, 30vh);
|
||||||
|
}
|
||||||
|
.error-message {
|
||||||
|
font-size: clamp(1.5rem, calc(1.5 / 10 * 20vw), 3.75rem);
|
||||||
|
font-weight: 300;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
@each $theme, $colors in $themes {
|
@use 'themes';
|
||||||
|
|
||||||
|
@each $theme, $colors in themes.$themes {
|
||||||
@each $name, $color in $colors {
|
@each $name, $color in $colors {
|
||||||
@if $name== 'primary' {
|
@if $name== 'primary' {
|
||||||
body[data-theme='#{$theme}'].neon-border {
|
body[data-theme='#{$theme}'].neon-border {
|
||||||
|
|||||||
@@ -39,10 +39,10 @@
|
|||||||
"js/base.js",
|
"js/base.js",
|
||||||
"js/windowMixin.js",
|
"js/windowMixin.js",
|
||||||
"js/event-reactions.js",
|
"js/event-reactions.js",
|
||||||
"js/bolt11-decoder.js",
|
"js/bolt11-decoder.js"
|
||||||
"js/lnurl.js"
|
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
"js/pages/error.js",
|
||||||
"js/pages/home.js",
|
"js/pages/home.js",
|
||||||
"js/pages/extensions_builder.js",
|
"js/pages/extensions_builder.js",
|
||||||
"js/pages/extensions.js",
|
"js/pages/extensions.js",
|
||||||
@@ -84,6 +84,7 @@
|
|||||||
"js/components/lnbits-header-wallets.js",
|
"js/components/lnbits-header-wallets.js",
|
||||||
"js/components/lnbits-drawer.js",
|
"js/components/lnbits-drawer.js",
|
||||||
"js/components/lnbits-theme.js",
|
"js/components/lnbits-theme.js",
|
||||||
|
"js/components/lnbits-qrcode-scanner.js",
|
||||||
"js/components/lnbits-manage-extension-list.js",
|
"js/components/lnbits-manage-extension-list.js",
|
||||||
"js/components/lnbits-manage-wallet-list.js",
|
"js/components/lnbits-manage-wallet-list.js",
|
||||||
"js/components/lnbits-language-dropdown.js",
|
"js/components/lnbits-language-dropdown.js",
|
||||||
|
|||||||
Vendored
+157
-85
@@ -1,4 +1,4 @@
|
|||||||
/*! Axios v1.12.0 Copyright (c) 2025 Matt Zabriskie and contributors */
|
/*! Axios v1.13.2 Copyright (c) 2025 Matt Zabriskie and contributors */
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
@@ -668,6 +668,13 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a bound version of a function with a specified `this` context
|
||||||
|
*
|
||||||
|
* @param {Function} fn - The function to bind
|
||||||
|
* @param {*} thisArg - The value to be passed as the `this` parameter
|
||||||
|
* @returns {Function} A new function that will call the original function with the specified `this` context
|
||||||
|
*/
|
||||||
function bind(fn, thisArg) {
|
function bind(fn, thisArg) {
|
||||||
return function wrap() {
|
return function wrap() {
|
||||||
return fn.apply(thisArg, arguments);
|
return fn.apply(thisArg, arguments);
|
||||||
@@ -1031,10 +1038,8 @@
|
|||||||
result[targetKey] = merge({}, val);
|
result[targetKey] = merge({}, val);
|
||||||
} else if (isArray(val)) {
|
} else if (isArray(val)) {
|
||||||
result[targetKey] = val.slice();
|
result[targetKey] = val.slice();
|
||||||
} else {
|
} else if (!skipUndefined || !isUndefined(val)) {
|
||||||
if (!skipUndefined || !isUndefined(val)) {
|
result[targetKey] = val;
|
||||||
result[targetKey] = val;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for (var i = 0, l = arguments.length; i < l; i++) {
|
for (var i = 0, l = arguments.length; i < l; i++) {
|
||||||
@@ -1833,7 +1838,7 @@
|
|||||||
*
|
*
|
||||||
* @param {Number} id The ID that was returned by `use`
|
* @param {Number} id The ID that was returned by `use`
|
||||||
*
|
*
|
||||||
* @returns {Boolean} `true` if the interceptor was removed, `false` otherwise
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
}, {
|
}, {
|
||||||
key: "eject",
|
key: "eject",
|
||||||
@@ -2704,20 +2709,33 @@
|
|||||||
var cookies = platform.hasStandardBrowserEnv ?
|
var cookies = platform.hasStandardBrowserEnv ?
|
||||||
// Standard browser envs support document.cookie
|
// Standard browser envs support document.cookie
|
||||||
{
|
{
|
||||||
write: function write(name, value, expires, path, domain, secure) {
|
write: function write(name, value, expires, path, domain, secure, sameSite) {
|
||||||
var cookie = [name + '=' + encodeURIComponent(value)];
|
if (typeof document === 'undefined') return;
|
||||||
utils$1.isNumber(expires) && cookie.push('expires=' + new Date(expires).toGMTString());
|
var cookie = ["".concat(name, "=").concat(encodeURIComponent(value))];
|
||||||
utils$1.isString(path) && cookie.push('path=' + path);
|
if (utils$1.isNumber(expires)) {
|
||||||
utils$1.isString(domain) && cookie.push('domain=' + domain);
|
cookie.push("expires=".concat(new Date(expires).toUTCString()));
|
||||||
secure === true && cookie.push('secure');
|
}
|
||||||
|
if (utils$1.isString(path)) {
|
||||||
|
cookie.push("path=".concat(path));
|
||||||
|
}
|
||||||
|
if (utils$1.isString(domain)) {
|
||||||
|
cookie.push("domain=".concat(domain));
|
||||||
|
}
|
||||||
|
if (secure === true) {
|
||||||
|
cookie.push('secure');
|
||||||
|
}
|
||||||
|
if (utils$1.isString(sameSite)) {
|
||||||
|
cookie.push("SameSite=".concat(sameSite));
|
||||||
|
}
|
||||||
document.cookie = cookie.join('; ');
|
document.cookie = cookie.join('; ');
|
||||||
},
|
},
|
||||||
read: function read(name) {
|
read: function read(name) {
|
||||||
var match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'));
|
if (typeof document === 'undefined') return null;
|
||||||
return match ? decodeURIComponent(match[3]) : null;
|
var match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
|
||||||
|
return match ? decodeURIComponent(match[1]) : null;
|
||||||
},
|
},
|
||||||
remove: function remove(name) {
|
remove: function remove(name) {
|
||||||
this.write(name, '', Date.now() - 86400000);
|
this.write(name, '', Date.now() - 86400000, '/');
|
||||||
}
|
}
|
||||||
} :
|
} :
|
||||||
// Non-standard browser env (web workers, react-native) lack needed support.
|
// Non-standard browser env (web workers, react-native) lack needed support.
|
||||||
@@ -3353,11 +3371,9 @@
|
|||||||
var DEFAULT_CHUNK_SIZE = 64 * 1024;
|
var DEFAULT_CHUNK_SIZE = 64 * 1024;
|
||||||
var isFunction = utils$1.isFunction;
|
var isFunction = utils$1.isFunction;
|
||||||
var globalFetchAPI = function (_ref) {
|
var globalFetchAPI = function (_ref) {
|
||||||
var fetch = _ref.fetch,
|
var Request = _ref.Request,
|
||||||
Request = _ref.Request,
|
|
||||||
Response = _ref.Response;
|
Response = _ref.Response;
|
||||||
return {
|
return {
|
||||||
fetch: fetch,
|
|
||||||
Request: Request,
|
Request: Request,
|
||||||
Response: Response
|
Response: Response
|
||||||
};
|
};
|
||||||
@@ -3376,11 +3392,14 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
var factory = function factory(env) {
|
var factory = function factory(env) {
|
||||||
var _Object$assign = Object.assign({}, globalFetchAPI, env),
|
env = utils$1.merge.call({
|
||||||
fetch = _Object$assign.fetch,
|
skipUndefined: true
|
||||||
Request = _Object$assign.Request,
|
}, globalFetchAPI, env);
|
||||||
Response = _Object$assign.Response;
|
var _env = env,
|
||||||
var isFetchSupported = isFunction(fetch);
|
envFetch = _env.fetch,
|
||||||
|
Request = _env.Request,
|
||||||
|
Response = _env.Response;
|
||||||
|
var isFetchSupported = envFetch ? isFunction(envFetch) : typeof fetch === 'function';
|
||||||
var isRequestSupported = isFunction(Request);
|
var isRequestSupported = isFunction(Request);
|
||||||
var isResponseSupported = isFunction(Response);
|
var isResponseSupported = isFunction(Response);
|
||||||
if (!isFetchSupported) {
|
if (!isFetchSupported) {
|
||||||
@@ -3521,31 +3540,32 @@
|
|||||||
}();
|
}();
|
||||||
return /*#__PURE__*/function () {
|
return /*#__PURE__*/function () {
|
||||||
var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(config) {
|
var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(config) {
|
||||||
var _resolveConfig, url, method, data, signal, cancelToken, timeout, onDownloadProgress, onUploadProgress, responseType, headers, _resolveConfig$withCr, withCredentials, fetchOptions, composedSignal, request, unsubscribe, requestContentLength, _request, contentTypeHeader, _progressEventDecorat, _progressEventDecorat2, onProgress, flush, isCredentialsSupported, resolvedOptions, response, isStreamResponse, options, responseContentLength, _ref6, _ref7, _onProgress, _flush, responseData;
|
var _resolveConfig, url, method, data, signal, cancelToken, timeout, onDownloadProgress, onUploadProgress, responseType, headers, _resolveConfig$withCr, withCredentials, fetchOptions, _fetch, composedSignal, request, unsubscribe, requestContentLength, _request, contentTypeHeader, _progressEventDecorat, _progressEventDecorat2, onProgress, flush, isCredentialsSupported, resolvedOptions, response, isStreamResponse, options, responseContentLength, _ref6, _ref7, _onProgress, _flush, responseData;
|
||||||
return _regeneratorRuntime().wrap(function _callee4$(_context4) {
|
return _regeneratorRuntime().wrap(function _callee4$(_context4) {
|
||||||
while (1) switch (_context4.prev = _context4.next) {
|
while (1) switch (_context4.prev = _context4.next) {
|
||||||
case 0:
|
case 0:
|
||||||
_resolveConfig = resolveConfig(config), url = _resolveConfig.url, method = _resolveConfig.method, data = _resolveConfig.data, signal = _resolveConfig.signal, cancelToken = _resolveConfig.cancelToken, timeout = _resolveConfig.timeout, onDownloadProgress = _resolveConfig.onDownloadProgress, onUploadProgress = _resolveConfig.onUploadProgress, responseType = _resolveConfig.responseType, headers = _resolveConfig.headers, _resolveConfig$withCr = _resolveConfig.withCredentials, withCredentials = _resolveConfig$withCr === void 0 ? 'same-origin' : _resolveConfig$withCr, fetchOptions = _resolveConfig.fetchOptions;
|
_resolveConfig = resolveConfig(config), url = _resolveConfig.url, method = _resolveConfig.method, data = _resolveConfig.data, signal = _resolveConfig.signal, cancelToken = _resolveConfig.cancelToken, timeout = _resolveConfig.timeout, onDownloadProgress = _resolveConfig.onDownloadProgress, onUploadProgress = _resolveConfig.onUploadProgress, responseType = _resolveConfig.responseType, headers = _resolveConfig.headers, _resolveConfig$withCr = _resolveConfig.withCredentials, withCredentials = _resolveConfig$withCr === void 0 ? 'same-origin' : _resolveConfig$withCr, fetchOptions = _resolveConfig.fetchOptions;
|
||||||
|
_fetch = envFetch || fetch;
|
||||||
responseType = responseType ? (responseType + '').toLowerCase() : 'text';
|
responseType = responseType ? (responseType + '').toLowerCase() : 'text';
|
||||||
composedSignal = composeSignals$1([signal, cancelToken && cancelToken.toAbortSignal()], timeout);
|
composedSignal = composeSignals$1([signal, cancelToken && cancelToken.toAbortSignal()], timeout);
|
||||||
request = null;
|
request = null;
|
||||||
unsubscribe = composedSignal && composedSignal.unsubscribe && function () {
|
unsubscribe = composedSignal && composedSignal.unsubscribe && function () {
|
||||||
composedSignal.unsubscribe();
|
composedSignal.unsubscribe();
|
||||||
};
|
};
|
||||||
_context4.prev = 5;
|
_context4.prev = 6;
|
||||||
_context4.t0 = onUploadProgress && supportsRequestStream && method !== 'get' && method !== 'head';
|
_context4.t0 = onUploadProgress && supportsRequestStream && method !== 'get' && method !== 'head';
|
||||||
if (!_context4.t0) {
|
if (!_context4.t0) {
|
||||||
_context4.next = 12;
|
_context4.next = 13;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_context4.next = 10;
|
_context4.next = 11;
|
||||||
return resolveBodyLength(headers, data);
|
return resolveBodyLength(headers, data);
|
||||||
case 10:
|
case 11:
|
||||||
_context4.t1 = requestContentLength = _context4.sent;
|
_context4.t1 = requestContentLength = _context4.sent;
|
||||||
_context4.t0 = _context4.t1 !== 0;
|
_context4.t0 = _context4.t1 !== 0;
|
||||||
case 12:
|
case 13:
|
||||||
if (!_context4.t0) {
|
if (!_context4.t0) {
|
||||||
_context4.next = 16;
|
_context4.next = 17;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_request = new Request(url, {
|
_request = new Request(url, {
|
||||||
@@ -3560,7 +3580,7 @@
|
|||||||
_progressEventDecorat = progressEventDecorator(requestContentLength, progressEventReducer(asyncDecorator(onUploadProgress))), _progressEventDecorat2 = _slicedToArray(_progressEventDecorat, 2), onProgress = _progressEventDecorat2[0], flush = _progressEventDecorat2[1];
|
_progressEventDecorat = progressEventDecorator(requestContentLength, progressEventReducer(asyncDecorator(onUploadProgress))), _progressEventDecorat2 = _slicedToArray(_progressEventDecorat, 2), onProgress = _progressEventDecorat2[0], flush = _progressEventDecorat2[1];
|
||||||
data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);
|
data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);
|
||||||
}
|
}
|
||||||
case 16:
|
case 17:
|
||||||
if (!utils$1.isString(withCredentials)) {
|
if (!utils$1.isString(withCredentials)) {
|
||||||
withCredentials = withCredentials ? 'include' : 'omit';
|
withCredentials = withCredentials ? 'include' : 'omit';
|
||||||
}
|
}
|
||||||
@@ -3577,9 +3597,9 @@
|
|||||||
credentials: isCredentialsSupported ? withCredentials : undefined
|
credentials: isCredentialsSupported ? withCredentials : undefined
|
||||||
});
|
});
|
||||||
request = isRequestSupported && new Request(url, resolvedOptions);
|
request = isRequestSupported && new Request(url, resolvedOptions);
|
||||||
_context4.next = 22;
|
_context4.next = 23;
|
||||||
return isRequestSupported ? fetch(request, fetchOptions) : fetch(url, resolvedOptions);
|
return isRequestSupported ? _fetch(request, fetchOptions) : _fetch(url, resolvedOptions);
|
||||||
case 22:
|
case 23:
|
||||||
response = _context4.sent;
|
response = _context4.sent;
|
||||||
isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response');
|
isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response');
|
||||||
if (supportsResponseStream && (onDownloadProgress || isStreamResponse && unsubscribe)) {
|
if (supportsResponseStream && (onDownloadProgress || isStreamResponse && unsubscribe)) {
|
||||||
@@ -3595,12 +3615,12 @@
|
|||||||
}), options);
|
}), options);
|
||||||
}
|
}
|
||||||
responseType = responseType || 'text';
|
responseType = responseType || 'text';
|
||||||
_context4.next = 28;
|
_context4.next = 29;
|
||||||
return resolvers[utils$1.findKey(resolvers, responseType) || 'text'](response, config);
|
return resolvers[utils$1.findKey(resolvers, responseType) || 'text'](response, config);
|
||||||
case 28:
|
case 29:
|
||||||
responseData = _context4.sent;
|
responseData = _context4.sent;
|
||||||
!isStreamResponse && unsubscribe && unsubscribe();
|
!isStreamResponse && unsubscribe && unsubscribe();
|
||||||
_context4.next = 32;
|
_context4.next = 33;
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
settle(resolve, reject, {
|
settle(resolve, reject, {
|
||||||
data: responseData,
|
data: responseData,
|
||||||
@@ -3611,26 +3631,26 @@
|
|||||||
request: request
|
request: request
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
case 32:
|
case 33:
|
||||||
return _context4.abrupt("return", _context4.sent);
|
return _context4.abrupt("return", _context4.sent);
|
||||||
case 35:
|
case 36:
|
||||||
_context4.prev = 35;
|
_context4.prev = 36;
|
||||||
_context4.t2 = _context4["catch"](5);
|
_context4.t2 = _context4["catch"](6);
|
||||||
unsubscribe && unsubscribe();
|
unsubscribe && unsubscribe();
|
||||||
if (!(_context4.t2 && _context4.t2.name === 'TypeError' && /Load failed|fetch/i.test(_context4.t2.message))) {
|
if (!(_context4.t2 && _context4.t2.name === 'TypeError' && /Load failed|fetch/i.test(_context4.t2.message))) {
|
||||||
_context4.next = 40;
|
_context4.next = 41;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
throw Object.assign(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request), {
|
throw Object.assign(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request), {
|
||||||
cause: _context4.t2.cause || _context4.t2
|
cause: _context4.t2.cause || _context4.t2
|
||||||
});
|
});
|
||||||
case 40:
|
|
||||||
throw AxiosError.from(_context4.t2, _context4.t2 && _context4.t2.code, config, request);
|
|
||||||
case 41:
|
case 41:
|
||||||
|
throw AxiosError.from(_context4.t2, _context4.t2 && _context4.t2.code, config, request);
|
||||||
|
case 42:
|
||||||
case "end":
|
case "end":
|
||||||
return _context4.stop();
|
return _context4.stop();
|
||||||
}
|
}
|
||||||
}, _callee4, null, [[5, 35]]);
|
}, _callee4, null, [[6, 36]]);
|
||||||
}));
|
}));
|
||||||
return function (_x5) {
|
return function (_x5) {
|
||||||
return _ref5.apply(this, arguments);
|
return _ref5.apply(this, arguments);
|
||||||
@@ -3639,9 +3659,7 @@
|
|||||||
};
|
};
|
||||||
var seedCache = new Map();
|
var seedCache = new Map();
|
||||||
var getFetch = function getFetch(config) {
|
var getFetch = function getFetch(config) {
|
||||||
var env = utils$1.merge.call({
|
var env = config && config.env || {};
|
||||||
skipUndefined: true
|
|
||||||
}, globalFetchAPI, config ? config.env : null);
|
|
||||||
var fetch = env.fetch,
|
var fetch = env.fetch,
|
||||||
Request = env.Request,
|
Request = env.Request,
|
||||||
Response = env.Response;
|
Response = env.Response;
|
||||||
@@ -3661,6 +3679,15 @@
|
|||||||
};
|
};
|
||||||
getFetch();
|
getFetch();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known adapters mapping.
|
||||||
|
* Provides environment-specific adapters for Axios:
|
||||||
|
* - `http` for Node.js
|
||||||
|
* - `xhr` for browsers
|
||||||
|
* - `fetch` for fetch API-based requests
|
||||||
|
*
|
||||||
|
* @type {Object<string, Function|Object>}
|
||||||
|
*/
|
||||||
var knownAdapters = {
|
var knownAdapters = {
|
||||||
http: httpAdapter,
|
http: httpAdapter,
|
||||||
xhr: xhrAdapter,
|
xhr: xhrAdapter,
|
||||||
@@ -3668,6 +3695,8 @@
|
|||||||
get: getFetch
|
get: getFetch
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Assign adapter names for easier debugging and identification
|
||||||
utils$1.forEach(knownAdapters, function (fn, value) {
|
utils$1.forEach(knownAdapters, function (fn, value) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
try {
|
try {
|
||||||
@@ -3682,47 +3711,85 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a rejection reason string for unknown or unsupported adapters
|
||||||
|
*
|
||||||
|
* @param {string} reason
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
var renderReason = function renderReason(reason) {
|
var renderReason = function renderReason(reason) {
|
||||||
return "- ".concat(reason);
|
return "- ".concat(reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the adapter is resolved (function, null, or false)
|
||||||
|
*
|
||||||
|
* @param {Function|null|false} adapter
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
var isResolvedHandle = function isResolvedHandle(adapter) {
|
var isResolvedHandle = function isResolvedHandle(adapter) {
|
||||||
return utils$1.isFunction(adapter) || adapter === null || adapter === false;
|
return utils$1.isFunction(adapter) || adapter === null || adapter === false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first suitable adapter from the provided list.
|
||||||
|
* Tries each adapter in order until a supported one is found.
|
||||||
|
* Throws an AxiosError if no adapter is suitable.
|
||||||
|
*
|
||||||
|
* @param {Array<string|Function>|string|Function} adapters - Adapter(s) by name or function.
|
||||||
|
* @param {Object} config - Axios request configuration
|
||||||
|
* @throws {AxiosError} If no suitable adapter is available
|
||||||
|
* @returns {Function} The resolved adapter function
|
||||||
|
*/
|
||||||
|
function getAdapter(adapters, config) {
|
||||||
|
adapters = utils$1.isArray(adapters) ? adapters : [adapters];
|
||||||
|
var _adapters = adapters,
|
||||||
|
length = _adapters.length;
|
||||||
|
var nameOrAdapter;
|
||||||
|
var adapter;
|
||||||
|
var rejectedReasons = {};
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
nameOrAdapter = adapters[i];
|
||||||
|
var id = void 0;
|
||||||
|
adapter = nameOrAdapter;
|
||||||
|
if (!isResolvedHandle(nameOrAdapter)) {
|
||||||
|
adapter = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()];
|
||||||
|
if (adapter === undefined) {
|
||||||
|
throw new AxiosError("Unknown adapter '".concat(id, "'"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (adapter && (utils$1.isFunction(adapter) || (adapter = adapter.get(config)))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
rejectedReasons[id || '#' + i] = adapter;
|
||||||
|
}
|
||||||
|
if (!adapter) {
|
||||||
|
var reasons = Object.entries(rejectedReasons).map(function (_ref) {
|
||||||
|
var _ref2 = _slicedToArray(_ref, 2),
|
||||||
|
id = _ref2[0],
|
||||||
|
state = _ref2[1];
|
||||||
|
return "adapter ".concat(id, " ") + (state === false ? 'is not supported by the environment' : 'is not available in the build');
|
||||||
|
});
|
||||||
|
var s = length ? reasons.length > 1 ? 'since :\n' + reasons.map(renderReason).join('\n') : ' ' + renderReason(reasons[0]) : 'as no adapter specified';
|
||||||
|
throw new AxiosError("There is no suitable adapter to dispatch the request " + s, 'ERR_NOT_SUPPORT');
|
||||||
|
}
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports Axios adapters and utility to resolve an adapter
|
||||||
|
*/
|
||||||
var adapters = {
|
var adapters = {
|
||||||
getAdapter: function getAdapter(adapters, config) {
|
/**
|
||||||
adapters = utils$1.isArray(adapters) ? adapters : [adapters];
|
* Resolve an adapter from a list of adapter names or functions.
|
||||||
var _adapters = adapters,
|
* @type {Function}
|
||||||
length = _adapters.length;
|
*/
|
||||||
var nameOrAdapter;
|
getAdapter: getAdapter,
|
||||||
var adapter;
|
/**
|
||||||
var rejectedReasons = {};
|
* Exposes all known adapters
|
||||||
for (var i = 0; i < length; i++) {
|
* @type {Object<string, Function|Object>}
|
||||||
nameOrAdapter = adapters[i];
|
*/
|
||||||
var id = void 0;
|
|
||||||
adapter = nameOrAdapter;
|
|
||||||
if (!isResolvedHandle(nameOrAdapter)) {
|
|
||||||
adapter = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()];
|
|
||||||
if (adapter === undefined) {
|
|
||||||
throw new AxiosError("Unknown adapter '".concat(id, "'"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (adapter && (utils$1.isFunction(adapter) || (adapter = adapter.get(config)))) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
rejectedReasons[id || '#' + i] = adapter;
|
|
||||||
}
|
|
||||||
if (!adapter) {
|
|
||||||
var reasons = Object.entries(rejectedReasons).map(function (_ref) {
|
|
||||||
var _ref2 = _slicedToArray(_ref, 2),
|
|
||||||
id = _ref2[0],
|
|
||||||
state = _ref2[1];
|
|
||||||
return "adapter ".concat(id, " ") + (state === false ? 'is not supported by the environment' : 'is not available in the build');
|
|
||||||
});
|
|
||||||
var s = length ? reasons.length > 1 ? 'since :\n' + reasons.map(renderReason).join('\n') : ' ' + renderReason(reasons[0]) : 'as no adapter specified';
|
|
||||||
throw new AxiosError("There is no suitable adapter to dispatch the request " + s, 'ERR_NOT_SUPPORT');
|
|
||||||
}
|
|
||||||
return adapter;
|
|
||||||
},
|
|
||||||
adapters: knownAdapters
|
adapters: knownAdapters
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3780,7 +3847,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var VERSION = "1.12.0";
|
var VERSION = "1.13.2";
|
||||||
|
|
||||||
var validators$1 = {};
|
var validators$1 = {};
|
||||||
|
|
||||||
@@ -4023,7 +4090,6 @@
|
|||||||
}
|
}
|
||||||
len = requestInterceptorChain.length;
|
len = requestInterceptorChain.length;
|
||||||
var newConfig = config;
|
var newConfig = config;
|
||||||
i = 0;
|
|
||||||
while (i < len) {
|
while (i < len) {
|
||||||
var onFulfilled = requestInterceptorChain[i++];
|
var onFulfilled = requestInterceptorChain[i++];
|
||||||
var onRejected = requestInterceptorChain[i++];
|
var onRejected = requestInterceptorChain[i++];
|
||||||
@@ -4317,7 +4383,13 @@
|
|||||||
InsufficientStorage: 507,
|
InsufficientStorage: 507,
|
||||||
LoopDetected: 508,
|
LoopDetected: 508,
|
||||||
NotExtended: 510,
|
NotExtended: 510,
|
||||||
NetworkAuthenticationRequired: 511
|
NetworkAuthenticationRequired: 511,
|
||||||
|
WebServerIsDown: 521,
|
||||||
|
ConnectionTimedOut: 522,
|
||||||
|
OriginIsUnreachable: 523,
|
||||||
|
TimeoutOccurred: 524,
|
||||||
|
SslHandshakeFailed: 525,
|
||||||
|
InvalidSslCertificate: 526
|
||||||
};
|
};
|
||||||
Object.entries(HttpStatusCode).forEach(function (_ref) {
|
Object.entries(HttpStatusCode).forEach(function (_ref) {
|
||||||
var _ref2 = _slicedToArray(_ref, 2),
|
var _ref2 = _slicedToArray(_ref, 2),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user