Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0da683e549 |
@@ -22,12 +22,10 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh release create "$tag" --generate-notes --draft
|
||||
upload_url=$(gh release view "$tag" --repo "${{ github.repository }}" --json uploadUrl -q ".uploadUrl")
|
||||
upload_url=$(gh release create "$tag" --generate-notes --draft --json upload_url -q '.upload_url')
|
||||
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
|
||||
|
||||
docker:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
@@ -37,7 +35,6 @@ jobs:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
docker-latest:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
@@ -47,7 +44,6 @@ jobs:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
pypi:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Install dependencies for building secp256k1
|
||||
|
||||
@@ -23,7 +23,7 @@ repos:
|
||||
- id: ruff
|
||||
args: [ --fix, --exit-non-zero-on-fix ]
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.7.4
|
||||
rev: v3.6.2
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [css, javascript, html, json]
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
||||
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
||||
|
||||
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
||||
|
||||
@@ -78,11 +77,13 @@ As well as working great in a browser, LNbits has native IoS and Android apps as
|
||||
|
||||
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
|
||||
|
||||
If you like this project [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/) [](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||
[](https://extensions.lnbits.com/)
|
||||
|
||||
[docs]: https://github.com/lnbits/lnbits/wiki
|
||||
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from .core.services import create_invoice, pay_invoice
|
||||
from .decorators import (
|
||||
check_account_exists,
|
||||
check_admin,
|
||||
check_super_user,
|
||||
check_user_exists,
|
||||
@@ -12,7 +11,6 @@ from .exceptions import InvoiceError, PaymentError
|
||||
__all__ = [
|
||||
"InvoiceError",
|
||||
"PaymentError",
|
||||
"check_account_exists",
|
||||
"check_admin",
|
||||
"check_super_user",
|
||||
"check_user_exists",
|
||||
|
||||
@@ -33,9 +33,9 @@ async def get_payment(checking_id: str, conn: Connection | None = None) -> Payme
|
||||
|
||||
async def get_standalone_payment(
|
||||
checking_id_or_hash: str,
|
||||
conn: Connection | None = None,
|
||||
incoming: bool | None = False,
|
||||
wallet_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment | None:
|
||||
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
||||
values = {
|
||||
@@ -46,7 +46,7 @@ async def get_standalone_payment(
|
||||
clause = f"({clause}) AND amount > 0"
|
||||
|
||||
if wallet_id:
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return None
|
||||
values["wallet_id"] = wallet.source_wallet_id
|
||||
@@ -69,7 +69,7 @@ async def get_standalone_payment(
|
||||
async def get_wallet_payment(
|
||||
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
||||
) -> Payment | None:
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return None
|
||||
payment = await (conn or db).fetchone(
|
||||
@@ -124,6 +124,7 @@ async def get_payments_paginated( # noqa: C901
|
||||
Filters payments to be returned by:
|
||||
- complete | pending | failed | outgoing | incoming.
|
||||
"""
|
||||
|
||||
values: dict[str, Any] = {
|
||||
"time": since,
|
||||
}
|
||||
@@ -133,7 +134,7 @@ async def get_payments_paginated( # noqa: C901
|
||||
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
||||
|
||||
if wallet_id:
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return Page(data=[], total=0)
|
||||
|
||||
@@ -325,7 +326,6 @@ async def get_payments_history(
|
||||
wallet_id: str | None = None,
|
||||
group: DateTrunc = "day",
|
||||
filters: Filters | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[PaymentHistoryPoint]:
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
@@ -361,13 +361,13 @@ async def get_payments_history(
|
||||
filters.values(values),
|
||||
)
|
||||
if wallet_id:
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return []
|
||||
balance = wallet.balance_msat
|
||||
values["wallet_id"] = wallet.source_wallet_id
|
||||
else:
|
||||
balance = await get_total_balance(conn=conn)
|
||||
balance = await get_total_balance()
|
||||
|
||||
# since we dont know the balance at the starting point,
|
||||
# we take the current balance and walk backwards
|
||||
|
||||
+11
-20
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||
from lnbits.core.crud.wallets import create_wallet, get_wallets
|
||||
from lnbits.core.crud.wallets import get_wallets
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import UserAcls
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
@@ -30,9 +30,9 @@ async def create_account(
|
||||
return account
|
||||
|
||||
|
||||
async def update_account(account: Account, conn: Connection | None = None) -> Account:
|
||||
async def update_account(account: Account) -> Account:
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
await (conn or db).update("accounts", account)
|
||||
await db.update("accounts", account)
|
||||
return account
|
||||
|
||||
|
||||
@@ -171,24 +171,17 @@ async def get_account_by_username_or_email(
|
||||
|
||||
|
||||
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
account = await get_account(user_id, conn=conn)
|
||||
if not account:
|
||||
return None
|
||||
return await get_user_from_account(account, conn=conn)
|
||||
account = await get_account(user_id, conn)
|
||||
if not account:
|
||||
return None
|
||||
return await get_user_from_account(account, conn)
|
||||
|
||||
|
||||
async def get_user_from_account(
|
||||
account: Account, conn: Connection | None = None
|
||||
) -> User | None:
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
extensions = await get_user_active_extensions_ids(account.id, conn=conn)
|
||||
wallets = await get_wallets(account.id, deleted=False, conn=conn)
|
||||
|
||||
if len(wallets) == 0:
|
||||
wallet = await create_wallet(user_id=account.id, conn=conn)
|
||||
wallets.append(wallet)
|
||||
|
||||
extensions = await get_user_active_extensions_ids(account.id, conn)
|
||||
wallets = await get_wallets(account.id, False, conn=conn)
|
||||
return User(
|
||||
id=account.id,
|
||||
email=account.email,
|
||||
@@ -207,11 +200,9 @@ async def get_user_from_account(
|
||||
)
|
||||
|
||||
|
||||
async def update_user_access_control_list(
|
||||
user_acls: UserAcls, conn: Connection | None = None
|
||||
):
|
||||
async def update_user_access_control_list(user_acls: UserAcls):
|
||||
user_acls.updated_at = datetime.now(timezone.utc)
|
||||
await (conn or db).update("accounts", user_acls)
|
||||
await db.update("accounts", user_acls)
|
||||
|
||||
|
||||
async def get_user_access_control_lists(
|
||||
|
||||
@@ -3,10 +3,9 @@ from time import time
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.wallets import BaseWallet, WalletsFilters, WalletType
|
||||
from lnbits.core.models.wallets import WalletsFilters, WalletType
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
from ..models import Wallet
|
||||
|
||||
@@ -52,11 +51,6 @@ async def delete_wallet(
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
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(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
@@ -246,24 +240,6 @@ async def get_wallet_for_key(
|
||||
return wallet
|
||||
|
||||
|
||||
async def get_base_wallet_for_key(
|
||||
key: str,
|
||||
conn: Connection | None = None,
|
||||
) -> BaseWallet | None:
|
||||
wallet = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
|
||||
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
||||
""",
|
||||
{"key": key},
|
||||
BaseWallet,
|
||||
)
|
||||
if not wallet:
|
||||
return None
|
||||
|
||||
return wallet
|
||||
|
||||
|
||||
async def get_source_wallet(
|
||||
wallet: Wallet, conn: Connection | None = None
|
||||
) -> Wallet | None:
|
||||
|
||||
@@ -786,65 +786,3 @@ async def m038_add_labels_for_payments(db: Connection):
|
||||
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,
|
||||
UserExtra,
|
||||
)
|
||||
from .wallets import CreateWallet, KeyType, Wallet, WalletInfo, WalletTypeInfo
|
||||
from .wallets import BaseWallet, CreateWallet, KeyType, Wallet, WalletTypeInfo
|
||||
from .webpush import CreateWebPushSubscription, WebPushSubscription
|
||||
|
||||
__all__ = [
|
||||
@@ -57,6 +57,7 @@ __all__ = [
|
||||
"AuditEntry",
|
||||
"AuditFilters",
|
||||
"BalanceDelta",
|
||||
"BaseWallet",
|
||||
"Callback",
|
||||
"CancelInvoice",
|
||||
"ConversionData",
|
||||
@@ -98,7 +99,6 @@ __all__ = [
|
||||
"UserAcls",
|
||||
"UserExtra",
|
||||
"Wallet",
|
||||
"WalletInfo",
|
||||
"WalletTypeInfo",
|
||||
"WebPushSubscription",
|
||||
]
|
||||
|
||||
@@ -6,7 +6,6 @@ import json
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
from asyncio.tasks import create_task
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -21,7 +20,6 @@ from lnbits.helpers import (
|
||||
version_parse,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
|
||||
class ExplicitRelease(BaseModel):
|
||||
@@ -608,37 +606,6 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
@classmethod
|
||||
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,
|
||||
) -> list[InstallableExtension]:
|
||||
extension_list: list[InstallableExtension] = []
|
||||
|
||||
@@ -196,15 +196,7 @@ class PaymentFilters(FilterModel):
|
||||
"labels",
|
||||
]
|
||||
|
||||
__sort_fields__ = [
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"amount",
|
||||
"fee",
|
||||
"memo",
|
||||
"time",
|
||||
"tag",
|
||||
]
|
||||
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
||||
|
||||
status: str | None
|
||||
tag: str | None
|
||||
|
||||
@@ -172,15 +172,8 @@ class UserAcls(BaseModel):
|
||||
return None
|
||||
|
||||
|
||||
class AccountId(BaseModel):
|
||||
class Account(BaseModel):
|
||||
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
|
||||
username: str | None = None
|
||||
password_hash: str | None = None
|
||||
@@ -201,10 +194,6 @@ class Account(AccountId):
|
||||
self.is_admin = settings.is_admin_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:
|
||||
"""sets and returns the hashed password"""
|
||||
salt = gensalt()
|
||||
@@ -248,7 +237,7 @@ class AccountOverview(Account):
|
||||
|
||||
class AccountFilters(FilterModel):
|
||||
__search_fields__ = [
|
||||
"id",
|
||||
"user",
|
||||
"email",
|
||||
"username",
|
||||
"pubkey",
|
||||
@@ -256,18 +245,17 @@ class AccountFilters(FilterModel):
|
||||
"wallet_id",
|
||||
]
|
||||
__sort_fields__ = [
|
||||
"id",
|
||||
"balance_msat",
|
||||
"email",
|
||||
"username",
|
||||
"pubkey",
|
||||
"external_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"transaction_count",
|
||||
"wallet_count",
|
||||
"last_payment",
|
||||
]
|
||||
|
||||
id: str | None = None
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
user: str | None = None
|
||||
username: str | None = None
|
||||
pubkey: str | None = None
|
||||
external_id: str | None = None
|
||||
wallet_id: str | None = None
|
||||
|
||||
@@ -11,7 +11,7 @@ from lnbits.db import FilterModel
|
||||
from lnbits.settings import settings
|
||||
|
||||
|
||||
class WalletInfo(BaseModel):
|
||||
class BaseWallet(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
adminkey: str
|
||||
@@ -110,16 +110,13 @@ class WalletExtra(BaseModel):
|
||||
]
|
||||
|
||||
|
||||
class BaseWallet(BaseModel):
|
||||
class Wallet(BaseModel):
|
||||
id: str
|
||||
user: str
|
||||
wallet_type: str = WalletType.LIGHTNING.value
|
||||
name: str
|
||||
adminkey: str
|
||||
inkey: str
|
||||
|
||||
|
||||
class Wallet(BaseWallet):
|
||||
name: str
|
||||
wallet_type: str = WalletType.LIGHTNING.value
|
||||
# Must be set only for shared wallets
|
||||
shared_wallet_id: str | None = None
|
||||
deleted: bool = False
|
||||
@@ -233,12 +230,6 @@ class WalletTypeInfo:
|
||||
wallet: Wallet
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseWalletTypeInfo:
|
||||
key_type: KeyType
|
||||
wallet: BaseWallet
|
||||
|
||||
|
||||
class WalletsFilters(FilterModel):
|
||||
__search_fields__ = ["id", "name", "currency"]
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ from lnbits.core.crud.extensions import (
|
||||
update_installed_extension,
|
||||
)
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.db import Connection
|
||||
from lnbits.settings import settings
|
||||
|
||||
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
||||
@@ -150,9 +149,9 @@ async def start_extension_background_work(ext_id: str) -> bool:
|
||||
|
||||
|
||||
async def get_valid_extensions(
|
||||
include_deactivated: bool | None = True, conn: Connection | None = None
|
||||
include_deactivated: bool | None = True,
|
||||
) -> list[Extension]:
|
||||
installed_extensions = await get_installed_extensions(conn=conn)
|
||||
installed_extensions = await get_installed_extensions()
|
||||
valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions]
|
||||
|
||||
if include_deactivated:
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.crud import get_wallet
|
||||
@@ -72,63 +70,6 @@ def check_stripe_signature(
|
||||
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:
|
||||
"""
|
||||
Test the connection to Stripe by checking if the API key is valid.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import smtplib
|
||||
from asyncio.tasks import create_task
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from http import HTTPStatus
|
||||
@@ -287,13 +286,6 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||
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):
|
||||
# TODO: websocket message should be a clean payment model
|
||||
# await websocket_manager.send(wallet.inkey, payment.json())
|
||||
|
||||
@@ -47,7 +47,7 @@ from ..models import (
|
||||
PaymentState,
|
||||
Wallet,
|
||||
)
|
||||
from .notifications import send_payment_notification_in_background
|
||||
from .notifications import send_payment_notification
|
||||
|
||||
payment_lock = asyncio.Lock()
|
||||
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
||||
@@ -67,14 +67,12 @@ async def pay_invoice(
|
||||
if settings.lnbits_only_allow_incoming_payments:
|
||||
raise PaymentError("Only incoming payments allowed.", status="failed")
|
||||
invoice = _validate_payment_request(payment_request, max_sat)
|
||||
|
||||
if not invoice.amount_msat:
|
||||
raise ValueError("Missig invoice amount.")
|
||||
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||
amount_msat = invoice.amount_msat
|
||||
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
|
||||
|
||||
if not wallet.can_send_payments:
|
||||
raise PaymentError(
|
||||
"Wallet does not have permission to pay invoices.",
|
||||
@@ -97,12 +95,10 @@ async def pay_invoice(
|
||||
labels=labels,
|
||||
)
|
||||
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||
payment = await _pay_invoice(
|
||||
wallet.source_wallet_id, create_payment_model, conn=new_conn
|
||||
)
|
||||
payment = await _pay_invoice(wallet.source_wallet_id, create_payment_model, conn)
|
||||
|
||||
await _credit_service_fee_wallet(wallet, payment, conn=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)
|
||||
|
||||
return payment
|
||||
|
||||
@@ -201,22 +197,20 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
||||
# do not save memo if description_hash or unhashed_description is set
|
||||
memo = ""
|
||||
|
||||
async with db.connect() as conn:
|
||||
payment = await create_invoice(
|
||||
wallet_id=wallet_id,
|
||||
amount=data.amount,
|
||||
memo=memo,
|
||||
currency=data.unit,
|
||||
description_hash=description_hash,
|
||||
unhashed_description=unhashed_description,
|
||||
expiry=data.expiry,
|
||||
extra=data.extra,
|
||||
webhook=data.webhook,
|
||||
internal=data.internal,
|
||||
payment_hash=data.payment_hash,
|
||||
labels=data.labels,
|
||||
conn=conn,
|
||||
)
|
||||
payment = await create_invoice(
|
||||
wallet_id=wallet_id,
|
||||
amount=data.amount,
|
||||
memo=memo,
|
||||
currency=data.unit,
|
||||
description_hash=description_hash,
|
||||
unhashed_description=unhashed_description,
|
||||
expiry=data.expiry,
|
||||
extra=data.extra,
|
||||
webhook=data.webhook,
|
||||
internal=data.internal,
|
||||
payment_hash=data.payment_hash,
|
||||
labels=data.labels,
|
||||
)
|
||||
|
||||
if data.lnurl_withdraw:
|
||||
try:
|
||||
@@ -361,15 +355,13 @@ async def update_pending_payments(wallet_id: str):
|
||||
await update_pending_payment(payment)
|
||||
|
||||
|
||||
async def update_pending_payment(
|
||||
payment: Payment, conn: Connection | None = None
|
||||
) -> Payment:
|
||||
async def update_pending_payment(payment: Payment) -> Payment:
|
||||
status = await payment.check_status()
|
||||
if status.failed:
|
||||
payment.status = PaymentState.FAILED
|
||||
await update_payment(payment, conn=conn)
|
||||
await update_payment(payment)
|
||||
elif status.success:
|
||||
payment = await update_payment_success_status(payment, status, conn=conn)
|
||||
payment = await update_payment_success_status(payment, status)
|
||||
return payment
|
||||
|
||||
|
||||
@@ -706,7 +698,6 @@ async def _pay_internal_invoice(
|
||||
internal_payment = await check_internal(
|
||||
create_payment_model.payment_hash, conn=conn
|
||||
)
|
||||
|
||||
if not internal_payment:
|
||||
return None
|
||||
|
||||
@@ -715,7 +706,6 @@ async def _pay_internal_invoice(
|
||||
internal_invoice = await get_standalone_payment(
|
||||
internal_payment.checking_id, incoming=True, conn=conn
|
||||
)
|
||||
|
||||
if not internal_invoice:
|
||||
raise PaymentError("Internal payment not found.", status="failed")
|
||||
|
||||
@@ -737,7 +727,6 @@ async def _pay_internal_invoice(
|
||||
|
||||
internal_id = f"internal_{create_payment_model.payment_hash}"
|
||||
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
||||
|
||||
payment = await create_payment(
|
||||
checking_id=internal_id,
|
||||
data=create_payment_model,
|
||||
@@ -752,7 +741,7 @@ async def _pay_internal_invoice(
|
||||
await update_payment(internal_payment, conn=conn)
|
||||
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
||||
|
||||
send_payment_notification_in_background(wallet, payment)
|
||||
await send_payment_notification(wallet, payment)
|
||||
|
||||
# notify receiver asynchronously
|
||||
from lnbits.tasks import internal_invoice_queue
|
||||
@@ -825,7 +814,7 @@ async def _pay_external_invoice(
|
||||
payment = await update_payment_success_status(
|
||||
payment, payment_response, conn=conn
|
||||
)
|
||||
send_payment_notification_in_background(wallet, payment)
|
||||
await send_payment_notification(wallet, payment)
|
||||
logger.success(f"payment successful {payment_response.checking_id}")
|
||||
|
||||
payment.checking_id = payment_response.checking_id
|
||||
|
||||
@@ -3,9 +3,7 @@ from uuid import uuid4
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.extensions import UserExtension
|
||||
from lnbits.db import Connection
|
||||
from lnbits.settings import (
|
||||
EditableSettings,
|
||||
SuperSettings,
|
||||
@@ -50,43 +48,37 @@ async def create_user_account_no_ckeck(
|
||||
account: Account | None = None,
|
||||
wallet_name: str | None = None,
|
||||
default_exts: list[str] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> 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.email and await get_account_by_email(account.email, conn=conn):
|
||||
raise ValueError("Email already exists.")
|
||||
if account:
|
||||
account.validate_fields()
|
||||
if account.username and await get_account_by_username(account.username):
|
||||
raise ValueError("Username already exists.")
|
||||
|
||||
if account.pubkey and await get_account_by_pubkey(
|
||||
account.pubkey, conn=conn
|
||||
):
|
||||
raise ValueError("Pubkey already exists.")
|
||||
if account.email and await get_account_by_email(account.email):
|
||||
raise ValueError("Email already exists.")
|
||||
|
||||
if not account.id:
|
||||
account.id = uuid4().hex
|
||||
if account.pubkey and await get_account_by_pubkey(account.pubkey):
|
||||
raise ValueError("Pubkey already exists.")
|
||||
|
||||
account = await create_account(account, conn=conn)
|
||||
await create_wallet(
|
||||
user_id=account.id,
|
||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||
conn=conn,
|
||||
)
|
||||
if not account.id:
|
||||
account.id = uuid4().hex
|
||||
|
||||
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
|
||||
for ext_id in user_extensions:
|
||||
try:
|
||||
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}")
|
||||
account = await create_account(account)
|
||||
await create_wallet(
|
||||
user_id=account.id,
|
||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||
)
|
||||
|
||||
user = await get_user_from_account(account, conn=conn)
|
||||
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
|
||||
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:
|
||||
raise ValueError("Cannot find user for account.")
|
||||
|
||||
|
||||
@@ -57,9 +57,7 @@ async def run_by_the_minute_tasks() -> None:
|
||||
if minute_counter % 60 == 0:
|
||||
try:
|
||||
# initialize the list of all extensions
|
||||
await InstallableExtension.get_installable_extensions(
|
||||
post_refresh_cache=True
|
||||
)
|
||||
await InstallableExtension.get_installable_extensions()
|
||||
except Exception as ex:
|
||||
logger.error(ex)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ from urllib.parse import urlparse
|
||||
from fastapi import APIRouter, Depends, File
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.models.notifications import NotificationType
|
||||
from lnbits.core.models.users import Account
|
||||
from lnbits.core.services import (
|
||||
enqueue_admin_notification,
|
||||
get_balance_delta,
|
||||
@@ -67,9 +67,9 @@ async def api_test_email():
|
||||
|
||||
@admin_router.get("/api/v1/settings")
|
||||
async def api_get_settings(
|
||||
account: Account = Depends(check_admin),
|
||||
user: User = Depends(check_admin),
|
||||
) -> AdminSettings | None:
|
||||
admin_settings = await get_admin_settings(account.is_super_user)
|
||||
admin_settings = await get_admin_settings(user.super_user)
|
||||
return admin_settings
|
||||
|
||||
|
||||
@@ -77,14 +77,12 @@ async def api_get_settings(
|
||||
"/api/v1/settings",
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
async def api_update_settings(
|
||||
data: UpdateSettings, account: Account = Depends(check_admin)
|
||||
):
|
||||
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
|
||||
enqueue_admin_notification(
|
||||
NotificationType.settings_update, {"username": account.username}
|
||||
NotificationType.settings_update, {"username": user.username}
|
||||
)
|
||||
await update_admin_settings(data)
|
||||
admin_settings = await get_admin_settings(account.is_super_user)
|
||||
admin_settings = await get_admin_settings(user.super_user)
|
||||
if not admin_settings:
|
||||
raise ValueError("Updated admin settings not found.")
|
||||
update_cached_settings(admin_settings.dict())
|
||||
@@ -96,11 +94,9 @@ async def api_update_settings(
|
||||
"/api/v1/settings",
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
async def api_update_settings_partial(
|
||||
data: dict, account: Account = Depends(check_admin)
|
||||
):
|
||||
async def api_update_settings_partial(data: dict, user: User = Depends(check_admin)):
|
||||
updatable_settings = dict_to_settings({**settings.dict(), **data})
|
||||
return await api_update_settings(updatable_settings, account)
|
||||
return await api_update_settings(updatable_settings, user)
|
||||
|
||||
|
||||
@admin_router.get(
|
||||
@@ -114,9 +110,9 @@ async def api_reset_settings(field_name: str):
|
||||
|
||||
|
||||
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
||||
async def api_delete_settings(account: Account = Depends(check_super_user)) -> None:
|
||||
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
||||
enqueue_admin_notification(
|
||||
NotificationType.settings_update, {"username": account.username}
|
||||
NotificationType.settings_update, {"username": user.username}
|
||||
)
|
||||
await reset_core_settings()
|
||||
server_restart.set()
|
||||
|
||||
@@ -8,17 +8,13 @@ from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from lnbits.core.models import (
|
||||
BaseWallet,
|
||||
ConversionData,
|
||||
CreateWallet,
|
||||
User,
|
||||
Wallet,
|
||||
)
|
||||
from lnbits.core.models.users import AccountId
|
||||
from lnbits.decorators import (
|
||||
check_account_exists,
|
||||
check_account_id_exists,
|
||||
check_user_exists,
|
||||
)
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.exchange_rates import (
|
||||
allowed_currencies,
|
||||
@@ -43,9 +39,7 @@ async def health() -> dict:
|
||||
|
||||
|
||||
@api_router.get("/api/v1/status", status_code=HTTPStatus.OK)
|
||||
async def health_check(
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
) -> dict:
|
||||
async def health_check(user: User = Depends(check_user_exists)) -> dict:
|
||||
stat: dict[str, Any] = {
|
||||
"server_time": int(time()),
|
||||
"up_time": settings.lnbits_server_up_time,
|
||||
@@ -53,7 +47,7 @@ async def health_check(
|
||||
}
|
||||
|
||||
stat["version"] = settings.version
|
||||
if not account_id.is_admin_id:
|
||||
if not user.admin:
|
||||
return stat
|
||||
|
||||
funding_source = get_funding_source()
|
||||
@@ -70,6 +64,7 @@ async def health_check(
|
||||
"/api/v1/wallets",
|
||||
name="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]:
|
||||
return user.wallets
|
||||
@@ -83,7 +78,7 @@ async def api_create_account(data: CreateWallet) -> Wallet:
|
||||
|
||||
@api_router.get(
|
||||
"/api/v1/rate/history",
|
||||
dependencies=[Depends(check_account_exists)],
|
||||
dependencies=[Depends(check_user_exists)],
|
||||
)
|
||||
async def api_exchange_rate_history() -> list[dict]:
|
||||
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.misc import SimpleStatus
|
||||
from lnbits.core.models.users import AccountId
|
||||
from lnbits.core.models.users import User
|
||||
from lnbits.core.services.assets import create_user_asset
|
||||
from lnbits.db import Filters, Page
|
||||
from lnbits.decorators import (
|
||||
check_account_id_exists,
|
||||
check_user_exists,
|
||||
optional_user_id,
|
||||
parse_filters,
|
||||
)
|
||||
@@ -35,10 +35,10 @@ upload_file_param = File(...)
|
||||
summary="Get paginated list user assets",
|
||||
)
|
||||
async def api_get_user_assets(
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
filters: Filters = Depends(parse_filters(AssetFilters)),
|
||||
) -> Page[AssetInfo]:
|
||||
return await get_user_assets(account_id.id, filters=filters)
|
||||
return await get_user_assets(user.id, filters=filters)
|
||||
|
||||
|
||||
@asset_router.get(
|
||||
@@ -48,9 +48,9 @@ async def api_get_user_assets(
|
||||
)
|
||||
async def api_get_asset(
|
||||
asset_id: str,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> AssetInfo:
|
||||
asset_info = await get_user_asset_info(account_id.id, asset_id)
|
||||
asset_info = await get_user_asset_info(user.id, asset_id)
|
||||
if not asset_info:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||
return asset_info
|
||||
@@ -120,12 +120,12 @@ async def api_get_asset_thumbnail(
|
||||
async def api_update_asset(
|
||||
asset_id: str,
|
||||
data: AssetUpdate,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> AssetInfo:
|
||||
if account_id.is_admin_id:
|
||||
if user.admin:
|
||||
asset_info = await get_asset_info(asset_id)
|
||||
else:
|
||||
asset_info = await get_user_asset_info(account_id.id, asset_id)
|
||||
asset_info = await get_user_asset_info(user.id, asset_id)
|
||||
|
||||
if not asset_info:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||
@@ -144,13 +144,13 @@ async def api_update_asset(
|
||||
summary="Upload user assets",
|
||||
)
|
||||
async def api_upload_asset(
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
file: UploadFile = upload_file_param,
|
||||
public_asset: bool = False,
|
||||
) -> AssetInfo:
|
||||
asset = await create_user_asset(account_id.id, file, public_asset)
|
||||
asset = await create_user_asset(user.id, file, public_asset)
|
||||
|
||||
asset_info = await get_user_asset_info(account_id.id, asset.id)
|
||||
asset_info = await get_user_asset_info(user.id, asset.id)
|
||||
if not asset_info:
|
||||
raise ValueError("Failed to retrieve asset info after upload.")
|
||||
|
||||
@@ -164,11 +164,11 @@ async def api_upload_asset(
|
||||
)
|
||||
async def api_delete_asset(
|
||||
asset_id: str,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> SimpleStatus:
|
||||
asset = await get_user_asset(account_id.id, asset_id)
|
||||
asset = await get_user_asset(user.id, asset_id)
|
||||
if not asset:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||
|
||||
await delete_user_asset(account_id.id, asset_id)
|
||||
await delete_user_asset(user.id, asset_id)
|
||||
return SimpleStatus(success=True, message="Asset deleted successfully.")
|
||||
|
||||
@@ -26,11 +26,7 @@ from lnbits.core.models.users import (
|
||||
)
|
||||
from lnbits.core.services import create_user_account
|
||||
from lnbits.core.services.users import update_user_account
|
||||
from lnbits.decorators import (
|
||||
access_token_payload,
|
||||
check_account_exists,
|
||||
check_user_exists,
|
||||
)
|
||||
from lnbits.decorators import access_token_payload, check_user_exists
|
||||
from lnbits.helpers import (
|
||||
create_access_token,
|
||||
decrypt_internal_message,
|
||||
@@ -123,11 +119,11 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
||||
@auth_router.get("/acl")
|
||||
async def api_get_user_acls(
|
||||
request: Request,
|
||||
account: Account = Depends(check_account_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> UserAcls:
|
||||
api_routes = get_api_routes(request.app.router.routes)
|
||||
|
||||
acls = await get_user_access_control_lists(account.id)
|
||||
acls = await get_user_access_control_lists(user.id)
|
||||
|
||||
# Add missing/new endpoints to the ACLs
|
||||
for acl in acls.access_control_list:
|
||||
@@ -140,7 +136,7 @@ async def api_get_user_acls(
|
||||
acl.endpoints.append(EndpointAccess(path=path, name=name))
|
||||
acl.endpoints.sort(key=lambda e: e.name.lower())
|
||||
|
||||
return UserAcls(id=account.id, access_control_list=acls.access_control_list)
|
||||
return UserAcls(id=user.id, access_control_list=acls.access_control_list)
|
||||
|
||||
|
||||
@auth_router.put("/acl")
|
||||
@@ -148,13 +144,13 @@ async def api_get_user_acls(
|
||||
async def api_update_user_acl(
|
||||
request: Request,
|
||||
data: UpdateAccessControlList,
|
||||
account: Account = Depends(check_account_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> UserAcls:
|
||||
|
||||
account = await get_account(user.id)
|
||||
if not account or not account.verify_password(data.password):
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||
|
||||
user_acls = await get_user_access_control_lists(account.id)
|
||||
user_acls = await get_user_access_control_lists(user.id)
|
||||
acl = user_acls.get_acl_by_id(data.id)
|
||||
if acl:
|
||||
user_acls.access_control_list.remove(acl)
|
||||
@@ -179,30 +175,33 @@ async def api_update_user_acl(
|
||||
|
||||
@auth_router.delete("/acl")
|
||||
async def api_delete_user_acl(
|
||||
data: DeleteAccessControlList, account: Account = Depends(check_account_exists)
|
||||
data: DeleteAccessControlList,
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
account = await get_account(user.id)
|
||||
if not account or not account.verify_password(data.password):
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||
|
||||
user_acls = await get_user_access_control_lists(account.id)
|
||||
user_acls = await get_user_access_control_lists(user.id)
|
||||
user_acls.delete_acl_by_id(data.id)
|
||||
await update_user_access_control_list(user_acls)
|
||||
|
||||
|
||||
@auth_router.post("/acl/token")
|
||||
async def api_create_user_api_token(
|
||||
data: ApiTokenRequest, account: Account = Depends(check_account_exists)
|
||||
data: ApiTokenRequest,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> ApiTokenResponse:
|
||||
if not data.expiration_time_minutes > 0:
|
||||
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):
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||
|
||||
if not account.username:
|
||||
raise ValueError("Username must be configured.")
|
||||
|
||||
acls = await get_user_access_control_lists(account.id)
|
||||
acls = await get_user_access_control_lists(user.id)
|
||||
acl = acls.get_acl_by_id(data.acl_id)
|
||||
if not acl:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
||||
@@ -219,16 +218,18 @@ async def api_create_user_api_token(
|
||||
|
||||
@auth_router.delete("/acl/token")
|
||||
async def api_delete_user_api_token(
|
||||
data: DeleteTokenRequest, account: Account = Depends(check_account_exists)
|
||||
data: DeleteTokenRequest,
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
|
||||
account = await get_account(user.id)
|
||||
if not account or not account.verify_password(data.password):
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||
|
||||
if not account.username:
|
||||
raise ValueError("Username must be configured.")
|
||||
|
||||
acls = await get_user_access_control_lists(account.id)
|
||||
acls = await get_user_access_control_lists(user.id)
|
||||
acl = acls.get_acl_by_id(data.acl_id)
|
||||
if not acl:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
|
||||
@@ -317,20 +318,24 @@ async def register(data: RegisterUser) -> JSONResponse:
|
||||
@auth_router.put("/pubkey")
|
||||
async def update_pubkey(
|
||||
data: UpdateUserPubkey,
|
||||
account: Account = Depends(check_account_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||
) -> User | None:
|
||||
if data.user_id != account.id:
|
||||
if data.user_id != user.id:
|
||||
raise ValueError("Invalid user ID.")
|
||||
|
||||
_validate_auth_timeout(payload.auth_time)
|
||||
if (
|
||||
data.pubkey
|
||||
and data.pubkey != account.pubkey
|
||||
and data.pubkey != user.pubkey
|
||||
and await get_account_by_pubkey(data.pubkey)
|
||||
):
|
||||
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)
|
||||
await update_account(account)
|
||||
return await get_user_from_account(account)
|
||||
@@ -339,19 +344,23 @@ async def update_pubkey(
|
||||
@auth_router.put("/password")
|
||||
async def update_password(
|
||||
data: UpdateUserPassword,
|
||||
account: Account = Depends(check_account_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||
) -> User | None:
|
||||
_validate_auth_timeout(payload.auth_time)
|
||||
if data.user_id != account.id:
|
||||
if data.user_id != user.id:
|
||||
raise ValueError("Invalid user ID.")
|
||||
if (
|
||||
data.username
|
||||
and account.username != data.username
|
||||
and user.username != data.username
|
||||
and await get_account_by_username(data.username)
|
||||
):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||
|
||||
account = await get_account(user.id)
|
||||
if not account:
|
||||
raise ValueError("Account not found.")
|
||||
|
||||
# old accounts do not have a password
|
||||
if account.password_hash:
|
||||
if not data.password_old:
|
||||
@@ -410,11 +419,15 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
||||
|
||||
@auth_router.put("/update")
|
||||
async def update(
|
||||
data: UpdateUser, account: Account = Depends(check_account_exists)
|
||||
data: UpdateUser, user: User = Depends(check_user_exists)
|
||||
) -> User | None:
|
||||
if data.user_id != account.id:
|
||||
if data.user_id != 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:
|
||||
account.username = data.username
|
||||
if data.extra:
|
||||
|
||||
@@ -10,7 +10,6 @@ from lnbits.core.models.misc import SimpleStatus
|
||||
from lnbits.core.models.payments import CreateInvoice
|
||||
from lnbits.core.services.fiat_providers import (
|
||||
check_stripe_signature,
|
||||
verify_paypal_webhook,
|
||||
)
|
||||
from lnbits.core.services.payments import create_fiat_invoice
|
||||
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
|
||||
@@ -38,17 +37,6 @@ async def api_generic_webhook_handler(
|
||||
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(
|
||||
success=False,
|
||||
message=f"Unknown fiat provider '{provider_name}'.",
|
||||
@@ -60,8 +48,6 @@ async def handle_stripe_event(event: dict):
|
||||
event_type = event.get("type")
|
||||
if event_type == "checkout.session.completed":
|
||||
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":
|
||||
await _handle_stripe_subscription_invoice_paid(event)
|
||||
else:
|
||||
@@ -70,38 +56,18 @@ 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):
|
||||
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")
|
||||
alan_action = event_object.get("metadata", {}).get("alan_action")
|
||||
lnbits_action = event_object.get("metadata", {}).get("lnbits_action")
|
||||
logger.debug(
|
||||
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
|
||||
f" Payment hash: '{payment_hash}'."
|
||||
)
|
||||
if alan_action != "invoice":
|
||||
logger.warning(f"Stripe event is not an invoice: '{alan_action}'.")
|
||||
if lnbits_action != "invoice":
|
||||
logger.warning(f"Stripe event is not an invoice: '{lnbits_action}'.")
|
||||
return
|
||||
|
||||
if not payment_hash:
|
||||
@@ -166,7 +132,7 @@ async def _get_stripe_subscription_payment_options(
|
||||
|
||||
metadata = parent.get("subscription_details", {}).get("metadata", {})
|
||||
|
||||
if metadata.get("alan_action") != "subscription":
|
||||
if metadata.get("lnbits_action") != "subscription":
|
||||
raise ValueError("Stripe invoice.paid metadata action is not 'subscription'.")
|
||||
|
||||
if "extra" in metadata:
|
||||
@@ -177,84 +143,3 @@ async def _get_stripe_subscription_payment_options(
|
||||
metadata["extra"] = {}
|
||||
|
||||
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,10 +7,9 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
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 (
|
||||
SimpleStatus,
|
||||
User,
|
||||
)
|
||||
from lnbits.core.models.extensions import (
|
||||
CreateExtension,
|
||||
@@ -24,7 +23,6 @@ from lnbits.core.models.extensions import (
|
||||
UserExtension,
|
||||
UserExtensionInfo,
|
||||
)
|
||||
from lnbits.core.models.users import Account, AccountId
|
||||
from lnbits.core.services import check_transaction_status, create_invoice
|
||||
from lnbits.core.services.extensions import (
|
||||
activate_extension,
|
||||
@@ -35,9 +33,8 @@ from lnbits.core.services.extensions import (
|
||||
uninstall_extension,
|
||||
)
|
||||
from lnbits.decorators import (
|
||||
check_account_exists,
|
||||
check_account_id_exists,
|
||||
check_admin,
|
||||
check_user_exists,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -145,10 +142,9 @@ async def api_extension_details(
|
||||
async def api_update_pay_to_enable(
|
||||
ext_id: str,
|
||||
data: PayToEnableInfo,
|
||||
account: Account = Depends(check_admin),
|
||||
user: User = Depends(check_admin),
|
||||
) -> SimpleStatus:
|
||||
user_wallet_ids = await get_wallets_ids(account.id, deleted=False)
|
||||
if data.wallet not in user_wallet_ids:
|
||||
if data.wallet not in user.wallet_ids:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST, "Wallet does not belong to this admin user."
|
||||
)
|
||||
@@ -167,7 +163,7 @@ async def api_update_pay_to_enable(
|
||||
|
||||
@extension_router.put("/{ext_id}/enable")
|
||||
async def api_enable_extension(
|
||||
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||
ext_id: str, user: User = Depends(check_user_exists)
|
||||
) -> SimpleStatus:
|
||||
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
@@ -181,12 +177,12 @@ async def api_enable_extension(
|
||||
if not ext.active:
|
||||
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||
|
||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||
user_ext = await get_user_extension(user.id, ext_id)
|
||||
if not user_ext:
|
||||
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||
user_ext = UserExtension(user=user.id, extension=ext_id, active=False)
|
||||
await create_user_extension(user_ext)
|
||||
|
||||
if account_id.is_admin_id or not ext.requires_payment:
|
||||
if user.admin or not ext.requires_payment:
|
||||
user_ext.active = True
|
||||
await update_user_extension(user_ext)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
||||
@@ -223,13 +219,13 @@ async def api_enable_extension(
|
||||
|
||||
@extension_router.put("/{ext_id}/disable")
|
||||
async def api_disable_extension(
|
||||
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||
ext_id: str, user: User = Depends(check_user_exists)
|
||||
) -> SimpleStatus:
|
||||
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST, f"Extension '{ext_id}' doesn't exist."
|
||||
)
|
||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||
user_ext = await get_user_extension(user.id, ext_id)
|
||||
if not user_ext or not user_ext.active:
|
||||
return SimpleStatus(
|
||||
success=True, message=f"Extension '{ext_id}' already disabled."
|
||||
@@ -380,9 +376,7 @@ async def get_pay_to_install_invoice(
|
||||
|
||||
@extension_router.put("/{ext_id}/invoice/enable")
|
||||
async def get_pay_to_enable_invoice(
|
||||
ext_id: str,
|
||||
data: PayToEnableInfo,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
ext_id: str, data: PayToEnableInfo, user: User = Depends(check_user_exists)
|
||||
):
|
||||
if not data.amount or data.amount <= 0:
|
||||
raise HTTPException(
|
||||
@@ -428,9 +422,9 @@ async def get_pay_to_enable_invoice(
|
||||
memo=f"Enable '{ext.name}' extension.",
|
||||
)
|
||||
|
||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||
user_ext = await get_user_extension(user.id, ext_id)
|
||||
if not user_ext:
|
||||
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||
user_ext = UserExtension(user=user.id, extension=ext_id, active=False)
|
||||
await create_user_extension(user_ext)
|
||||
user_ext_info = user_ext.extra if user_ext.extra else UserExtensionInfo()
|
||||
user_ext_info.payment_hash_to_enable = payment.payment_hash
|
||||
@@ -441,7 +435,7 @@ async def get_pay_to_enable_invoice(
|
||||
|
||||
@extension_router.get(
|
||||
"/release/{org}/{repo}/{tag_name}",
|
||||
dependencies=[Depends(check_account_exists)],
|
||||
dependencies=[Depends(check_user_exists)],
|
||||
)
|
||||
async def get_extension_release(org: str, repo: str, tag_name: str):
|
||||
try:
|
||||
@@ -462,18 +456,15 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
|
||||
|
||||
@extension_router.get("")
|
||||
async def api_get_user_extensions(
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> list[Extension]:
|
||||
async with db.connect() as conn:
|
||||
user_extensions_ids = [
|
||||
ue.extension for ue in await get_user_extensions(account_id.id, conn=conn)
|
||||
]
|
||||
valid_extensions = [
|
||||
ext
|
||||
for ext in await get_valid_extensions(False, conn=conn)
|
||||
if ext.code in user_extensions_ids
|
||||
]
|
||||
return valid_extensions
|
||||
|
||||
user_extensions_ids = [ue.extension for ue in await get_user_extensions(user.id)]
|
||||
return [
|
||||
ext
|
||||
for ext in await get_valid_extensions(False)
|
||||
if ext.code in user_extensions_ids
|
||||
]
|
||||
|
||||
|
||||
@extension_router.delete(
|
||||
@@ -507,22 +498,11 @@ async def delete_extension_db(ext_id: str):
|
||||
|
||||
# TODO: create a response model for this
|
||||
@extension_router.get("/all")
|
||||
async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
async with db.connect() as conn:
|
||||
installed_exts: list[InstallableExtension] = await get_installed_extensions(
|
||||
conn=conn
|
||||
)
|
||||
all_ext_ids = [ext.code for ext in await get_valid_extensions(conn=conn)]
|
||||
inactive_extensions = [
|
||||
e.id for e in await get_installed_extensions(active=False, conn=conn)
|
||||
]
|
||||
db_versions = await get_db_versions(conn=conn)
|
||||
|
||||
async def extensions(user: User = Depends(check_user_exists)):
|
||||
installed_exts: list[InstallableExtension] = await get_installed_extensions()
|
||||
installed_exts_ids = [e.id for e in installed_exts]
|
||||
|
||||
installable_exts = await InstallableExtension.get_installable_extensions(
|
||||
post_refresh_cache=account_id.is_admin_id
|
||||
)
|
||||
installable_exts = await InstallableExtension.get_installable_extensions()
|
||||
installable_exts_ids = [e.id for e in installable_exts]
|
||||
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
|
||||
|
||||
@@ -530,7 +510,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
|
||||
if installed_ext and installed_ext.meta:
|
||||
installed_release = installed_ext.meta.installed_release
|
||||
if installed_ext.meta.pay_to_enable and not account_id.is_admin_id:
|
||||
if installed_ext.meta.pay_to_enable and not user.admin:
|
||||
# not a security leak, but better not to share the wallet id
|
||||
installed_ext.meta.pay_to_enable.wallet = None
|
||||
pay_to_enable = installed_ext.meta.pay_to_enable
|
||||
@@ -548,6 +528,10 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
e.short_description = installed_ext.short_description
|
||||
e.icon = installed_ext.icon
|
||||
|
||||
all_ext_ids = [ext.code for ext in await get_valid_extensions()]
|
||||
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
|
||||
db_versions = await get_db_versions()
|
||||
|
||||
extension_data = [
|
||||
{
|
||||
"id": ext.id,
|
||||
|
||||
@@ -7,6 +7,7 @@ from fastapi.responses import FileResponse
|
||||
|
||||
from lnbits.core.models import (
|
||||
SimpleStatus,
|
||||
User,
|
||||
)
|
||||
from lnbits.core.models.extensions import (
|
||||
Extension,
|
||||
@@ -15,7 +16,6 @@ from lnbits.core.models.extensions import (
|
||||
UserExtension,
|
||||
)
|
||||
from lnbits.core.models.extensions_builder import ExtensionData
|
||||
from lnbits.core.models.users import Account, AccountId
|
||||
from lnbits.core.services.extensions import (
|
||||
activate_extension,
|
||||
install_extension,
|
||||
@@ -26,9 +26,9 @@ from lnbits.core.services.extensions_builder import (
|
||||
zip_directory,
|
||||
)
|
||||
from lnbits.decorators import (
|
||||
check_account_id_exists,
|
||||
check_admin,
|
||||
check_extension_builder,
|
||||
check_user_exists,
|
||||
)
|
||||
|
||||
from ..crud import (
|
||||
@@ -84,9 +84,9 @@ async def api_build_extension(data: ExtensionData) -> FileResponse:
|
||||
)
|
||||
async def api_deploy_extension(
|
||||
data: ExtensionData,
|
||||
account: Account = Depends(check_admin),
|
||||
user: User = Depends(check_admin),
|
||||
) -> SimpleStatus:
|
||||
working_dir_name = "deploy_" + sha256(account.id.encode("utf-8")).hexdigest()
|
||||
working_dir_name = "deploy_" + sha256(user.id.encode("utf-8")).hexdigest()
|
||||
stub_ext_id = "extension_builder_stub"
|
||||
release, build_dir = await build_extension_from_data(
|
||||
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))
|
||||
|
||||
user_ext = await get_user_extension(account.id, data.id)
|
||||
user_ext = await get_user_extension(user.id, data.id)
|
||||
if not user_ext:
|
||||
user_ext = UserExtension(user=account.id, extension=data.id, active=True)
|
||||
user_ext = UserExtension(user=user.id, extension=data.id, active=True)
|
||||
await create_user_extension(user_ext)
|
||||
elif not user_ext.active:
|
||||
user_ext.active = True
|
||||
@@ -128,10 +128,10 @@ async def api_deploy_extension(
|
||||
)
|
||||
async def api_preview_extension(
|
||||
data: ExtensionData,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> SimpleStatus:
|
||||
stub_ext_id = "extension_builder_stub"
|
||||
working_dir_name = "preview_" + sha256(account_id.id.encode("utf-8")).hexdigest()
|
||||
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
||||
await build_extension_from_data(data, stub_ext_id, working_dir_name)
|
||||
|
||||
return SimpleStatus(success=True, message=f"Extension '{data.id}' preview ready.")
|
||||
|
||||
@@ -75,28 +75,20 @@ async def cancel_subscription(
|
||||
)
|
||||
async def connection_token(provider: str):
|
||||
fiat_provider = await get_fiat_provider(provider)
|
||||
if not fiat_provider:
|
||||
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:
|
||||
if provider == "stripe":
|
||||
if not isinstance(fiat_provider, StripeWallet):
|
||||
raise HTTPException(
|
||||
status_code=502, detail="Stripe returned no connection token"
|
||||
status_code=500, detail="Stripe wallet/provider not configured"
|
||||
)
|
||||
return {"secret": secret}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Failed to create connection token"
|
||||
) from e
|
||||
try:
|
||||
tok = await fiat_provider.create_terminal_connection_token()
|
||||
secret = tok.get("secret")
|
||||
if not secret:
|
||||
raise HTTPException(
|
||||
status_code=502, detail="Stripe returned no connection token"
|
||||
)
|
||||
return {"secret": secret}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Failed to create connection token"
|
||||
) from e
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
from hashlib import sha256
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
import httpx
|
||||
from fastapi import Depends, Request
|
||||
from fastapi import Cookie, Depends, Query, Request
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from lnurl import url_decode
|
||||
from pydantic.types import UUID4
|
||||
|
||||
from lnbits.core.helpers import to_valid_user_id
|
||||
from lnbits.core.models import User
|
||||
@@ -23,7 +25,12 @@ from lnbits.decorators import (
|
||||
from lnbits.helpers import check_callback_url, template_renderer
|
||||
from lnbits.settings import settings
|
||||
|
||||
from ..crud import get_user
|
||||
from ...utils.exchange_rates import allowed_currencies
|
||||
from ..crud import (
|
||||
create_wallet,
|
||||
get_user,
|
||||
get_wallet,
|
||||
)
|
||||
|
||||
generic_router = APIRouter(
|
||||
tags=["Core NON-API Website Routes"], include_in_schema=False
|
||||
@@ -35,9 +42,60 @@ async def favicon():
|
||||
return RedirectResponse(settings.lnbits_qr_logo)
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
"/wallet",
|
||||
response_class=HTMLResponse,
|
||||
description="show wallet page",
|
||||
)
|
||||
async def get_user_wallet(
|
||||
request: Request,
|
||||
lnbits_last_active_wallet: Annotated[str | None, Cookie()] = None,
|
||||
user: User = Depends(check_user_exists),
|
||||
wal: UUID4 | None = Query(None),
|
||||
):
|
||||
if wal:
|
||||
wallet = await get_wallet(wal.hex)
|
||||
elif len(user.wallets) == 0:
|
||||
wallet = await create_wallet(user_id=user.id)
|
||||
user.wallets.append(wallet)
|
||||
elif lnbits_last_active_wallet and user.get_wallet(lnbits_last_active_wallet):
|
||||
wallet = await get_wallet(lnbits_last_active_wallet)
|
||||
else:
|
||||
wallet = user.wallets[0]
|
||||
|
||||
if not wallet or wallet.deleted:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Wallet not found",
|
||||
)
|
||||
if wallet.user != user.id:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Not your wallet.",
|
||||
)
|
||||
context = {
|
||||
"user": user.json(),
|
||||
"wallet": wallet.json(),
|
||||
"wallet_name": wallet.name,
|
||||
"currencies": allowed_currencies(),
|
||||
"service_fee": settings.lnbits_service_fee,
|
||||
"service_fee_max": settings.lnbits_service_fee_max,
|
||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
||||
}
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"core/wallet.html",
|
||||
{**context, "ajax": _is_ajax_request(request)},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/robots.txt", response_class=HTMLResponse)
|
||||
async def robots():
|
||||
data = "User-agent: *\nDisallow: /"
|
||||
data = """
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
"""
|
||||
return HTMLResponse(content=data, media_type="text/plain")
|
||||
|
||||
|
||||
@@ -76,8 +134,8 @@ async def extensions_builder_preview(
|
||||
request,
|
||||
"error.html",
|
||||
{
|
||||
"status_code": 404,
|
||||
"message": f"Extension {ext_id} not found, refresh Preview.",
|
||||
"err": f"Extension {ext_id} not found",
|
||||
"message": "Please 'Refresh Preview' first.",
|
||||
},
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
@@ -87,6 +145,7 @@ async def extensions_builder_preview(
|
||||
html_file_path.as_posix(),
|
||||
{
|
||||
"user": user.json(),
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -95,7 +154,6 @@ async def extensions_builder_preview(
|
||||
"style-src 'self' 'unsafe-inline'; "
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -196,8 +254,6 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
||||
|
||||
|
||||
@generic_router.get("/payments")
|
||||
@generic_router.get("/wallet")
|
||||
@generic_router.get("/wallet/{wallet_id}")
|
||||
@generic_router.get("/wallets")
|
||||
@generic_router.get("/account")
|
||||
@generic_router.get("/extensions")
|
||||
@@ -224,7 +280,7 @@ async def index(
|
||||
@generic_router.get("/node/public")
|
||||
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
||||
async def index_public(request: Request) -> HTMLResponse:
|
||||
return template_renderer().TemplateResponse(request, "index.html", {"public": True})
|
||||
return template_renderer().TemplateResponse(request, "index_public.html")
|
||||
|
||||
|
||||
@generic_router.get("/uuidv4/{hex_value}")
|
||||
@@ -285,3 +341,7 @@ async def lnurlwallet(request: Request, lightning: str = ""):
|
||||
return RedirectResponse(
|
||||
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 (
|
||||
WalletTypeInfo,
|
||||
require_admin_key,
|
||||
require_base_invoice_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import check_callback_url
|
||||
from lnbits.settings import settings
|
||||
@@ -48,7 +48,7 @@ async def _handle(lnurl: str) -> LnurlResponseModel:
|
||||
|
||||
@lnurl_router.get(
|
||||
"/api/v1/lnurlscan/{code}",
|
||||
dependencies=[Depends(require_base_invoice_key)],
|
||||
dependencies=[Depends(require_invoice_key)],
|
||||
deprecated=True,
|
||||
response_model=LnurlPayResponse
|
||||
| LnurlWithdrawResponse
|
||||
@@ -64,7 +64,7 @@ async def api_lnurlscan(code: str) -> LnurlResponseModel:
|
||||
|
||||
@lnurl_router.post(
|
||||
"/api/v1/lnurlscan",
|
||||
dependencies=[Depends(require_base_invoice_key)],
|
||||
dependencies=[Depends(require_invoice_key)],
|
||||
response_model=LnurlPayResponse
|
||||
| LnurlWithdrawResponse
|
||||
| LnurlAuthResponse
|
||||
|
||||
@@ -18,7 +18,6 @@ from lnbits.core.crud.payments import (
|
||||
update_payment,
|
||||
)
|
||||
from lnbits.core.crud.users import get_account
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import (
|
||||
CancelInvoice,
|
||||
CreateInvoice,
|
||||
@@ -36,16 +35,14 @@ from lnbits.core.models import (
|
||||
SimpleStatus,
|
||||
)
|
||||
from lnbits.core.models.payments import UpdatePaymentLabels
|
||||
from lnbits.core.models.users import AccountId
|
||||
from lnbits.core.models.wallets import BaseWalletTypeInfo
|
||||
from lnbits.core.models.users import User
|
||||
from lnbits.db import Filters, Page
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
check_account_id_exists,
|
||||
check_user_exists,
|
||||
parse_filters,
|
||||
require_admin_key,
|
||||
require_base_admin_key,
|
||||
require_base_invoice_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import (
|
||||
filter_dict_keys,
|
||||
@@ -70,6 +67,7 @@ from ..services import (
|
||||
perform_withdraw,
|
||||
settle_hold_invoice,
|
||||
update_pending_payment,
|
||||
update_pending_payments,
|
||||
)
|
||||
|
||||
payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
|
||||
@@ -84,9 +82,10 @@ payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
|
||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||
)
|
||||
async def api_payments(
|
||||
key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key),
|
||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||
):
|
||||
await update_pending_payments(key_info.wallet.id)
|
||||
return await get_payments(
|
||||
wallet_id=key_info.wallet.id,
|
||||
pending=True,
|
||||
@@ -102,10 +101,11 @@ async def api_payments(
|
||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||
)
|
||||
async def api_payments_history(
|
||||
key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key),
|
||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||
group: DateTrunc = Query("day"),
|
||||
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)
|
||||
|
||||
|
||||
@@ -118,14 +118,14 @@ async def api_payments_history(
|
||||
async def api_payments_counting_stats(
|
||||
count_by: PaymentCountField = Query("tag"),
|
||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
if account_id.is_admin_id:
|
||||
if user.admin:
|
||||
# admin user can see payments from all wallets
|
||||
for_user_id = None
|
||||
else:
|
||||
# regular user can only see payments from their wallets
|
||||
for_user_id = account_id.id
|
||||
for_user_id = 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(
|
||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
if account_id.is_admin_id:
|
||||
if user.admin:
|
||||
# admin user can see payments from all wallets
|
||||
for_user_id = None
|
||||
else:
|
||||
# regular user can only see payments from their wallets
|
||||
for_user_id = account_id.id
|
||||
for_user_id = 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),
|
||||
)
|
||||
async def api_payments_daily_stats(
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||
):
|
||||
if account_id.is_admin_id:
|
||||
if user.admin:
|
||||
# admin user can see payments from all wallets
|
||||
for_user_id = None
|
||||
else:
|
||||
# regular user can only see payments from their wallets
|
||||
for_user_id = account_id.id
|
||||
for_user_id = user.id
|
||||
return await get_payments_daily_stats(filters, user_id=for_user_id)
|
||||
|
||||
|
||||
@@ -178,30 +178,18 @@ async def api_payments_daily_stats(
|
||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||
)
|
||||
async def api_payments_paginated(
|
||||
key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key),
|
||||
recheck_pending: bool = Query(
|
||||
False, description="Force check and update of pending payments."
|
||||
),
|
||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||
) -> Page[Payment]:
|
||||
async with db.connect() as conn:
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=key_info.wallet.id,
|
||||
filters=filters,
|
||||
conn=conn,
|
||||
)
|
||||
if not recheck_pending:
|
||||
return page
|
||||
):
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=key_info.wallet.id,
|
||||
filters=filters,
|
||||
)
|
||||
for payment in page.data:
|
||||
if payment.pending:
|
||||
await update_pending_payment(payment)
|
||||
|
||||
payments = []
|
||||
for payment in page.data:
|
||||
if payment.pending:
|
||||
refreshed_payment = await update_pending_payment(payment, conn=conn)
|
||||
payments.append(refreshed_payment)
|
||||
else:
|
||||
payments.append(payment)
|
||||
|
||||
return Page(data=payments, total=page.total)
|
||||
return page
|
||||
|
||||
|
||||
@payment_router.get(
|
||||
@@ -214,19 +202,19 @@ async def api_payments_paginated(
|
||||
)
|
||||
async def api_all_payments_paginated(
|
||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
if account_id.is_admin_id:
|
||||
if user.admin:
|
||||
# admin user can see payments from all wallets
|
||||
for_user_id = None
|
||||
else:
|
||||
# regular user can only see payments from their wallets
|
||||
for_user_id = account_id.id
|
||||
for_user_id = user.id
|
||||
|
||||
async with db.connect() as conn:
|
||||
return await get_payments_paginated(
|
||||
filters=filters, user_id=for_user_id, conn=conn
|
||||
)
|
||||
return await get_payments_paginated(
|
||||
filters=filters,
|
||||
user_id=for_user_id,
|
||||
)
|
||||
|
||||
|
||||
@payment_router.post(
|
||||
@@ -249,10 +237,10 @@ async def api_all_payments_paginated(
|
||||
)
|
||||
async def api_payments_create(
|
||||
invoice_data: CreateInvoice,
|
||||
key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key),
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
) -> Payment:
|
||||
wallet_id = key_info.wallet.id
|
||||
if invoice_data.out is True and key_info.key_type == KeyType.admin:
|
||||
wallet_id = wallet.wallet.id
|
||||
if invoice_data.out is True and wallet.key_type == KeyType.admin:
|
||||
if not invoice_data.bolt11:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
@@ -280,8 +268,9 @@ async def api_payments_create(
|
||||
async def api_update_payment_labels(
|
||||
payment_hash: str,
|
||||
data: UpdatePaymentLabels,
|
||||
key_type: BaseWalletTypeInfo = Depends(require_base_admin_key),
|
||||
key_type: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> SimpleStatus:
|
||||
|
||||
payment = await get_standalone_payment(payment_hash, wallet_id=key_type.wallet.id)
|
||||
if payment is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Payment does not exist.")
|
||||
|
||||
@@ -7,11 +7,10 @@ from fastapi import (
|
||||
)
|
||||
from starlette.responses import RedirectResponse
|
||||
|
||||
from lnbits.core.models.wallets import BaseWalletTypeInfo
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
require_admin_key,
|
||||
require_base_invoice_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
|
||||
from ..crud import (
|
||||
@@ -51,12 +50,12 @@ async def api_create_tinyurl(
|
||||
description="get a tinyurl by id",
|
||||
)
|
||||
async def api_get_tinyurl(
|
||||
tinyurl_id: str, key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key)
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||
):
|
||||
try:
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == key_info.wallet.id:
|
||||
if tinyurl.wallet == wallet.wallet.id:
|
||||
return tinyurl
|
||||
raise HTTPException(
|
||||
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")
|
||||
async def api_update_user(
|
||||
user_id: str, data: CreateUser, account: Account = Depends(check_admin)
|
||||
user_id: str, data: CreateUser, user: User = Depends(check_admin)
|
||||
) -> CreateUser:
|
||||
if user_id != data.id:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
|
||||
|
||||
if user_id == settings.super_user and account.id != settings.super_user:
|
||||
if user_id == settings.super_user and user.id != settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Action only allowed for super user.",
|
||||
@@ -154,7 +154,7 @@ async def api_update_user(
|
||||
name="Delete user by Id",
|
||||
)
|
||||
async def api_users_delete_user(
|
||||
user_id: str, account: Account = Depends(check_admin)
|
||||
user_id: str, user: User = Depends(check_admin)
|
||||
) -> SimpleStatus:
|
||||
wallets = await get_wallets(user_id, deleted=False)
|
||||
if len(wallets) > 0:
|
||||
@@ -169,7 +169,7 @@ async def api_users_delete_user(
|
||||
detail="Cannot delete super user.",
|
||||
)
|
||||
|
||||
if user_id in settings.lnbits_admin_users and not account.is_super_user:
|
||||
if user_id in settings.lnbits_admin_users and not user.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
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",
|
||||
)
|
||||
async def api_users_delete_user_wallet(
|
||||
user_id: str, wallet: str, account: Account = Depends(check_admin)
|
||||
user_id: str, wallet: str, user: User = Depends(check_admin)
|
||||
) -> SimpleStatus:
|
||||
wal = await get_wallet(wallet)
|
||||
if not wal:
|
||||
@@ -304,7 +304,7 @@ async def api_users_delete_user_wallet(
|
||||
detail="Wallet does not exist.",
|
||||
)
|
||||
|
||||
if user_id == settings.super_user and account.id != settings.super_user:
|
||||
if user_id == settings.super_user and user.id != settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Action only allowed for super user.",
|
||||
|
||||
@@ -12,10 +12,9 @@ from lnbits.core.crud.wallets import (
|
||||
create_wallet,
|
||||
get_wallets_paginated,
|
||||
)
|
||||
from lnbits.core.models import CreateWallet, KeyType, Wallet, WalletTypeInfo
|
||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
|
||||
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
|
||||
from lnbits.core.models.misc import SimpleStatus
|
||||
from lnbits.core.models.users import Account, AccountId
|
||||
from lnbits.core.models.wallets import (
|
||||
WalletsFilters,
|
||||
WalletSharePermission,
|
||||
@@ -30,14 +29,12 @@ from lnbits.core.services.wallets import (
|
||||
)
|
||||
from lnbits.db import Filters, Page
|
||||
from lnbits.decorators import (
|
||||
check_account_exists,
|
||||
check_account_id_exists,
|
||||
check_user_exists,
|
||||
parse_filters,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import generate_filter_params_openapi
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
from ..crud import (
|
||||
delete_wallet,
|
||||
@@ -68,11 +65,11 @@ async def api_wallet(key_info: WalletTypeInfo = Depends(require_invoice_key)):
|
||||
openapi_extra=generate_filter_params_openapi(WalletsFilters),
|
||||
)
|
||||
async def api_wallets_paginated(
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
filters: Filters = Depends(parse_filters(WalletsFilters)),
|
||||
):
|
||||
page = await get_wallets_paginated(
|
||||
user_id=account_id.id,
|
||||
user_id=user.id,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
@@ -88,7 +85,7 @@ async def api_invite_wallet_share(
|
||||
|
||||
@wallet_router.delete("/share/invite/{share_request_id}")
|
||||
async def api_reject_wallet_invitation(
|
||||
share_request_id: str, invited_user: Account = Depends(check_account_exists)
|
||||
share_request_id: str, invited_user: User = Depends(check_user_exists)
|
||||
) -> SimpleStatus:
|
||||
await reject_wallet_invitation(invited_user.id, share_request_id)
|
||||
return SimpleStatus(success=True, message="Invitation rejected.")
|
||||
@@ -127,17 +124,12 @@ async def api_update_wallet_name(
|
||||
|
||||
@wallet_router.put("/reset/{wallet_id}")
|
||||
async def api_reset_wallet_keys(
|
||||
wallet_id: str,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
wallet_id: str, user: User = Depends(check_user_exists)
|
||||
) -> Wallet:
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet or wallet.user != account_id.id:
|
||||
if not wallet or wallet.user != user.id:
|
||||
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.inkey = uuid4().hex
|
||||
await update_wallet(wallet)
|
||||
@@ -183,10 +175,10 @@ async def api_update_wallet(
|
||||
|
||||
@wallet_router.delete("/{wallet_id}")
|
||||
async def api_delete_wallet(
|
||||
wallet_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||
wallet_id: str, user: User = Depends(check_user_exists)
|
||||
) -> None:
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet or wallet.user != account_id.id:
|
||||
if not wallet or wallet.user != user.id:
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||
|
||||
await delete_wallet(
|
||||
@@ -197,14 +189,11 @@ async def api_delete_wallet(
|
||||
|
||||
@wallet_router.post("")
|
||||
async def api_create_wallet(
|
||||
data: CreateWallet, account_id: AccountId = Depends(check_account_id_exists)
|
||||
data: CreateWallet,
|
||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Wallet:
|
||||
|
||||
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:
|
||||
return await create_wallet(user_id=key_info.wallet.user, wallet_name=data.name)
|
||||
|
||||
if data.wallet_type == WalletType.LIGHTNING_SHARED:
|
||||
if not data.shared_wallet_id:
|
||||
@@ -213,9 +202,11 @@ async def api_create_wallet(
|
||||
"Shared wallet ID is required for shared wallets.",
|
||||
)
|
||||
return await create_lightning_shared_wallet(
|
||||
user_id=account_id.id,
|
||||
user_id=key_info.wallet.user,
|
||||
source_wallet_id=data.shared_wallet_id,
|
||||
)
|
||||
|
||||
# default WalletType.LIGHTNING:
|
||||
return await create_wallet(user_id=account_id.id, wallet_name=data.name)
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
f"Unknown wallet type: {data.wallet_type}.",
|
||||
)
|
||||
|
||||
@@ -15,9 +15,9 @@ from lnbits.core.models import (
|
||||
CreateWebPushSubscription,
|
||||
WebPushSubscription,
|
||||
)
|
||||
from lnbits.core.models.users import AccountId
|
||||
from lnbits.core.models.users import User
|
||||
from lnbits.decorators import (
|
||||
check_account_id_exists,
|
||||
check_user_exists,
|
||||
)
|
||||
|
||||
from ..crud import (
|
||||
@@ -33,20 +33,20 @@ webpush_router = APIRouter(prefix="/api/v1/webpush", tags=["Webpush"])
|
||||
async def api_create_webpush_subscription(
|
||||
request: Request,
|
||||
data: CreateWebPushSubscription,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> WebPushSubscription:
|
||||
try:
|
||||
subscription = json.loads(data.subscription)
|
||||
endpoint = subscription["endpoint"]
|
||||
host = urlparse(str(request.url)).netloc
|
||||
|
||||
subscription = await get_webpush_subscription(endpoint, account_id.id)
|
||||
subscription = await get_webpush_subscription(endpoint, user.id)
|
||||
if subscription:
|
||||
return subscription
|
||||
else:
|
||||
return await create_webpush_subscription(
|
||||
endpoint,
|
||||
account_id.id,
|
||||
user.id,
|
||||
data.subscription,
|
||||
host,
|
||||
)
|
||||
@@ -61,13 +61,13 @@ async def api_create_webpush_subscription(
|
||||
@webpush_router.delete("", status_code=HTTPStatus.OK)
|
||||
async def api_delete_webpush_subscription(
|
||||
request: Request,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
try:
|
||||
endpoint = unquote(
|
||||
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
|
||||
)
|
||||
count = await delete_webpush_subscription(endpoint, account_id.id)
|
||||
count = await delete_webpush_subscription(endpoint, user.id)
|
||||
return {"count": count}
|
||||
except Exception as exc:
|
||||
logger.debug(exc)
|
||||
|
||||
+15
-29
@@ -235,14 +235,11 @@ class Connection(Compat):
|
||||
table_name: if provided some optimisations can be applied.
|
||||
"""
|
||||
|
||||
if table_name and not _valid_sql_name(table_name):
|
||||
raise ValueError(f"Invalid table name: '{table_name}'.")
|
||||
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
|
||||
if table_name:
|
||||
if not _valid_sql_name(table_name):
|
||||
raise ValueError(f"Invalid table name: '{table_name}'.")
|
||||
filters.set_table_name(table_name)
|
||||
|
||||
clause = filters.where(where)
|
||||
parsed_values = filters.values(values)
|
||||
|
||||
@@ -494,7 +491,6 @@ class Page(BaseModel, Generic[T]):
|
||||
|
||||
|
||||
class Filter(BaseModel, Generic[TFilterModel]):
|
||||
table_name: str | None = None
|
||||
field: str
|
||||
op: Operator = Operator.EQ
|
||||
model: type[TFilterModel] | None
|
||||
@@ -522,7 +518,7 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
||||
if field in model.__fields__:
|
||||
compare_field = model.__fields__[field]
|
||||
values: dict = {}
|
||||
if op in {Operator.EVERY, Operator.ANY, Operator.INCLUDE, Operator.EXCLUDE}:
|
||||
if op in {Operator.EVERY, Operator.ANY}:
|
||||
raw_values = [v for rv in raw_values for v in rv.split(",")]
|
||||
|
||||
for index, raw_value in enumerate(raw_values):
|
||||
@@ -537,23 +533,20 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
@property
|
||||
def statement(self) -> str:
|
||||
prefix = f"{self.table_name}." if self.table_name else ""
|
||||
stmt = []
|
||||
for key in self.values.keys() if self.values else []:
|
||||
if self.model and self.model.__fields__[self.field].type_ == datetime:
|
||||
clean_key = key.split("__")[0]
|
||||
if self.model and self.model.__fields__[clean_key].type_ == datetime:
|
||||
placeholder = compat_timestamp_placeholder(key)
|
||||
stmt.append(f"{prefix}{self.field} {self.op.as_sql} {placeholder}")
|
||||
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
||||
stmt.append(f":{key}")
|
||||
stmt.append(f"{clean_key} {self.op.as_sql} {placeholder}")
|
||||
else:
|
||||
stmt.append(f"{prefix}{self.field} {self.op.as_sql} :{key}")
|
||||
stmt.append(f"{clean_key} {self.op.as_sql} :{key}")
|
||||
|
||||
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
||||
statement = f"{prefix}{self.field} {self.op.as_sql} ({', '.join(stmt)})"
|
||||
elif self.op == Operator.EVERY:
|
||||
if self.op == Operator.EVERY:
|
||||
statement = " AND ".join(stmt)
|
||||
else:
|
||||
statement = " OR ".join(stmt)
|
||||
|
||||
return f"({statement})"
|
||||
|
||||
|
||||
@@ -570,14 +563,13 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
search: str | None = None
|
||||
|
||||
offset: int | None = None
|
||||
limit: int | None = 10
|
||||
limit: int | None = None
|
||||
|
||||
sortby: str | None = None
|
||||
direction: Literal["asc", "desc"] | None = None
|
||||
|
||||
model: type[TFilterModel] | None = None
|
||||
|
||||
table_name: str | None = None
|
||||
|
||||
@root_validator(pre=True)
|
||||
def validate_sortby(cls, values):
|
||||
sortby = values.get("sortby")
|
||||
@@ -592,8 +584,8 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
def pagination(self) -> str:
|
||||
stmt = ""
|
||||
self.limit = self.limit or 10
|
||||
stmt += f"LIMIT {min(1000, self.limit)} "
|
||||
if self.limit:
|
||||
stmt += f"LIMIT {self.limit} "
|
||||
if self.offset:
|
||||
stmt += f"OFFSET {self.offset}"
|
||||
return stmt
|
||||
@@ -619,8 +611,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
def order_by(self) -> str:
|
||||
if self.sortby:
|
||||
prefix = f"{self.table_name}." if self.table_name else ""
|
||||
return f"ORDER BY {prefix}{self.sortby} {self.direction or 'asc'}"
|
||||
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
||||
return ""
|
||||
|
||||
def values(self, values: dict | None = None) -> dict:
|
||||
@@ -640,11 +631,6 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
values["search"] = f"%{self.search.lower()}%"
|
||||
return values
|
||||
|
||||
def set_table_name(self, table_name: str) -> None:
|
||||
self.table_name = table_name
|
||||
for page_filter in self.filters:
|
||||
page_filter.table_name = table_name
|
||||
|
||||
|
||||
class DbJsonEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
|
||||
+53
-221
@@ -19,8 +19,6 @@ from lnbits.core.crud import (
|
||||
get_wallet_for_key,
|
||||
)
|
||||
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 (
|
||||
AccessTokenPayload,
|
||||
Account,
|
||||
@@ -29,12 +27,9 @@ from lnbits.core.models import (
|
||||
User,
|
||||
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.helpers import normalize_path, path_segments, sha256s
|
||||
from lnbits.helpers import normalize_path, path_segments
|
||||
from lnbits.settings import AuthMethods, settings
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(
|
||||
tokenUrl="api/v1/auth",
|
||||
@@ -57,7 +52,7 @@ api_key_query = APIKeyQuery(
|
||||
)
|
||||
|
||||
|
||||
class BaseKeyChecker(SecurityBase):
|
||||
class KeyChecker(SecurityBase):
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str | None = None,
|
||||
@@ -82,7 +77,8 @@ class BaseKeyChecker(SecurityBase):
|
||||
)
|
||||
self.model: APIKey = openapi_model # type: ignore
|
||||
|
||||
def _extract_key_value(self, request):
|
||||
async def __call__(self, request: Request) -> WalletTypeInfo:
|
||||
|
||||
key_value = (
|
||||
self._api_key
|
||||
if self._api_key
|
||||
@@ -95,88 +91,27 @@ class BaseKeyChecker(SecurityBase):
|
||||
detail="No Api Key provided.",
|
||||
)
|
||||
|
||||
return key_value
|
||||
wallet = await get_wallet_for_key(key_value)
|
||||
|
||||
async def _extract_key_type(self, key_value: str, wallet: BaseWallet) -> KeyType:
|
||||
if not wallet:
|
||||
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:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Invalid adminkey.",
|
||||
)
|
||||
|
||||
await _check_user_extension_access(wallet.user, request["path"])
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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(
|
||||
request: Request,
|
||||
api_key_header: str = Security(api_key_header),
|
||||
@@ -189,18 +124,6 @@ async def require_admin_key(
|
||||
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(
|
||||
request: Request,
|
||||
api_key_header: str = Security(api_key_header),
|
||||
@@ -213,18 +136,6 @@ async def require_invoice_key(
|
||||
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(
|
||||
header_access_token: Annotated[str | None, Depends(oauth2_scheme)],
|
||||
cookie_access_token: Annotated[str | None, Cookie()] = None,
|
||||
@@ -233,95 +144,33 @@ async def check_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, 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(
|
||||
r: Request,
|
||||
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||
usr: UUID4 | None = None,
|
||||
) -> User:
|
||||
async with db.connect() as conn:
|
||||
account = await _check_account_exists(r, access_token, usr, conn=conn)
|
||||
user = await get_user_from_account(account, conn=conn)
|
||||
if not user:
|
||||
if access_token:
|
||||
account = await _get_account_from_token(access_token, r["path"], r["method"])
|
||||
elif usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||
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:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not found.")
|
||||
await _check_user_extension_access(user.id, r["path"])
|
||||
return user
|
||||
|
||||
|
||||
@@ -349,36 +198,29 @@ async def access_token_payload(
|
||||
return AccessTokenPayload(**payload)
|
||||
|
||||
|
||||
async def check_admin(
|
||||
account: Annotated[Account, Depends(check_account_exists)],
|
||||
) -> Account:
|
||||
if (
|
||||
account.id != settings.super_user
|
||||
and account.id not in settings.lnbits_admin_users
|
||||
):
|
||||
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "User not authorized. No admin privileges."
|
||||
)
|
||||
if not account.has_password:
|
||||
if not user.has_password:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "Admin users must have credentials configured."
|
||||
)
|
||||
|
||||
return account
|
||||
return user
|
||||
|
||||
|
||||
async def check_super_user(
|
||||
account: Annotated[Account, Depends(check_admin)],
|
||||
) -> Account:
|
||||
if account.id != settings.super_user:
|
||||
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||
if user.id != settings.super_user:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "User not authorized. No super user privileges."
|
||||
)
|
||||
if not account.has_password:
|
||||
if not user.has_password:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "Super user must have credentials configured."
|
||||
)
|
||||
return account
|
||||
return user
|
||||
|
||||
|
||||
def parse_filters(model: type[TFilterModel]):
|
||||
@@ -438,17 +280,9 @@ async def check_user_extension_access(
|
||||
return SimpleStatus(success=True, message="OK")
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
async def _check_user_extension_access(user_id: str, path: str):
|
||||
ext_id = path_segments(path)[0]
|
||||
status = await check_user_extension_access(user_id, ext_id, conn=conn)
|
||||
status = await check_user_extension_access(user_id, ext_id)
|
||||
if not status.success:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
@@ -457,12 +291,12 @@ async def _check_user_extension_access(
|
||||
|
||||
|
||||
async def _get_account_from_token(
|
||||
access_token: str, path: str, method: str, conn: Connection | None = None
|
||||
access_token: str, path: str, method: str
|
||||
) -> Account | None:
|
||||
try:
|
||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
||||
return await _get_account_from_jwt_payload(
|
||||
AccessTokenPayload(**payload), path, method, conn=conn
|
||||
AccessTokenPayload(**payload), path, method
|
||||
)
|
||||
|
||||
except jwt.ExpiredSignatureError as exc:
|
||||
@@ -475,35 +309,33 @@ async def _get_account_from_token(
|
||||
|
||||
|
||||
async def _get_account_from_jwt_payload(
|
||||
payload: AccessTokenPayload, path: str, method: str, conn: Connection | None = None
|
||||
payload: AccessTokenPayload, path: str, method: str
|
||||
) -> Account | None:
|
||||
account = None
|
||||
if payload.sub:
|
||||
account = await get_account_by_username(payload.sub, conn=conn)
|
||||
account = await get_account_by_username(payload.sub)
|
||||
elif payload.usr:
|
||||
account = await get_account(payload.usr, conn=conn)
|
||||
account = await get_account(payload.usr)
|
||||
elif payload.email:
|
||||
account = await get_account_by_email(payload.email, conn=conn)
|
||||
account = await get_account_by_email(payload.email)
|
||||
|
||||
if not account:
|
||||
return None
|
||||
|
||||
if payload.api_token_id:
|
||||
await _check_account_api_access(
|
||||
account.id, payload.api_token_id, path, method, conn=conn
|
||||
)
|
||||
await _check_account_api_access(account.id, payload.api_token_id, path, method)
|
||||
|
||||
return account
|
||||
|
||||
|
||||
async def _check_account_api_access(
|
||||
user_id: str, token_id: str, path: str, method: str, conn: Connection | None = None
|
||||
user_id: str, token_id: str, path: str, method: str
|
||||
):
|
||||
segments = path.split("/")
|
||||
if len(segments) < 3:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Not an API endpoint.")
|
||||
|
||||
acls = await get_user_access_control_lists(user_id, conn=conn)
|
||||
acls = await get_user_access_control_lists(user_id)
|
||||
acl = acls.get_acl_by_token_id(token_id)
|
||||
if not acl:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Invalid token id.")
|
||||
|
||||
@@ -138,9 +138,7 @@ def register_exception_handlers(app: FastAPI): # noqa: C901
|
||||
|
||||
@app.exception_handler(404)
|
||||
async def error_handler_404(request: Request, exc: HTTPException):
|
||||
|
||||
if not request.url.path.endswith("routes.json"):
|
||||
logger.error(f"404: {request.url.path} {exc.status_code}: {exc.detail}")
|
||||
logger.error(f"404: {request.url.path} {exc.status_code}: {exc.detail}")
|
||||
|
||||
if not _is_browser_request(request):
|
||||
return JSONResponse(
|
||||
|
||||
@@ -8,7 +8,6 @@ from loguru import logger
|
||||
from lnbits.fiat.base import FiatProvider
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .paypal import PayPalWallet
|
||||
from .stripe import StripeWallet
|
||||
|
||||
fiat_module = importlib.import_module("lnbits.fiat")
|
||||
@@ -16,7 +15,6 @@ fiat_module = importlib.import_module("lnbits.fiat")
|
||||
|
||||
class FiatProviderType(Enum):
|
||||
stripe = "StripeWallet"
|
||||
paypal = "PayPalWallet"
|
||||
|
||||
|
||||
async def get_fiat_provider(name: str) -> FiatProvider | None:
|
||||
@@ -51,6 +49,5 @@ fiat_providers: dict[str, FiatProvider] = {}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PayPalWallet",
|
||||
"StripeWallet",
|
||||
]
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
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),
|
||||
]
|
||||
)
|
||||
@@ -170,7 +170,7 @@ class StripeWallet(FiatProvider):
|
||||
("line_items[0][price]", subscription_id),
|
||||
("line_items[0][quantity]", f"{quantity}"),
|
||||
]
|
||||
subscription_data = {**payment_options.dict(), "alan_action": "subscription"}
|
||||
subscription_data = {**payment_options.dict(), "lnbits_action": "subscription"}
|
||||
subscription_data["extra"] = json.dumps(subscription_data.get("extra") or {})
|
||||
|
||||
form_data += self._encode_metadata(
|
||||
@@ -315,7 +315,7 @@ class StripeWallet(FiatProvider):
|
||||
("mode", "payment"),
|
||||
("success_url", success_url),
|
||||
("metadata[payment_hash]", payment_hash),
|
||||
("metadata[alan_action]", "invoice"),
|
||||
("metadata[lnbits_action]", "invoice"),
|
||||
("line_items[0][price_data][currency]", currency.lower()),
|
||||
("line_items[0][price_data][product_data][name]", line_item_name),
|
||||
("line_items[0][price_data][unit_amount]", str(amount_cents)),
|
||||
|
||||
@@ -18,7 +18,6 @@ from pydantic.schema import field_schema
|
||||
from lnbits.jinja2_templating import Jinja2Templates
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
from lnbits.utils.exchange_rates import currencies
|
||||
|
||||
from .db import FilterModel
|
||||
|
||||
@@ -68,7 +67,6 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
folders.extend(additional_folders)
|
||||
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
||||
t.env.globals["static_url_for"] = static_url_for
|
||||
t.env.globals["normalize_path"] = normalize_path
|
||||
|
||||
window_settings = {
|
||||
"AD_SPACE": settings.lnbits_ad_space,
|
||||
@@ -92,7 +90,6 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
||||
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
||||
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
||||
"LNBITS_APPLE_TOUCH_ICON": settings.lnbits_apple_touch_icon,
|
||||
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
||||
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
||||
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
||||
@@ -112,9 +109,6 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
||||
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
|
||||
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
|
||||
"LNBITS_CURRENCIES": list(currencies.keys()),
|
||||
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
|
||||
"CACHE_KEY": settings.server_startup_time,
|
||||
}
|
||||
|
||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||
|
||||
+4
-44
@@ -6,7 +6,6 @@ import inspect
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from os import path
|
||||
@@ -19,21 +18,6 @@ from loguru import logger
|
||||
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
||||
|
||||
|
||||
def generate_default_boltz_mnemonic() -> str | None:
|
||||
"""
|
||||
Generate a BIP39 mnemonic using the bundled embit dependency.
|
||||
If embit is unavailable at runtime, return None to avoid blocking startup.
|
||||
"""
|
||||
try:
|
||||
from embit import bip39
|
||||
except Exception as exc: # pragma: no cover - dependency missing at runtime
|
||||
logger.warning(f"Unable to generate Boltz mnemonic (embit missing): {exc}")
|
||||
return None
|
||||
|
||||
entropy = secrets.token_bytes(16) # 128 bits -> 12-word mnemonic
|
||||
return bip39.mnemonic_from_bytes(entropy)
|
||||
|
||||
|
||||
def list_parse_fallback(v: str):
|
||||
v = v.replace(" ", "")
|
||||
if len(v) > 0:
|
||||
@@ -286,7 +270,6 @@ class ThemesSettings(LNbitsSettings):
|
||||
lnbits_allowed_currencies: list[str] = Field(default=[])
|
||||
lnbits_default_accounting_currency: str | None = Field(default=None)
|
||||
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
|
||||
lnbits_apple_touch_icon: str | None = Field(default=None)
|
||||
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
||||
lnbits_default_theme: str = Field(default="salvador")
|
||||
lnbits_default_border: str = Field(default="hard-border")
|
||||
@@ -620,9 +603,7 @@ class BoltzFundingSource(LNbitsSettings):
|
||||
boltz_client_wallet: str | None = Field(default="lnbits")
|
||||
boltz_client_password: str = Field(default="")
|
||||
boltz_client_cert: str | None = Field(default=None)
|
||||
boltz_mnemonic: str | None = Field(
|
||||
default_factory=generate_default_boltz_mnemonic
|
||||
)
|
||||
boltz_mnemonic: str | None = Field(default=None)
|
||||
|
||||
|
||||
class StrikeFundingSource(LNbitsSettings):
|
||||
@@ -660,20 +641,6 @@ class StripeFiatProvider(LNbitsSettings):
|
||||
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):
|
||||
lightning_invoice_expiry: int = Field(default=3600, gt=0)
|
||||
|
||||
@@ -709,7 +676,7 @@ class FundingSourcesSettings(
|
||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||
|
||||
|
||||
class FiatProvidersSettings(StripeFiatProvider, PayPalFiatProvider):
|
||||
class FiatProvidersSettings(StripeFiatProvider):
|
||||
def is_fiat_provider_enabled(self, provider: str | None) -> bool:
|
||||
"""
|
||||
Checks if a specific fiat provider is enabled.
|
||||
@@ -718,8 +685,7 @@ class FiatProvidersSettings(StripeFiatProvider, PayPalFiatProvider):
|
||||
return False
|
||||
if provider == "stripe":
|
||||
return self.stripe_enabled
|
||||
if provider == "paypal":
|
||||
return self.paypal_enabled
|
||||
# Add checks for other fiat providers here as needed
|
||||
return False
|
||||
|
||||
def get_fiat_providers_for_user(self, user_id: str) -> list[str]:
|
||||
@@ -733,12 +699,7 @@ class FiatProvidersSettings(StripeFiatProvider, PayPalFiatProvider):
|
||||
):
|
||||
allowed_providers.append("stripe")
|
||||
|
||||
if self.paypal_enabled and (
|
||||
not self.paypal_limits.allowed_users
|
||||
or user_id in self.paypal_limits.allowed_users
|
||||
):
|
||||
allowed_providers.append("paypal")
|
||||
|
||||
# Add other fiat providers here as needed
|
||||
return allowed_providers
|
||||
|
||||
def get_fiat_provider_limits(self, provider_name: str) -> FiatProviderLimits | None:
|
||||
@@ -795,7 +756,6 @@ class AuthSettings(LNbitsSettings):
|
||||
# How many seconds after login the user is allowed to update its credentials.
|
||||
# A fresh login is required afterwards.
|
||||
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):
|
||||
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
@@ -217,7 +217,7 @@ body.body--dark .q-header,
|
||||
body.body--dark .q-drawer {
|
||||
--q-dark: rgba(29, 29, 29, 0.3);
|
||||
background-color: var(--q-dark);
|
||||
backdrop-filter: blur(6px) brightness(0.8);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -455,12 +455,3 @@ video {
|
||||
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,8 +74,6 @@ window.localisation.en = {
|
||||
update_name: 'Update name',
|
||||
fiat_tracking: 'Fiat tracking',
|
||||
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',
|
||||
update_currency: 'Update currency',
|
||||
press_to_claim: 'Press to claim bitcoin',
|
||||
@@ -239,7 +237,6 @@ window.localisation.en = {
|
||||
webhook_url: 'Webhook URL',
|
||||
webhook_url_hint:
|
||||
'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_stripe_description:
|
||||
'One the stripe side you must configure a webhook with a URL that points to your LNbits server.',
|
||||
@@ -431,7 +428,6 @@ window.localisation.en = {
|
||||
look_and_feel: 'Look and Feel',
|
||||
endpoint: 'Endpoint',
|
||||
api: 'API',
|
||||
api_stripe: 'API',
|
||||
api_token: 'API Token',
|
||||
api_tokens: 'API Tokens',
|
||||
access_control_list: 'Access Control List',
|
||||
@@ -578,9 +574,6 @@ window.localisation.en = {
|
||||
authentication: 'Authentication',
|
||||
auth_token_expiry_label: 'Token expire minutes',
|
||||
auth_token_expiry_hint: 'Time in minutes until the token expires',
|
||||
auth_authentication_cache_label: 'Cache time (minutes)',
|
||||
auth_authentication_cache_hint:
|
||||
'Time in minutes to cache successful authentication (0 to disable)',
|
||||
auth_allowed_methods_label: 'Allowed authorization methods',
|
||||
auth_allowed_methods_hint: 'Select authorization methods',
|
||||
auth_nostr_label: 'Nostr Request URL',
|
||||
@@ -645,8 +638,6 @@ window.localisation.en = {
|
||||
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
||||
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
||||
ui_qr_code_logo_hint: 'QR code and favicon logo url',
|
||||
ui_apple_touch_icon: 'Apple Touch Icon',
|
||||
ui_apple_touch_icon_hint: 'Apple touch icon url',
|
||||
ui_custom_image: 'Custom Image',
|
||||
ui_custom_image_label: 'URL to custom image',
|
||||
ui_custom_image_hint: 'Image showed at homepage/login',
|
||||
@@ -708,15 +699,10 @@ window.localisation.en = {
|
||||
filter_labels: 'Filter labels',
|
||||
filter_date: 'Filter by date',
|
||||
websocket_example: 'Websocket example',
|
||||
client_id: 'Client ID',
|
||||
secret_key: 'Secret Key',
|
||||
signing_secret: 'Signing Secret',
|
||||
signing_secret_hint:
|
||||
'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_hint:
|
||||
'The user will be redirected to this URL after the payment is successful',
|
||||
@@ -730,14 +716,11 @@ window.localisation.en = {
|
||||
add_label: 'Add Label',
|
||||
label: 'Label',
|
||||
labels: 'Labels',
|
||||
label_filter: 'Label Filter',
|
||||
no_labels_defined: 'No labels defined yet',
|
||||
manage_labels: 'Manage Labels',
|
||||
update_label: 'Update Label',
|
||||
delete_label: 'Delete Label',
|
||||
add_remove_labels: 'Add or Remove Labels',
|
||||
payment_labels_updated: 'Payment labels updated',
|
||||
color: 'Color',
|
||||
sort: 'Sort',
|
||||
sort_by: 'Sort by'
|
||||
color: 'Color'
|
||||
}
|
||||
|
||||
@@ -628,15 +628,10 @@ window.localisation.fi = {
|
||||
filter_payments: 'Suodata maksuja',
|
||||
filter_date: 'Suodata päiväyksellä',
|
||||
websocket_example: 'Websocket example',
|
||||
client_id: 'Client ID',
|
||||
secret_key: 'Secret Key',
|
||||
signing_secret: 'Signing Secret',
|
||||
signing_secret_hint:
|
||||
'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_hint:
|
||||
'The user will be redirected to this URL after the payment is successful'
|
||||
|
||||
+11
-16
@@ -89,12 +89,6 @@ window._lnbitsApi = {
|
||||
}
|
||||
})
|
||||
},
|
||||
getAuthUser() {
|
||||
return axios({
|
||||
method: 'GET',
|
||||
url: '/api/v1/auth'
|
||||
})
|
||||
},
|
||||
login(username, password) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
@@ -129,12 +123,14 @@ window._lnbitsApi = {
|
||||
getWallet(wallet) {
|
||||
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
||||
},
|
||||
createWallet(name, walletType, opts = {}) {
|
||||
return this.request('post', '/api/v1/wallet', null, {
|
||||
createWallet(wallet, name, walletType, ops = {}) {
|
||||
return this.request('post', '/api/v1/wallet', wallet.adminkey, {
|
||||
name: name,
|
||||
wallet_type: walletType,
|
||||
...opts
|
||||
}).catch(LNbits.utils.notifyApiError)
|
||||
...ops
|
||||
}).then(res => {
|
||||
window.location = '/wallet?wal=' + res.data.id
|
||||
})
|
||||
},
|
||||
updateWallet(name, wallet) {
|
||||
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
||||
@@ -149,7 +145,11 @@ window._lnbitsApi = {
|
||||
)
|
||||
},
|
||||
deleteWallet(wallet) {
|
||||
return this.request('delete', `/api/v1/wallet/${wallet.id}`)
|
||||
return this.request('delete', `/api/v1/wallet/${wallet.id}`).then(_ => {
|
||||
let url = new URL(window.location.href)
|
||||
url.searchParams.delete('wal')
|
||||
window.location = url
|
||||
})
|
||||
},
|
||||
getPayments(wallet, params) {
|
||||
return this.request(
|
||||
@@ -171,10 +171,5 @@ window._lnbitsApi = {
|
||||
return this.request('GET', '/api/v1/currencies').then(response => {
|
||||
return ['sats', ...response.data]
|
||||
})
|
||||
},
|
||||
getDefaultSetting(fieldName) {
|
||||
return LNbits.api
|
||||
.request('GET', `/admin/api/v1/settings/default?field_name=${fieldName}`)
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,22 @@ window.LNbits = {
|
||||
g: window.g,
|
||||
utils: window._lnbitsUtils,
|
||||
api: window._lnbitsApi,
|
||||
events: {
|
||||
onInvoicePaid(wallet, cb) {
|
||||
ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`)
|
||||
ws.onmessage = ev => {
|
||||
const data = JSON.parse(ev.data)
|
||||
if (data.payment) {
|
||||
cb(data)
|
||||
}
|
||||
}
|
||||
return ws.onclose
|
||||
}
|
||||
},
|
||||
map: {
|
||||
user(data) {
|
||||
const obj = {
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
admin: data.admin,
|
||||
email: data.email,
|
||||
extensions: data.extensions,
|
||||
@@ -16,12 +27,16 @@ window.LNbits = {
|
||||
extra: data.extra ?? {}
|
||||
}
|
||||
const mapWallet = this.wallet
|
||||
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
||||
if (a.extra.pinned !== b.extra.pinned) {
|
||||
return a.extra.pinned ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
obj.wallets = obj.wallets
|
||||
.map(obj => {
|
||||
return mapWallet(obj)
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.extra.pinned !== b.extra.pinned) {
|
||||
return a.extra.pinned ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
obj.walletOptions = obj.wallets.map(obj => {
|
||||
return {
|
||||
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
||||
@@ -51,6 +66,9 @@ window.LNbits = {
|
||||
}
|
||||
newWallet.msat = data.balance_msat
|
||||
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') {
|
||||
const perms = newWallet.sharePermissions
|
||||
newWallet.canReceivePayments = perms.includes('receive-payments')
|
||||
@@ -59,6 +77,51 @@ window.LNbits = {
|
||||
newWallet.url = `/wallet?&wal=${data.id}`
|
||||
newWallet.storedPaylinks = data.stored_paylinks.links
|
||||
return newWallet
|
||||
},
|
||||
payment(data) {
|
||||
obj = {
|
||||
checking_id: data.checking_id,
|
||||
status: data.status,
|
||||
amount: data.amount,
|
||||
fee: data.fee,
|
||||
memo: data.memo,
|
||||
time: data.time,
|
||||
bolt11: data.bolt11,
|
||||
preimage: data.preimage,
|
||||
payment_hash: data.payment_hash,
|
||||
expiry: data.expiry,
|
||||
extra: data.extra ?? {},
|
||||
wallet_id: data.wallet_id,
|
||||
webhook: data.webhook,
|
||||
webhook_status: data.webhook_status,
|
||||
fiat_amount: data.fiat_amount,
|
||||
fiat_currency: data.fiat_currency,
|
||||
labels: data.labels
|
||||
}
|
||||
|
||||
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
|
||||
obj.dateFrom = moment.utc(data.created_at).local().fromNow()
|
||||
|
||||
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
|
||||
obj.expirydateFrom = moment.utc(obj.expiry).local().fromNow()
|
||||
obj.msat = obj.amount
|
||||
obj.sat = obj.msat / 1000
|
||||
obj.tag = obj.extra?.tag
|
||||
obj.fsat = new Intl.NumberFormat(window.i18n.global.locale).format(
|
||||
obj.sat
|
||||
)
|
||||
obj.isIn = obj.amount > 0
|
||||
obj.isOut = obj.amount < 0
|
||||
obj.isPending = obj.status === 'pending'
|
||||
obj.isPaid = obj.status === 'success'
|
||||
obj.isFailed = obj.status === 'failed'
|
||||
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
|
||||
try {
|
||||
obj.details = JSON.parse(data.extra?.details || '{}')
|
||||
} catch {
|
||||
obj.details = {extraDetails: data.extra?.details}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,11 @@ window.app.component('lnbits-payment-details', {
|
||||
template: '#lnbits-payment-details',
|
||||
props: ['payment'],
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPreimage() {
|
||||
return (
|
||||
@@ -111,11 +116,11 @@ window.app.component('lnbits-lnurlpay-success-action', {
|
||||
},
|
||||
mounted() {
|
||||
if (this.success_action.tag !== 'aes') return null
|
||||
this.utils
|
||||
.decryptLnurlPayAES(this.success_action, this.payment.preimage)
|
||||
.then(value => {
|
||||
decryptLnurlPayAES(this.success_action, this.payment.preimage).then(
|
||||
value => {
|
||||
this.decryptedValue = value
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -359,8 +364,11 @@ window.app.component('lnbits-update-balance', {
|
||||
mixins: [window.windowMixin],
|
||||
props: ['wallet_id', 'small_btn'],
|
||||
computed: {
|
||||
denomination() {
|
||||
return LNBITS_DENOMINATION
|
||||
},
|
||||
admin() {
|
||||
return this.g.user?.super_user === true
|
||||
return user.super_user
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -700,7 +708,7 @@ window.app.component('lnbits-node-qrcode', {
|
||||
dense
|
||||
unelevated
|
||||
size="md"
|
||||
@click="utils.copyText(info.id)"
|
||||
@click="copyText(info.id)"
|
||||
>Public Key<q-tooltip> Click to copy </q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
@@ -749,6 +757,24 @@ 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', {
|
||||
props: ['info'],
|
||||
data() {
|
||||
@@ -781,7 +807,7 @@ window.app.component('lnbits-node-info', {
|
||||
flat
|
||||
dense
|
||||
icon='content_paste'
|
||||
@click='utils.copyText(info.id)'
|
||||
@click='copyText(info.id)'
|
||||
></q-btn>
|
||||
<q-btn
|
||||
size='xs'
|
||||
|
||||
@@ -70,11 +70,6 @@ window.app.component('lnbits-admin-exchange-providers', {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getDefaultSetting(fieldName) {
|
||||
LNbits.api.getDefaultSetting(fieldName).then(response => {
|
||||
this.formData[fieldName] = response.data.default_value
|
||||
})
|
||||
},
|
||||
getExchangeRateHistory() {
|
||||
LNbits.api
|
||||
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
|
||||
@@ -129,7 +124,7 @@ window.app.component('lnbits-admin-exchange-providers', {
|
||||
},
|
||||
initExchangeChart(data) {
|
||||
const xValues = data.map(d =>
|
||||
this.utils.formatTimestamp(d.timestamp, 'HH:mm')
|
||||
Quasar.date.formatDate(new Date(d.timestamp * 1000), 'HH:mm')
|
||||
)
|
||||
const exchanges = [
|
||||
...this.formData.lnbits_exchange_rate_providers,
|
||||
|
||||
@@ -5,79 +5,10 @@ window.app.component('lnbits-admin-fiat-providers', {
|
||||
data() {
|
||||
return {
|
||||
formAddStripeUser: '',
|
||||
formAddPaypalUser: '',
|
||||
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: {
|
||||
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() {
|
||||
const addUser = this.formAddStripeUser || ''
|
||||
if (
|
||||
@@ -95,23 +26,6 @@ window.app.component('lnbits-admin-fiat-providers', {
|
||||
this.formData.stripe_limits.allowed_users =
|
||||
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) {
|
||||
LNbits.api
|
||||
.request('PUT', `/api/v1/fiat/check/${providerName}`)
|
||||
|
||||
@@ -161,21 +161,15 @@ window.app.component('lnbits-admin-funding-sources', {
|
||||
'Boltz',
|
||||
{
|
||||
boltz_client_endpoint: 'Endpoint',
|
||||
boltz_client_macaroon: {
|
||||
label: 'Admin Macaroon path or hex',
|
||||
default: '/home/ubuntu/.boltz/macaroons/admin.macaroon'
|
||||
},
|
||||
boltz_client_cert: {
|
||||
label: 'Certificate path or hex',
|
||||
default: '/home/ben/.boltz/tls.cert'
|
||||
},
|
||||
boltz_client_macaroon: 'Admin Macaroon path or hex',
|
||||
boltz_client_cert: 'Certificate path or hex',
|
||||
boltz_client_wallet: 'Wallet Name',
|
||||
boltz_client_password: 'Wallet Password (can be empty)',
|
||||
boltz_mnemonic: {
|
||||
label: 'Liquid mnemonic (copy into greenwallet)',
|
||||
readonly: true,
|
||||
copy: true,
|
||||
qrcode: true,
|
||||
hint: 'Paste or type your liquid mnemonic'
|
||||
qrcode: true
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
window.app.component('lnbits-admin-server', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-server',
|
||||
mixins: [window.windowMixin]
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
currencies: []
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.currencies = await LNbits.api.getCurrencies()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
window.app.component('lnbits-error', {
|
||||
template: '#lnbits-error',
|
||||
mixins: [window.windowMixin],
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -9,7 +9,10 @@ window.app.component('lnbits-footer', {
|
||||
return `${this.SITE_TITLE}, ${this.SITE_TAGLINE}`
|
||||
},
|
||||
showFooter() {
|
||||
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||
return (
|
||||
this.SITE_TITLE == 'LNbits' &&
|
||||
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,6 +2,9 @@ window.app.component('lnbits-header', {
|
||||
template: '#lnbits-header',
|
||||
mixins: [window.windowMixin],
|
||||
computed: {
|
||||
denomination() {
|
||||
return this.LNBITS_DENOMINATION || 'sat'
|
||||
},
|
||||
hasServiceFeeMax() {
|
||||
return (
|
||||
this.g.user &&
|
||||
@@ -38,28 +41,17 @@ window.app.component('lnbits-header', {
|
||||
customLogoUrl() {
|
||||
return this.USE_CUSTOM_LOGO || null
|
||||
},
|
||||
userPictureUrl() {
|
||||
return this.g.user.config.picture
|
||||
},
|
||||
hasUserPicture() {
|
||||
return this.g.user && this.g.user.config && this.g.user.config.picture
|
||||
},
|
||||
showAdmin() {
|
||||
return this.g.user && (this.g.user.super_user || this.g.user.admin)
|
||||
},
|
||||
showVoidwallet() {
|
||||
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: {
|
||||
showLogos() {
|
||||
return (
|
||||
this.g.isSatsDenomination &&
|
||||
this.LNBITS_DENOMINATION == 'sats' &&
|
||||
this.SITE_TITLE == 'LNbits' &&
|
||||
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||
)
|
||||
|
||||
@@ -9,18 +9,27 @@ window.app.component('lnbits-manage-extension-list', {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'g.user.extensions'() {
|
||||
this.loadExtensions()
|
||||
'g.user.extensions': {
|
||||
async handler() {
|
||||
await this.loadExtensions()
|
||||
}
|
||||
},
|
||||
searchTerm() {
|
||||
this.filterUserExtensionsByTerm()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
map(data) {
|
||||
const obj = {...data}
|
||||
obj.url = ['/', obj.code, '/'].join('')
|
||||
return obj
|
||||
},
|
||||
async loadExtensions() {
|
||||
try {
|
||||
res = await LNbits.api.request('GET', '/api/v1/extension')
|
||||
this.extensions = res.data.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
|
||||
this.extensions = data
|
||||
.map(extension => this.map(extension))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
this.filterUserExtensionsByTerm()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
@@ -36,6 +45,10 @@ window.app.component('lnbits-manage-extension-list', {
|
||||
.toLocaleLowerCase()
|
||||
.includes(this.searchTerm.toLocaleLowerCase())
|
||||
})
|
||||
.map(obj => {
|
||||
obj.isActive = window.location.pathname.startsWith(obj.url)
|
||||
return obj
|
||||
})
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
|
||||
@@ -3,90 +3,7 @@ window.app.component('lnbits-manage-wallet-list', {
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
walletName: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
window.app.component('lnbits-payment-list', {
|
||||
template: '#lnbits-payment-list',
|
||||
props: ['wallet', 'paymentFilter'],
|
||||
props: ['update', 'lazy', 'wallet', 'paymentFilter'],
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
denomination: LNBITS_DENOMINATION,
|
||||
payments: [],
|
||||
paymentsTable: {
|
||||
columns: [
|
||||
@@ -17,7 +18,7 @@ window.app.component('lnbits-payment-list', {
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'right',
|
||||
label: this.$t('amount'),
|
||||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
||||
field: 'sat',
|
||||
sortable: true
|
||||
}
|
||||
@@ -29,13 +30,6 @@ window.app.component('lnbits-payment-list', {
|
||||
descending: true,
|
||||
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: '',
|
||||
loading: false
|
||||
},
|
||||
@@ -52,10 +46,10 @@ window.app.component('lnbits-payment-list', {
|
||||
paymentsCSV: {
|
||||
columns: [
|
||||
{
|
||||
name: 'status',
|
||||
align: 'right',
|
||||
label: this.$t('status'),
|
||||
field: 'status'
|
||||
name: 'pending',
|
||||
align: 'left',
|
||||
label: 'Pending',
|
||||
field: 'pending'
|
||||
},
|
||||
{
|
||||
name: 'memo',
|
||||
@@ -73,14 +67,14 @@ window.app.component('lnbits-payment-list', {
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'right',
|
||||
label: this.$t('amount'),
|
||||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
||||
field: 'sat',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'fee',
|
||||
align: 'right',
|
||||
label: this.$t('fee'),
|
||||
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
|
||||
field: 'fee'
|
||||
},
|
||||
{
|
||||
@@ -133,6 +127,9 @@ window.app.component('lnbits-payment-list', {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentWallet() {
|
||||
return this.wallet || this.g.wallet
|
||||
},
|
||||
filteredPayments() {
|
||||
const q = this.paymentsTable.search
|
||||
if (!q || q === '') return this.payments
|
||||
@@ -150,47 +147,6 @@ window.app.component('lnbits-payment-list', {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mapPayment(data) {
|
||||
const obj = {
|
||||
checking_id: data.checking_id,
|
||||
status: data.status,
|
||||
amount: data.amount,
|
||||
fee: data.fee,
|
||||
memo: data.memo,
|
||||
time: data.time,
|
||||
bolt11: data.bolt11,
|
||||
preimage: data.preimage,
|
||||
payment_hash: data.payment_hash,
|
||||
expiry: data.expiry,
|
||||
extra: data.extra ?? {},
|
||||
wallet_id: data.wallet_id,
|
||||
webhook: data.webhook,
|
||||
webhook_status: data.webhook_status,
|
||||
fiat_amount: data.fiat_amount,
|
||||
fiat_currency: data.fiat_currency,
|
||||
labels: data.labels
|
||||
}
|
||||
obj.date = this.utils.formatDate(data.created_at)
|
||||
obj.dateFrom = this.utils.formatDateFrom(data.created_at)
|
||||
obj.expirydate = this.utils.formatDate(data.expiry)
|
||||
obj.expirydateFrom = this.utils.formatDateFrom(data.expiry)
|
||||
obj.msat = obj.amount
|
||||
obj.sat = obj.msat / 1000
|
||||
obj.tag = obj.extra?.tag
|
||||
obj.fsat = this.utils.formatSat(obj.sat)
|
||||
obj.isIn = obj.amount > 0
|
||||
obj.isOut = obj.amount < 0
|
||||
obj.isPending = obj.status === 'pending'
|
||||
obj.isPaid = obj.status === 'success'
|
||||
obj.isFailed = obj.status === 'failed'
|
||||
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
|
||||
try {
|
||||
obj.details = JSON.parse(data.extra?.details || '{}')
|
||||
} catch {
|
||||
obj.details = {extraDetails: data.extra?.details}
|
||||
}
|
||||
return obj
|
||||
},
|
||||
searchByDate() {
|
||||
if (typeof this.searchDate === 'string') {
|
||||
this.searchDate = {
|
||||
@@ -204,6 +160,7 @@ window.app.component('lnbits-payment-list', {
|
||||
if (this.searchDate.to) {
|
||||
this.paymentFilter['time[le]'] = this.searchDate.to + 'T23:59:59'
|
||||
}
|
||||
|
||||
this.fetchPayments()
|
||||
},
|
||||
searchByLabels(labels) {
|
||||
@@ -227,39 +184,30 @@ window.app.component('lnbits-payment-list', {
|
||||
this.fetchPayments()
|
||||
},
|
||||
fetchPayments(props) {
|
||||
this.paymentsTable.loading = true
|
||||
const params = LNbits.utils.prepareFilterQuery(
|
||||
this.paymentsTable,
|
||||
props,
|
||||
this.paymentFilter
|
||||
)
|
||||
this.paymentsTable.loading = true
|
||||
return LNbits.api
|
||||
.getPayments(this.wallet, params)
|
||||
.getPayments(this.currentWallet, params)
|
||||
.then(response => {
|
||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||
this.payments = response.data.data.map(this.mapPayment)
|
||||
this.paymentsTable.loading = false
|
||||
this.recheckPendingPayments()
|
||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||
this.payments = response.data.data.map(obj => {
|
||||
return LNbits.map.payment(obj)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
this.paymentsTable.loading = false
|
||||
if (g.user.admin) {
|
||||
this.fetchPaymentsAsAdmin(this.wallet.id, params)
|
||||
this.fetchPaymentsAsAdmin(this.currentWallet.id, params)
|
||||
} else {
|
||||
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) {
|
||||
params = (params || '') + '&wallet_id=' + walletId
|
||||
return LNbits.api
|
||||
@@ -267,7 +215,9 @@ window.app.component('lnbits-payment-list', {
|
||||
.then(response => {
|
||||
this.paymentsTable.loading = false
|
||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||
this.payments = response.data.data.map(this.mapPayment)
|
||||
this.payments = response.data.data.map(obj => {
|
||||
return LNbits.map.payment(obj)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
this.paymentsTable.loading = false
|
||||
@@ -276,7 +226,7 @@ window.app.component('lnbits-payment-list', {
|
||||
},
|
||||
checkPayment(payment_hash) {
|
||||
LNbits.api
|
||||
.getPayment(this.wallet, payment_hash)
|
||||
.getPayment(this.g.wallet, payment_hash)
|
||||
.then(res => {
|
||||
this.update = !this.update
|
||||
if (res.data.status == 'success') {
|
||||
@@ -294,41 +244,6 @@ window.app.component('lnbits-payment-list', {
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
recheckPendingPayments() {
|
||||
const pendingPayments = this.payments.filter(p => p.status === 'pending')
|
||||
if (pendingPayments.length === 0) return
|
||||
|
||||
const params = [
|
||||
'recheck_pending=true',
|
||||
'checking_id[in]=' + pendingPayments.map(p => p.checking_id).join(',')
|
||||
].join('&')
|
||||
|
||||
LNbits.api
|
||||
.getPayments(this.wallet, params)
|
||||
.then(response => {
|
||||
let updatedPayments = 0
|
||||
response.data.data.forEach(updatedPayment => {
|
||||
if (updatedPayment.status !== 'pending') {
|
||||
const index = this.payments.findIndex(
|
||||
p => p.checking_id === updatedPayment.checking_id
|
||||
)
|
||||
if (index !== -1) {
|
||||
this.payments.splice(index, 1, this.mapPayment(updatedPayment))
|
||||
updatedPayments += 1
|
||||
}
|
||||
}
|
||||
})
|
||||
if (updatedPayments > 0) {
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: this.$t('payment_successful')
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn(err)
|
||||
})
|
||||
},
|
||||
showHoldInvoiceDialog(payment) {
|
||||
this.hodlInvoice.show = true
|
||||
this.hodlInvoice.preimage = ''
|
||||
@@ -336,7 +251,7 @@ window.app.component('lnbits-payment-list', {
|
||||
},
|
||||
cancelHoldInvoice(payment_hash) {
|
||||
LNbits.api
|
||||
.cancelInvoice(this.wallet, payment_hash)
|
||||
.cancelInvoice(this.g.wallet, payment_hash)
|
||||
.then(() => {
|
||||
this.update = !this.update
|
||||
Quasar.Notify.create({
|
||||
@@ -348,7 +263,7 @@ window.app.component('lnbits-payment-list', {
|
||||
},
|
||||
settleHoldInvoice(preimage) {
|
||||
LNbits.api
|
||||
.settleInvoice(this.wallet, preimage)
|
||||
.settleInvoice(this.g.wallet, preimage)
|
||||
.then(() => {
|
||||
this.update = !this.update
|
||||
Quasar.Notify.create({
|
||||
@@ -371,8 +286,8 @@ window.app.component('lnbits-payment-list', {
|
||||
direction: pagination.descending ? 'desc' : 'asc'
|
||||
}
|
||||
const params = new URLSearchParams(query)
|
||||
LNbits.api.getPayments(this.wallet, params).then(response => {
|
||||
let payments = response.data.data.map(this.mapPayment)
|
||||
LNbits.api.getPayments(this.g.wallet, params).then(response => {
|
||||
let payments = response.data.data.map(LNbits.map.payment)
|
||||
let columns = this.paymentsCSV.columns
|
||||
|
||||
if (detailed) {
|
||||
@@ -399,7 +314,7 @@ window.app.component('lnbits-payment-list', {
|
||||
LNbits.utils.exportCSV(
|
||||
columns,
|
||||
payments,
|
||||
this.wallet.name + '-payments'
|
||||
this.g.wallet.name + '-payments'
|
||||
)
|
||||
})
|
||||
},
|
||||
@@ -468,7 +383,7 @@ window.app.component('lnbits-payment-list', {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/payments/${this.selectedPayment.payment_hash}/labels`,
|
||||
this.wallet.adminkey,
|
||||
this.g.wallet.adminkey,
|
||||
{
|
||||
labels: labels
|
||||
}
|
||||
@@ -508,11 +423,23 @@ window.app.component('lnbits-payment-list', {
|
||||
this.fetchPayments()
|
||||
}
|
||||
},
|
||||
lazy(newVal) {
|
||||
if (newVal === true) this.fetchPayments()
|
||||
},
|
||||
update() {
|
||||
this.fetchPayments()
|
||||
},
|
||||
'g.updatePayments'() {
|
||||
this.fetchPayments()
|
||||
},
|
||||
'g.wallet': {
|
||||
handler(newWallet) {
|
||||
this.fetchPayments()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchPayments()
|
||||
if (this.lazy === undefined) this.fetchPayments()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
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],
|
||||
template: '#lnbits-qrcode',
|
||||
components: {
|
||||
QrcodeVue: QrcodeVue.default
|
||||
QrcodeVue
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
@@ -13,10 +13,6 @@ window.app.component('lnbits-qrcode', {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
print: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showButtons: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
@@ -45,44 +41,9 @@ window.app.component('lnbits-qrcode', {
|
||||
}
|
||||
},
|
||||
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) {
|
||||
if (this.href === '') {
|
||||
this.utils.copyText(this.value)
|
||||
this.copyText(this.value)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return false
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
window.app.component('lnbits-theme', {
|
||||
mixins: [window.windowMixin],
|
||||
watch: {
|
||||
'g.walletFlip'(val) {
|
||||
this.$q.localStorage.setItem('lnbits.walletFlip', val)
|
||||
if (val === true && this.$q.screen.lt.md) {
|
||||
this.g.visibleDrawer = false
|
||||
}
|
||||
},
|
||||
'g.disclaimerShown'(val) {
|
||||
this.$q.localStorage.setItem('lnbits.disclaimerShown', val)
|
||||
},
|
||||
|
||||
@@ -39,7 +39,6 @@ window.app.component('lnbits-wallet-api-docs', {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
origin: window.location.origin,
|
||||
inkeyHidden: true,
|
||||
adminkeyHidden: true,
|
||||
walletIdHidden: true
|
||||
|
||||
@@ -102,62 +102,63 @@ window.app.component('lnbits-wallet-charts', {
|
||||
if (this.walletBalanceInOut) {
|
||||
this.walletBalanceInOut.destroy()
|
||||
}
|
||||
const ref = this.$refs.walletBalanceInOut
|
||||
if (!ref) return
|
||||
this.walletBalanceInOut = new Chart(ref.getContext('2d'), {
|
||||
type: 'bar',
|
||||
options: this.barOptions,
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Balance In',
|
||||
borderRadius: 5,
|
||||
data: data.map(s => s.balance_in),
|
||||
backgroundColor: this.colorPrimary
|
||||
},
|
||||
{
|
||||
label: 'Balance Out',
|
||||
borderRadius: 5,
|
||||
data: data.map(s => s.balance_out),
|
||||
backgroundColor: this.colorSecondary
|
||||
}
|
||||
]
|
||||
this.walletBalanceInOut = new Chart(
|
||||
this.$refs.walletBalanceInOut.getContext('2d'),
|
||||
{
|
||||
type: 'bar',
|
||||
options: this.barOptions,
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Balance In',
|
||||
borderRadius: 5,
|
||||
data: data.map(s => s.balance_in),
|
||||
backgroundColor: this.colorPrimary
|
||||
},
|
||||
{
|
||||
label: 'Balance Out',
|
||||
borderRadius: 5,
|
||||
data: data.map(s => s.balance_out),
|
||||
backgroundColor: this.colorSecondary
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
drawPaymentInOut(data, labels) {
|
||||
if (this.walletPaymentInOut) {
|
||||
this.walletPaymentInOut.destroy()
|
||||
}
|
||||
const ref = this.$refs.walletPaymentInOut
|
||||
if (!ref) return
|
||||
this.walletPaymentInOut = new Chart(ref.getContext('2d'), {
|
||||
type: 'bar',
|
||||
options: this.barOptions,
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Payments In',
|
||||
data: data.map(s => s.count_in),
|
||||
backgroundColor: this.colorPrimary
|
||||
},
|
||||
{
|
||||
label: 'Payments Out',
|
||||
data: data.map(s => -s.count_out),
|
||||
backgroundColor: this.colorSecondary
|
||||
}
|
||||
]
|
||||
this.walletPaymentInOut = new Chart(
|
||||
this.$refs.walletPaymentInOut.getContext('2d'),
|
||||
{
|
||||
type: 'bar',
|
||||
options: this.barOptions,
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Payments In',
|
||||
data: data.map(s => s.count_in),
|
||||
backgroundColor: this.colorPrimary
|
||||
},
|
||||
{
|
||||
label: 'Payments Out',
|
||||
data: data.map(s => -s.count_out),
|
||||
backgroundColor: this.colorSecondary
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
drawBalanceChart(data, labels) {
|
||||
const ref = this.$refs.walletBalanceChart
|
||||
if (this.walletBalanceChart) {
|
||||
this.walletBalanceChart.destroy()
|
||||
}
|
||||
const ref = this.$refs.walletBalanceChart
|
||||
if (!ref) return
|
||||
this.walletBalanceChart = new Chart(ref.getContext('2d'), {
|
||||
type: 'line',
|
||||
options: {
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
window.app.component('lnbits-wallet-extra', {
|
||||
template: '#lnbits-wallet-extra',
|
||||
mixins: [window.windowMixin],
|
||||
props: ['chartConfig'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
handleSendLnurl(lnurl) {
|
||||
this.$emit('send-lnurl', lnurl)
|
||||
},
|
||||
updateWallet(wallet) {
|
||||
this.$emit('update-wallet', wallet)
|
||||
},
|
||||
handleFiatTracking() {
|
||||
this.g.fiatTracking = !this.g.fiatTracking
|
||||
if (!this.g.fiatTracking) {
|
||||
this.g.isFiatPriority = false
|
||||
this.g.wallet.currency = ''
|
||||
this.updateWallet({currency: ''})
|
||||
} else {
|
||||
this.updateWallet({currency: this.g.wallet.currency})
|
||||
this.updateFiatBalance()
|
||||
}
|
||||
},
|
||||
deleteWallet() {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||
.onOk(() => {
|
||||
LNbits.api
|
||||
.deleteWallet(this.g.wallet)
|
||||
.then(() => {
|
||||
this.g.user.wallets = this.g.user.wallets.filter(
|
||||
w => w.id !== this.g.wallet.id
|
||||
)
|
||||
this.g.lastActiveWallet = this.g.user.wallets[0].id
|
||||
this.$router.push(`/wallet/${this.g.lastActiveWallet}`)
|
||||
Quasar.Notify.create({
|
||||
timeout: 3000,
|
||||
message: `Wallet deleted!`,
|
||||
spinner: true
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
updateFiatBalance() {
|
||||
// set rate from local storage to avoid clunky api calls
|
||||
if (
|
||||
this.$q.localStorage.getItem(
|
||||
'lnbits.exchangeRate.' + this.g.wallet.currency
|
||||
)
|
||||
) {
|
||||
this.g.exchangeRate = this.$q.localStorage.getItem(
|
||||
'lnbits.exchangeRate.' + this.g.wallet.currency
|
||||
)
|
||||
this.g.fiatBalance =
|
||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||
}
|
||||
LNbits.api
|
||||
.request('GET', `/api/v1/rate/` + this.g.wallet.currency, null)
|
||||
.then(response => {
|
||||
this.g.fiatBalance =
|
||||
(response.data.price / 100000000) * this.g.wallet.sat
|
||||
this.g.exchangeRate = response.data.price.toFixed(2)
|
||||
this.g.fiatTracking = true
|
||||
this.$q.localStorage.set(
|
||||
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
||||
this.g.exchangeRate
|
||||
)
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.g.wallet.currency !== '' && this.g.isSatsDenomination) {
|
||||
this.g.fiatTracking = true
|
||||
this.updateFiatBalance()
|
||||
} else {
|
||||
this.g.fiatTracking = false
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -4,27 +4,10 @@ window.app.component('lnbits-wallet-new', {
|
||||
data() {
|
||||
return {
|
||||
walletTypes: [{label: 'Lightning Wallet', value: 'lightning'}],
|
||||
wallet: {name: '', sharedWalletId: ''},
|
||||
showNewWalletDialog: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'g.newWalletType'(val) {
|
||||
if (val === null) return
|
||||
this.showNewWalletDialog = true
|
||||
},
|
||||
showNewWalletDialog(val) {
|
||||
if (val === true) return
|
||||
this.reset()
|
||||
newWallet: {name: '', sharedWalletId: ''}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLightning() {
|
||||
return this.g.newWalletType === 'lightning'
|
||||
},
|
||||
isLightningShared() {
|
||||
return this.g.newWalletType === 'lightning-shared'
|
||||
},
|
||||
inviteWalletOptions() {
|
||||
return (this.g.user?.extra?.wallet_invite_requests || []).map(i => ({
|
||||
label: `${i.to_wallet_name} (from ${i.from_user_name})`,
|
||||
@@ -33,16 +16,11 @@ window.app.component('lnbits-wallet-new', {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.showNewWalletDialog = false
|
||||
this.g.newWalletType = null
|
||||
this.wallet = {name: '', sharedWalletId: ''}
|
||||
},
|
||||
async submitRejectWalletInvitation() {
|
||||
try {
|
||||
const inviteRequests = this.g.user.extra.wallet_invite_requests || []
|
||||
const invite = inviteRequests.find(
|
||||
invite => invite.to_wallet_id === this.wallet.sharedWalletId
|
||||
invite => invite.to_wallet_id === this.newWallet.sharedWalletId
|
||||
)
|
||||
if (!invite) {
|
||||
Quasar.Notify.create({
|
||||
@@ -68,8 +46,8 @@ window.app.component('lnbits-wallet-new', {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
},
|
||||
submitAddWallet() {
|
||||
const data = this.wallet
|
||||
async submitAddWallet() {
|
||||
const data = this.newWallet
|
||||
if (this.g.newWalletType === 'lightning' && !data.name) {
|
||||
this.$q.notify({
|
||||
message: 'Please enter a name for the wallet',
|
||||
@@ -77,6 +55,7 @@ window.app.component('lnbits-wallet-new', {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (this.g.newWalletType === 'lightning-shared' && !data.sharedWalletId) {
|
||||
this.$q.notify({
|
||||
message: 'Missing a shared wallet ID',
|
||||
@@ -84,20 +63,19 @@ window.app.component('lnbits-wallet-new', {
|
||||
})
|
||||
return
|
||||
}
|
||||
LNbits.api
|
||||
.createWallet(data.name, this.g.newWalletType, {
|
||||
shared_wallet_id: data.sharedWalletId
|
||||
})
|
||||
.then(res => {
|
||||
this.$q.notify({
|
||||
message: 'Wallet created successfully',
|
||||
color: 'positive'
|
||||
})
|
||||
this.reset()
|
||||
this.g.user.wallets.push(LNbits.map.wallet(res.data))
|
||||
this.g.lastWalletId = res.data.id
|
||||
this.$router.push(`/wallet/${res.data.id}`)
|
||||
})
|
||||
try {
|
||||
await LNbits.api.createWallet(
|
||||
this.g.user.wallets[0],
|
||||
data.name,
|
||||
this.g.newWalletType,
|
||||
{
|
||||
shared_wallet_id: data.sharedWalletId
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
@@ -15,6 +15,10 @@ window.app.component('lnbits-wallet-paylinks', {
|
||||
this.storedPaylinks = this.g.wallet.storedPaylinks
|
||||
},
|
||||
methods: {
|
||||
dateFromNow(unix) {
|
||||
const date = new Date(unix * 1000)
|
||||
return moment.utc(date).local().fromNow()
|
||||
},
|
||||
updatePaylinks() {
|
||||
LNbits.api
|
||||
.request(
|
||||
|
||||
@@ -26,7 +26,7 @@ window.app.component('lnbits-wallet-share', {
|
||||
{label: 'Send', value: 'send-payments'}
|
||||
],
|
||||
walletShareInvite: {
|
||||
username: '',
|
||||
unsername: '',
|
||||
permissions: []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,6 +473,7 @@ function confettiStars() {
|
||||
F = t.width * I.x,
|
||||
N = t.height * I.y;
|
||||
E--;
|
||||
|
||||
)
|
||||
S.push(
|
||||
((o = {
|
||||
|
||||
@@ -9,31 +9,27 @@ const localStore = (key, defaultValue) => {
|
||||
}
|
||||
|
||||
window.g = Vue.reactive({
|
||||
errorCode: null,
|
||||
errorMessage: null,
|
||||
user: null,
|
||||
wallet: null,
|
||||
isPublicPage: true,
|
||||
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
||||
offline: !navigator.onLine,
|
||||
hasCamera: false,
|
||||
visibleDrawer: false,
|
||||
extensions: WINDOW_SETTINGS.EXTENSIONS,
|
||||
extensions: [],
|
||||
user: null,
|
||||
wallet: {},
|
||||
fiatBalance: 0,
|
||||
exchangeRate: 0,
|
||||
fiatTracking: false,
|
||||
wallets: [],
|
||||
payments: [],
|
||||
walletEventListeners: [],
|
||||
updatePayments: false, // used for updating the lnbits-payment-list
|
||||
updatePaymentsHash: false, // used for closing the receive dialog
|
||||
currencies: WINDOW_SETTINGS.LNBITS_CURRENCIES ?? [],
|
||||
allowedCurrencies: WINDOW_SETTINGS.LNBITS_ALLOWED_CURRENCIES ?? [],
|
||||
showNewWalletDialog: false,
|
||||
newWalletType: 'lightning',
|
||||
updatePayments: false,
|
||||
updatePaymentsHash: '',
|
||||
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
|
||||
disclaimerShown: localStore('lnbits.disclaimerShown', false),
|
||||
isFiatPriority: localStore('lnbits.isFiatPriority', false),
|
||||
mobileSimple: localStore('lnbits.mobileSimple', true),
|
||||
walletFlip: localStore('lnbits.walletFlip', false),
|
||||
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
|
||||
darkChoice: localStore('lnbits.darkMode', true),
|
||||
themeChoice: localStore('lnbits.theme', WINDOW_SETTINGS.LNBITS_DEFAULT_THEME),
|
||||
borderChoice: localStore(
|
||||
@@ -54,9 +50,7 @@ window.g = Vue.reactive({
|
||||
),
|
||||
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
|
||||
denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION,
|
||||
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats',
|
||||
scanner: null,
|
||||
newWalletType: null
|
||||
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats'
|
||||
})
|
||||
|
||||
window.dateFormat = 'YYYY-MM-DD HH:mm'
|
||||
@@ -82,9 +76,3 @@ if (navigator.serviceWorker != null) {
|
||||
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')
|
||||
})
|
||||
}
|
||||
|
||||
+157
-88
@@ -1,53 +1,144 @@
|
||||
const quasarConfig = {
|
||||
config: {
|
||||
loading: {
|
||||
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
|
||||
props: {
|
||||
fetchUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
scripts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
keys: []
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadDynamicContent()
|
||||
},
|
||||
methods: {
|
||||
async loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const existingScript = document.querySelector(`script[src="${src}"]`)
|
||||
if (existingScript) {
|
||||
existingScript.remove()
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.src = src
|
||||
script.async = true
|
||||
script.onload = resolve
|
||||
script.onerror = () =>
|
||||
reject(new Error(`Failed to load script: ${src}`))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
},
|
||||
async loadDynamicContent() {
|
||||
this.$q.loading.show()
|
||||
try {
|
||||
const cleanUrl = this.fetchUrl.split('#')[0]
|
||||
//grab page content, need to be before loading scripts
|
||||
const response = await fetch(cleanUrl, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Accept: 'text/html',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
|
||||
const html = await response.text()
|
||||
|
||||
// load window variables
|
||||
const parser = new DOMParser()
|
||||
const htmlDocument = parser.parseFromString(html, 'text/html')
|
||||
const inlineScript = htmlDocument.querySelector('#window-vars-script')
|
||||
if (inlineScript) {
|
||||
new Function(inlineScript.innerHTML)() // Execute the script
|
||||
}
|
||||
|
||||
//load scripts defined in the route
|
||||
await this.loadScript('/static/js/base.js')
|
||||
for (const script of this.scripts) {
|
||||
await this.loadScript(script)
|
||||
}
|
||||
|
||||
//housecleaning, remove old component
|
||||
const previousRouteName =
|
||||
this.$router.currentRoute.value.meta.previousRouteName
|
||||
if (
|
||||
previousRouteName &&
|
||||
window.app._context.components[previousRouteName]
|
||||
) {
|
||||
delete window.app._context.components[previousRouteName]
|
||||
}
|
||||
//load component logic
|
||||
const logicKey = `${this.$route.name}PageLogic`
|
||||
const componentLogic = window[logicKey]
|
||||
|
||||
if (!componentLogic) {
|
||||
throw new Error(
|
||||
`Component logic '${logicKey}' not found. Ensure it is defined in the script.`
|
||||
)
|
||||
}
|
||||
|
||||
//Add mixins
|
||||
componentLogic.mixins = componentLogic.mixins || []
|
||||
if (window.windowMixin) {
|
||||
componentLogic.mixins.push(window.windowMixin)
|
||||
}
|
||||
|
||||
//Build component
|
||||
window.app.component(this.$route.name, {
|
||||
...componentLogic,
|
||||
template: html // Use the fetched HTML as the template
|
||||
})
|
||||
delete window[logicKey] //dont need this anymore
|
||||
this.$forceUpdate()
|
||||
} catch (error) {
|
||||
console.error('Error loading dynamic content:', error)
|
||||
} finally {
|
||||
this.$q.loading.hide()
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
const validRouteNames = routes.map(route => route.name)
|
||||
if (validRouteNames.includes(to.name)) {
|
||||
this.$router.currentRoute.value.meta.previousRouteName = from.name
|
||||
this.loadDynamicContent()
|
||||
} else {
|
||||
console.log(
|
||||
`Route '${to.name}' is not valid. Leave this one to Fastapi.`
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<component :is="$route.name"></component>
|
||||
`
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/wallet',
|
||||
name: 'Wallet',
|
||||
component: DynamicComponent,
|
||||
props: route => {
|
||||
let fetchUrl = '/wallet'
|
||||
if (Object.keys(route.query).length > 0) {
|
||||
fetchUrl += '?'
|
||||
for (const [key, value] of Object.entries(route.query)) {
|
||||
fetchUrl += `${key}=${value}&`
|
||||
}
|
||||
fetchUrl = fetchUrl.slice(0, -1) // remove last &
|
||||
}
|
||||
return {
|
||||
fetchUrl,
|
||||
scripts: ['/static/js/wallet.js']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/node',
|
||||
name: 'Node',
|
||||
@@ -68,19 +159,6 @@ const routes = [
|
||||
name: 'Audit',
|
||||
component: PageAudit
|
||||
},
|
||||
{
|
||||
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',
|
||||
component: PageWallet
|
||||
},
|
||||
{
|
||||
path: '/wallets',
|
||||
name: 'Wallets',
|
||||
@@ -120,16 +198,6 @@ const routes = [
|
||||
path: '/',
|
||||
name: 'PageHome',
|
||||
component: PageHome
|
||||
},
|
||||
{
|
||||
path: '/error',
|
||||
name: 'PageError',
|
||||
component: PageError
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'DynamicComponent',
|
||||
component: DynamicComponent
|
||||
}
|
||||
]
|
||||
|
||||
@@ -138,8 +206,25 @@ window.router = VueRouter.createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
// BACKWARDS compatibility extensions
|
||||
window.LOCALE = window.g.locale
|
||||
window.app.mixin({
|
||||
computed: {
|
||||
isVueRoute() {
|
||||
const currentPath = window.location.pathname
|
||||
const matchedRoute = window.router.resolve(currentPath)
|
||||
const isVueRoute = matchedRoute?.matched?.length > 0
|
||||
return isVueRoute
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.app.use(VueQrcodeReader)
|
||||
window.app.use(Quasar, {
|
||||
config: {
|
||||
loading: {
|
||||
spinner: Quasar.QSpinnerBars
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.i18n = new VueI18n.createI18n({
|
||||
locale: window.g.locale,
|
||||
@@ -147,25 +232,9 @@ window.i18n = new VueI18n.createI18n({
|
||||
messages: window.localisation
|
||||
})
|
||||
|
||||
window.app.mixin({
|
||||
data() {
|
||||
return {
|
||||
api: window._lnbitsApi,
|
||||
utils: window._lnbitsUtils,
|
||||
g: window.g,
|
||||
...WINDOW_SETTINGS
|
||||
}
|
||||
},
|
||||
// backwards compatibility for extensions, should not be used in the future
|
||||
methods: {
|
||||
copyText: window._lnbitsUtils.copyText,
|
||||
formatBalance: window._lnbitsUtils.formatBalance
|
||||
}
|
||||
})
|
||||
|
||||
window.app.use(VueQrcodeReader)
|
||||
window.app.use(Quasar, quasarConfig)
|
||||
|
||||
window.app.use(window.i18n)
|
||||
|
||||
window.app.provide('g', g)
|
||||
window.app.use(window.router)
|
||||
window.app.component('DynamicComponent', DynamicComponent)
|
||||
window.app.mount('#vue')
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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,6 +3,7 @@ window.PageAccount = {
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
user: null,
|
||||
untouchedUser: null,
|
||||
hasUsername: false,
|
||||
showUserId: false,
|
||||
@@ -190,14 +191,6 @@ window.PageAccount = {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tab(tab) {
|
||||
this.$router.push(`/account#${tab}`)
|
||||
},
|
||||
$route(to) {
|
||||
if (to.hash.length > 1) {
|
||||
this.tab = to.hash.replace('#', '')
|
||||
}
|
||||
},
|
||||
'assetsTable.search': {
|
||||
handler() {
|
||||
const props = {}
|
||||
@@ -210,7 +203,7 @@ window.PageAccount = {
|
||||
},
|
||||
computed: {
|
||||
isUserTouched() {
|
||||
return !_.isEqual(this.g.user, this.untouchedUser)
|
||||
return JSON.stringify(this.user) !== JSON.stringify(this.untouchedUser)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -228,13 +221,14 @@ window.PageAccount = {
|
||||
'/api/v1/auth/update',
|
||||
null,
|
||||
{
|
||||
user_id: this.g.user.id,
|
||||
username: this.g.user.username,
|
||||
email: this.g.user.email,
|
||||
extra: this.g.user.extra
|
||||
user_id: this.user.id,
|
||||
username: this.user.username,
|
||||
email: this.user.email,
|
||||
extra: this.user.extra
|
||||
}
|
||||
)
|
||||
this.untouchedUser = JSON.parse(JSON.stringify(this.g.user))
|
||||
this.user = data
|
||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
||||
this.hasUsername = !!data.username
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
@@ -266,13 +260,14 @@ window.PageAccount = {
|
||||
'/api/v1/auth/password',
|
||||
null,
|
||||
{
|
||||
user_id: this.g.user.id,
|
||||
user_id: this.user.id,
|
||||
username: this.credentialsData.username,
|
||||
password_old: this.credentialsData.oldPassword,
|
||||
password: this.credentialsData.newPassword,
|
||||
password_repeat: this.credentialsData.newPasswordRepeat
|
||||
}
|
||||
)
|
||||
this.user = data
|
||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
||||
this.hasUsername = !!data.username
|
||||
this.credentialsData.show = false
|
||||
@@ -291,10 +286,11 @@ window.PageAccount = {
|
||||
'/api/v1/auth/pubkey',
|
||||
null,
|
||||
{
|
||||
user_id: this.g.user.id,
|
||||
user_id: this.user.id,
|
||||
pubkey: this.credentialsData.pubkey
|
||||
}
|
||||
)
|
||||
this.user = data
|
||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
||||
this.hasUsername = !!data.username
|
||||
this.credentialsData.show = false
|
||||
@@ -310,8 +306,8 @@ window.PageAccount = {
|
||||
this.credentialsData = {
|
||||
show: true,
|
||||
oldPassword: null,
|
||||
username: this.g.user.username,
|
||||
pubkey: this.g.user.pubkey,
|
||||
username: this.user.username,
|
||||
pubkey: this.user.pubkey,
|
||||
newPassword: null,
|
||||
newPasswordRepeat: null
|
||||
}
|
||||
@@ -427,7 +423,7 @@ window.PageAccount = {
|
||||
'/api/v1/auth/acl',
|
||||
null,
|
||||
{
|
||||
id: this.g.user.id,
|
||||
id: this.user.id,
|
||||
password: this.apiAcl.password,
|
||||
...this.selectedApiAcl
|
||||
}
|
||||
@@ -611,7 +607,7 @@ window.PageAccount = {
|
||||
},
|
||||
copyAssetLinkToClipboard(asset) {
|
||||
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/binary`
|
||||
this.utils.copyText(assetUrl)
|
||||
this.copyText(assetUrl)
|
||||
},
|
||||
addUserLabel() {
|
||||
if (!this.labelsDialog.data.name) {
|
||||
@@ -628,8 +624,8 @@ window.PageAccount = {
|
||||
})
|
||||
return
|
||||
}
|
||||
this.g.user.extra.labels = this.g.user.extra.labels || []
|
||||
const duplicate = this.g.user.extra.labels.find(
|
||||
this.user.extra.labels = this.user.extra.labels || []
|
||||
const duplicate = this.user.extra.labels.find(
|
||||
label => label.name === this.labelsDialog.data.name
|
||||
)
|
||||
if (duplicate) {
|
||||
@@ -639,7 +635,7 @@ window.PageAccount = {
|
||||
})
|
||||
return
|
||||
}
|
||||
this.g.user.extra.labels.unshift({...this.labelsDialog.data})
|
||||
this.user.extra.labels.unshift({...this.labelsDialog.data})
|
||||
this.labelsDialog.show = false
|
||||
return true
|
||||
},
|
||||
@@ -661,15 +657,13 @@ window.PageAccount = {
|
||||
},
|
||||
updateUserLabel() {
|
||||
const label = this.labelsDialog.data
|
||||
const existingLabels = JSON.parse(
|
||||
JSON.stringify(this.g.user.extra.labels)
|
||||
)
|
||||
this.g.user.extra.labels = this.g.user.extra.labels.filter(
|
||||
const existingLabels = JSON.parse(JSON.stringify(this.user.extra.labels))
|
||||
this.user.extra.labels = this.user.extra.labels.filter(
|
||||
l => l.name !== label.name
|
||||
)
|
||||
const labelUpdated = this.addUserLabel()
|
||||
if (!labelUpdated) {
|
||||
this.g.user.extra.labels = existingLabels
|
||||
this.user.extra.labels = existingLabels
|
||||
}
|
||||
this.labelsDialog.show = false
|
||||
},
|
||||
@@ -677,7 +671,7 @@ window.PageAccount = {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this label?')
|
||||
.onOk(() => {
|
||||
this.g.user.extra.labels = this.g.user.extra.labels.filter(
|
||||
this.user.extra.labels = this.user.extra.labels.filter(
|
||||
l => l.name !== label.name
|
||||
)
|
||||
})
|
||||
@@ -685,13 +679,22 @@ window.PageAccount = {
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.untouchedUser = JSON.parse(JSON.stringify(this.g.user))
|
||||
this.hasUsername = !!this.g.user.username
|
||||
if (this.$route.hash.length > 1) {
|
||||
this.tab = this.$route.hash.replace('#', '')
|
||||
try {
|
||||
const {data} = await LNbits.api.getAuthenticatedUser()
|
||||
this.user = data
|
||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
||||
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.getUserAssets()
|
||||
|
||||
// filter out themes that are not allowed
|
||||
this.themeOptions = this.themeOptions.filter(theme =>
|
||||
this.LNBITS_THEME_OPTIONS.includes(theme.name)
|
||||
|
||||
@@ -15,21 +15,12 @@ window.PageAdmin = {
|
||||
needsRestart: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tab(tab) {
|
||||
this.$router.push(`/admin#${tab}`)
|
||||
},
|
||||
$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()
|
||||
const hash = window.location.hash.replace('#', '')
|
||||
if (hash) {
|
||||
this.tab = hash
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checkChanges() {
|
||||
@@ -38,9 +29,17 @@ window.PageAdmin = {
|
||||
},
|
||||
methods: {
|
||||
getDefaultSetting(fieldName) {
|
||||
LNbits.api.getDefaultSetting(fieldName).then(response => {
|
||||
this.formData[fieldName] = response.data.default_value
|
||||
})
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
`/admin/api/v1/settings/default?field_name=${fieldName}`
|
||||
)
|
||||
.then(response => {
|
||||
this.formData[fieldName] = response.data.default_value
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
restartServer() {
|
||||
LNbits.api
|
||||
|
||||
@@ -106,6 +106,9 @@ window.PageAudit = {
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatDate(dateString) {
|
||||
return LNbits.utils.formatDateString(dateString)
|
||||
},
|
||||
async fetchAudit(props) {
|
||||
try {
|
||||
const params = LNbits.utils.prepareFilterQuery(this.auditTable, props)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
window.PageError = {
|
||||
template: '#page-error',
|
||||
mixins: [window.windowMixin]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
window.PageExtensions = {
|
||||
template: '#page-extensions',
|
||||
mixins: [window.windowMixin],
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {
|
||||
extbuilderEnabled: false,
|
||||
@@ -8,9 +8,9 @@ window.PageExtensions = {
|
||||
fullscreen: false,
|
||||
autoplay: true,
|
||||
searchTerm: '',
|
||||
tab: 'installed',
|
||||
tab: 'all',
|
||||
manageExtensionTab: 'releases',
|
||||
filteredExtensions: [],
|
||||
filteredExtensions: null,
|
||||
updatableExtensions: [],
|
||||
showUninstallDialog: false,
|
||||
showManageExtensionDialog: false,
|
||||
@@ -26,18 +26,19 @@ window.PageExtensions = {
|
||||
selectedRelease: null,
|
||||
uninstallAndDropDb: false,
|
||||
maxStars: 5,
|
||||
paylinkWebsocket: null
|
||||
paylinkWebsocket: null,
|
||||
user: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchTerm(term) {
|
||||
this.filterExtensions(term, this.tab)
|
||||
},
|
||||
tab(val) {
|
||||
this.filterExtensions(this.searchTerm, val)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTabChanged(tab) {
|
||||
this.filterExtensions(this.searchTerm, tab)
|
||||
},
|
||||
filterExtensions(term, tab) {
|
||||
// Filter the extensions list
|
||||
function extensionNameContains(searchTerm) {
|
||||
@@ -64,6 +65,7 @@ window.PageExtensions = {
|
||||
details_link:
|
||||
e.installedRelease?.details_link || e.latestRelease?.details_link
|
||||
}))
|
||||
this.tab = tab
|
||||
},
|
||||
|
||||
async installExtension(release) {
|
||||
@@ -72,31 +74,29 @@ window.PageExtensions = {
|
||||
this.unsubscribeFromPaylinkWs()
|
||||
|
||||
const extension = this.selectedExtension
|
||||
this.selectedExtension.inProgress = true
|
||||
extension.inProgress = true
|
||||
this.showManageExtensionDialog = false
|
||||
release.payment_hash =
|
||||
release.payment_hash || this.getPaylinkHash(release.pay_link)
|
||||
|
||||
LNbits.api
|
||||
.request('POST', `/api/v1/extension`, this.g.user.wallets[0].adminkey, {
|
||||
ext_id: this.selectedExtension.id,
|
||||
ext_id: extension.id,
|
||||
archive: release.archive,
|
||||
source_repo: release.source_repo,
|
||||
payment_hash: release.payment_hash,
|
||||
version: release.version
|
||||
})
|
||||
.then(response => {
|
||||
const extension = this.extensions.find(
|
||||
ext => ext.id === this.selectedExtension.id
|
||||
)
|
||||
extension.isAvailable = true
|
||||
extension.isInstalled = true
|
||||
extension.installedRelease = release
|
||||
this.toggleExtension(extension)
|
||||
extension.inProgress = false
|
||||
this.selectedExtension = extension
|
||||
this.extensions = this.extensions.concat([])
|
||||
this.filteredExtensions = this.extensions.concat([])
|
||||
this.handleTabChanged('installed')
|
||||
this.tab = 'installed'
|
||||
this.refreshRoute()
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn(err)
|
||||
@@ -105,32 +105,34 @@ window.PageExtensions = {
|
||||
})
|
||||
},
|
||||
async uninstallExtension() {
|
||||
const extension = this.selectedExtension
|
||||
this.showManageExtensionDialog = false
|
||||
this.showUninstallDialog = false
|
||||
this.selectedExtension.inProgress = true
|
||||
extension.inProgress = true
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
`/api/v1/extension/${this.selectedExtension.id}`,
|
||||
`/api/v1/extension/${extension.id}`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(response => {
|
||||
const extension = this.extensions.find(
|
||||
ext => ext.id === this.selectedExtension.id
|
||||
)
|
||||
extension.isAvailable = false
|
||||
extension.isInstalled = false
|
||||
extension.inProgress = false
|
||||
extension.installedRelease = null
|
||||
this.filteredExtensions = this.filteredExtensions.filter(
|
||||
ext => ext.id !== extension.id
|
||||
)
|
||||
this.filteredExtensions = this.extensions.concat([])
|
||||
this.handleTabChanged('installed')
|
||||
this.tab = 'installed'
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Extension uninstalled!'
|
||||
})
|
||||
if (this.uninstallAndDropDb) {
|
||||
this.showDropDb()
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.refreshRoute()
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -158,6 +160,9 @@ window.PageExtensions = {
|
||||
type: 'positive',
|
||||
message: 'Extension DB deleted!'
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.refreshRoute()
|
||||
}, 300)
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
@@ -621,6 +626,9 @@ window.PageExtensions = {
|
||||
message: `${count ? count : 'No'} extensions updated!`
|
||||
})
|
||||
this.showUpdateAllDialog = false
|
||||
setTimeout(() => {
|
||||
this.refreshRoute()
|
||||
}, 2000)
|
||||
},
|
||||
async fetchAllExtensions() {
|
||||
try {
|
||||
@@ -635,23 +643,21 @@ window.PageExtensions = {
|
||||
},
|
||||
async created() {
|
||||
this.extensions = await this.fetchAllExtensions()
|
||||
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
|
||||
|
||||
if (this.g.user.extensions.length === 0) {
|
||||
this.tab = 'all'
|
||||
}
|
||||
|
||||
const extId = window.location.hash.replace('#', '')
|
||||
const ext = this.extensions.find(ext => ext.id === extId)
|
||||
|
||||
this.extbuilderEnabled = user.admin || this.LNBITS_EXT_BUILDER
|
||||
this.filteredExtensions = this.extensions.concat([])
|
||||
const hash = window.location.hash.replace('#', '')
|
||||
const ext = this.filteredExtensions.find(ext => ext.id === hash)
|
||||
if (ext) {
|
||||
this.searchTerm = ext.id
|
||||
if (ext.isInstalled) this.tab = 'installed'
|
||||
this.handleTabChanged(ext.isInstalled ? 'installed' : 'all')
|
||||
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.hasNewVersion(ext)
|
||||
)
|
||||
|
||||
this.filterExtensions(this.searchTerm, this.tab)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ window.PageHome = {
|
||||
return this.LNBITS_CUSTOM_IMAGE
|
||||
},
|
||||
showHomepageElements() {
|
||||
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS === true
|
||||
return this.HOMEPAGE_ELEMENTS_ENABLED
|
||||
},
|
||||
siteTitle() {
|
||||
return this.SITE_TITLE || ''
|
||||
@@ -54,6 +54,13 @@ window.PageHome = {
|
||||
},
|
||||
ads() {
|
||||
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: {
|
||||
@@ -79,7 +86,7 @@ window.PageHome = {
|
||||
this.password,
|
||||
this.passwordRepeat
|
||||
)
|
||||
this.refreshAuthUser()
|
||||
window.location.href = '/wallet'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
@@ -91,7 +98,7 @@ window.PageHome = {
|
||||
this.password,
|
||||
this.passwordRepeat
|
||||
)
|
||||
this.refreshAuthUser()
|
||||
window.location.href = '/wallet'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
@@ -99,7 +106,7 @@ window.PageHome = {
|
||||
async login() {
|
||||
try {
|
||||
await LNbits.api.login(this.username, this.password)
|
||||
this.refreshAuthUser()
|
||||
window.location.href = '/wallet'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
@@ -107,18 +114,8 @@ window.PageHome = {
|
||||
async loginUsr() {
|
||||
try {
|
||||
await LNbits.api.loginUsr(this.usr)
|
||||
this.refreshAuthUser()
|
||||
} 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}`)
|
||||
this.usr = ''
|
||||
window.location.href = '/wallet'
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
LNbits.utils.notifyApiError(e)
|
||||
@@ -126,7 +123,7 @@ window.PageHome = {
|
||||
},
|
||||
createWallet() {
|
||||
LNbits.api.createAccount(this.walletName).then(res => {
|
||||
this.$router.push(`/wallet/${res.data.id}`)
|
||||
window.location = '/wallet?usr=' + res.data.user + '&wal=' + res.data.id
|
||||
})
|
||||
},
|
||||
processing() {
|
||||
@@ -139,7 +136,7 @@ window.PageHome = {
|
||||
},
|
||||
created() {
|
||||
if (this.g.isUserAuthorized) {
|
||||
return this.refreshAuthUser()
|
||||
window.location.href = '/wallet'
|
||||
}
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
this.reset_key = urlParams.get('reset_key')
|
||||
|
||||
@@ -91,14 +91,14 @@ window.PageNode = {
|
||||
{
|
||||
name: 'sat',
|
||||
align: 'right',
|
||||
label: this.$t('amount'),
|
||||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
||||
field: row => this.formatMsat(row.amount),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'fee',
|
||||
align: 'right',
|
||||
label: this.$t('fee'),
|
||||
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
|
||||
field: 'fee'
|
||||
},
|
||||
{
|
||||
@@ -144,7 +144,7 @@ window.PageNode = {
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
label: this.$t('amount'),
|
||||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
||||
field: row => this.formatMsat(row.amount),
|
||||
sortable: true
|
||||
},
|
||||
|
||||
@@ -256,6 +256,9 @@ window.PagePayments = {
|
||||
this.paymentDetails = payment
|
||||
return (this.showDetails = !this.showDetails)
|
||||
},
|
||||
formatDate(dateString) {
|
||||
return LNbits.utils.formatDateString(dateString)
|
||||
},
|
||||
formatCurrency(amount, currency) {
|
||||
try {
|
||||
return LNbits.utils.formatCurrency(amount, currency)
|
||||
|
||||
@@ -85,10 +85,10 @@ window.PageUsers = {
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
name: 'user',
|
||||
align: 'left',
|
||||
label: 'User Id',
|
||||
field: 'id',
|
||||
field: 'user',
|
||||
sortable: false
|
||||
},
|
||||
|
||||
@@ -119,7 +119,7 @@ window.PageUsers = {
|
||||
align: 'left',
|
||||
label: 'Balance',
|
||||
field: 'balance_msat',
|
||||
sortable: false
|
||||
sortable: true
|
||||
},
|
||||
|
||||
{
|
||||
@@ -127,7 +127,7 @@ window.PageUsers = {
|
||||
align: 'left',
|
||||
label: 'Payments',
|
||||
field: 'transaction_count',
|
||||
sortable: false
|
||||
sortable: true
|
||||
},
|
||||
|
||||
{
|
||||
@@ -135,24 +135,16 @@ window.PageUsers = {
|
||||
align: 'left',
|
||||
label: 'Last Payment',
|
||||
field: 'last_payment',
|
||||
sortable: false
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
sortBy: 'created_at',
|
||||
sortBy: 'balance_msat',
|
||||
rowsPerPage: 10,
|
||||
page: 1,
|
||||
descending: true,
|
||||
rowsNumber: 10
|
||||
},
|
||||
sortFields: [
|
||||
{name: 'id', label: 'User ID'},
|
||||
{name: 'username', label: 'Username'},
|
||||
{name: 'email', label: 'Email'},
|
||||
{name: 'pubkey', label: 'Public Key'},
|
||||
{name: 'created_at', label: 'Creation Date'},
|
||||
{name: 'updated_at', label: 'Last Updated'}
|
||||
],
|
||||
search: null,
|
||||
hideEmpty: true,
|
||||
loading: false
|
||||
@@ -176,6 +168,9 @@ window.PageUsers = {
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatDate(date) {
|
||||
return LNbits.utils.formatDateString(date)
|
||||
},
|
||||
formatSat(value) {
|
||||
return LNbits.utils.formatSat(Math.floor(value / 1000))
|
||||
},
|
||||
@@ -198,21 +193,11 @@ window.PageUsers = {
|
||||
)
|
||||
.onOk(() => {
|
||||
const url = window.location.origin + '?reset_key=' + res.data
|
||||
this.utils.copyText(url)
|
||||
this.copyText(url)
|
||||
})
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
sortByColumn(columnName) {
|
||||
if (this.usersTable.pagination.sortBy === columnName) {
|
||||
this.usersTable.pagination.descending =
|
||||
!this.usersTable.pagination.descending
|
||||
} else {
|
||||
this.usersTable.pagination.sortBy = columnName
|
||||
this.usersTable.pagination.descending = false
|
||||
}
|
||||
this.fetchUsers()
|
||||
},
|
||||
createUser() {
|
||||
LNbits.api
|
||||
.request('POST', '/users/api/v1/user', null, this.activeUser.data)
|
||||
@@ -347,7 +332,7 @@ window.PageUsers = {
|
||||
},
|
||||
copyWalletLink(walletId) {
|
||||
const url = `${window.location.origin}/wallet?usr=${this.activeWallet.userId}&wal=${walletId}`
|
||||
this.utils.copyText(url)
|
||||
this.copyText(url)
|
||||
},
|
||||
fetchUsers(props) {
|
||||
this.relaxFilterForFields(['username', 'email'])
|
||||
|
||||
@@ -74,7 +74,7 @@ window.PageWallets = {
|
||||
}
|
||||
},
|
||||
goToWallet(walletId) {
|
||||
this.$router.push({path: '/wallet', query: {wal: walletId}})
|
||||
window.location = `/wallet?wal=${walletId}`
|
||||
},
|
||||
formattedFiatAmount(amount, currency) {
|
||||
return LNbits.utils.formatCurrency(Number(amount).toFixed(2), currency)
|
||||
|
||||
@@ -1,44 +1,4 @@
|
||||
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) {
|
||||
return Quasar.Dialog.create({
|
||||
message: msg,
|
||||
@@ -74,33 +34,11 @@ window._lnbitsUtils = {
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
return hashHex
|
||||
},
|
||||
formatTimestamp(timestamp, format = null) {
|
||||
format = format || window.dateFormat
|
||||
return Quasar.date.formatDate(new Date(timestamp * 1000), format)
|
||||
formatDate(timestamp) {
|
||||
return Quasar.date.formatDate(new Date(timestamp * 1000), window.dateFormat)
|
||||
},
|
||||
// backwards compatibility for extensions
|
||||
formatDateString(isoDateString) {
|
||||
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)
|
||||
return Quasar.date.formatDate(new Date(isoDateString), window.dateFormat)
|
||||
},
|
||||
formatCurrency(value, currency) {
|
||||
return new Intl.NumberFormat(window.i18n.global.locale, {
|
||||
@@ -221,34 +159,5 @@ window._lnbitsUtils = {
|
||||
converter.setFlavor('github')
|
||||
converter.setOption('simpleLineBreaks', true)
|
||||
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,8 +1,10 @@
|
||||
window.PageWallet = {
|
||||
template: '#page-wallet',
|
||||
window.WalletPageLogic = {
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
origin: window.location.origin,
|
||||
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
||||
websocketUrl: `${'http:' ? 'ws://' : 'wss://'}${window.location.host}/api/v1/ws`,
|
||||
parse: {
|
||||
show: false,
|
||||
invoice: null,
|
||||
@@ -32,7 +34,7 @@ window.PageWallet = {
|
||||
amountMsat: null,
|
||||
minMax: [0, 2100000000000000],
|
||||
lnurl: null,
|
||||
units: [],
|
||||
units: ['sat'],
|
||||
unit: 'sat',
|
||||
fiatProvider: '',
|
||||
data: {
|
||||
@@ -49,6 +51,7 @@ window.PageWallet = {
|
||||
hasNfc: false,
|
||||
nfcReaderAbortController: null,
|
||||
formattedFiatAmount: 0,
|
||||
formattedExchange: null,
|
||||
paymentFilter: {
|
||||
'status[ne]': 'failed'
|
||||
},
|
||||
@@ -74,10 +77,12 @@ window.PageWallet = {
|
||||
return this.parse.invoice.sat <= this.g.wallet.sat
|
||||
},
|
||||
formattedAmount() {
|
||||
if (this.receive.unit != 'sat' || !this.g.isSatsDenomination) {
|
||||
if (this.receive.unit != 'sat' || LNBITS_DENOMINATION != 'sats') {
|
||||
return LNbits.utils.formatCurrency(
|
||||
Number(this.receive.data.amount).toFixed(2),
|
||||
!this.g.isSatsDenomination ? this.g.denomination : this.receive.unit
|
||||
LNBITS_DENOMINATION != 'sats'
|
||||
? LNBITS_DENOMINATION
|
||||
: this.receive.unit
|
||||
)
|
||||
} else {
|
||||
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
|
||||
@@ -93,9 +98,29 @@ window.PageWallet = {
|
||||
this.parse.show = true
|
||||
this.lnurlScan()
|
||||
},
|
||||
formatFiatAmount(amount, currency) {
|
||||
this.update.currency = currency
|
||||
this.formattedFiatAmount = LNbits.utils.formatCurrency(
|
||||
amount.toFixed(2),
|
||||
currency
|
||||
)
|
||||
this.formattedExchange = LNbits.utils.formatCurrency(
|
||||
this.g.exchangeRate,
|
||||
currency
|
||||
)
|
||||
},
|
||||
msatoshiFormat(value) {
|
||||
return LNbits.utils.formatSat(value / 1000)
|
||||
},
|
||||
closeCamera() {
|
||||
this.parse.camera.show = false
|
||||
},
|
||||
showCamera() {
|
||||
this.parse.camera.show = true
|
||||
},
|
||||
focusInput(el) {
|
||||
this.$nextTick(() => this.$refs[el].focus())
|
||||
},
|
||||
showReceiveDialog() {
|
||||
this.receive.show = true
|
||||
this.receive.status = 'pending'
|
||||
@@ -105,17 +130,12 @@ window.PageWallet = {
|
||||
this.receive.data.memo = null
|
||||
this.receive.data.internalMemo = 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.g.wallet.currency || 'sat'
|
||||
: 'sat'
|
||||
this.receive.minMax = [0, 2100000000000000]
|
||||
this.receive.lnurl = null
|
||||
this.focusInput('setAmount')
|
||||
},
|
||||
onReceiveDialogHide() {
|
||||
if (this.hasNfc) {
|
||||
@@ -134,6 +154,7 @@ window.PageWallet = {
|
||||
this.parse.data.internalMemo = null
|
||||
this.parse.data.paymentChecker = null
|
||||
this.parse.camera.show = false
|
||||
this.focusInput('textArea')
|
||||
},
|
||||
closeParseDialog() {
|
||||
setTimeout(() => {
|
||||
@@ -145,7 +166,7 @@ window.PageWallet = {
|
||||
},
|
||||
createInvoice() {
|
||||
this.receive.status = 'loading'
|
||||
if (!this.g.isSatsDenomination) {
|
||||
if (LNBITS_DENOMINATION != 'sats') {
|
||||
this.receive.data.amount = this.receive.data.amount * 100
|
||||
}
|
||||
|
||||
@@ -204,6 +225,35 @@ window.PageWallet = {
|
||||
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() {
|
||||
LNbits.api
|
||||
.request('POST', '/api/v1/lnurlscan', this.g.wallet.adminkey, {
|
||||
@@ -251,8 +301,8 @@ window.PageWallet = {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
decodeQR(val) {
|
||||
this.parse.data.request = val
|
||||
decodeQR(res) {
|
||||
this.parse.data.request = res[0].rawValue
|
||||
this.decodeRequest()
|
||||
this.parse.camera.show = false
|
||||
},
|
||||
@@ -370,7 +420,7 @@ window.PageWallet = {
|
||||
)
|
||||
.then(response => {
|
||||
dismissPaymentMsg()
|
||||
this.g.updatePayments = !this.g.updatePayments
|
||||
this.updatePayments = !this.updatePayments
|
||||
this.parse.show = false
|
||||
if (response.data.status == 'success') {
|
||||
Quasar.Notify.create({
|
||||
@@ -426,7 +476,7 @@ window.PageWallet = {
|
||||
})
|
||||
break
|
||||
case 'aes':
|
||||
this.utils.decryptLnurlPayAES(action, response.data.preimage)
|
||||
decryptLnurlPayAES(action, response.data.preimage)
|
||||
Quasar.Notify.create({
|
||||
message: value,
|
||||
caption: extra.success_action.description,
|
||||
@@ -498,6 +548,51 @@ window.PageWallet = {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
deleteWallet() {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||
.onOk(() => {
|
||||
LNbits.api
|
||||
.deleteWallet(this.g.wallet)
|
||||
.then(_ => {
|
||||
Quasar.Notify.create({
|
||||
timeout: 3000,
|
||||
message: `Wallet deleted!`,
|
||||
spinner: true
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
updateFiatBalance(currency) {
|
||||
// set rate from local storage to avoid clunky api calls
|
||||
if (this.$q.localStorage.getItem('lnbits.exchangeRate.' + currency)) {
|
||||
this.g.exchangeRate = this.$q.localStorage.getItem(
|
||||
'lnbits.exchangeRate.' + currency
|
||||
)
|
||||
this.g.fiatBalance =
|
||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||
this.formatFiatAmount(this.g.fiatBalance, currency)
|
||||
}
|
||||
if (currency && this.g.wallet.currency == currency) {
|
||||
LNbits.api
|
||||
.request('GET', `/api/v1/rate/` + currency, null)
|
||||
.then(response => {
|
||||
this.g.fiatBalance =
|
||||
(response.data.price / 100000000) * this.g.wallet.sat
|
||||
this.g.exchangeRate = response.data.price.toFixed(2)
|
||||
this.g.fiatTracking = true
|
||||
this.formatFiatAmount(this.g.fiatBalance, this.g.wallet.currency)
|
||||
this.$q.localStorage.set(
|
||||
'lnbits.exchangeRate.' + currency,
|
||||
this.g.exchangeRate
|
||||
)
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
}
|
||||
},
|
||||
pasteToTextArea() {
|
||||
this.$refs.textArea.focus()
|
||||
navigator.clipboard.readText().then(text => {
|
||||
@@ -599,6 +694,36 @@ window.PageWallet = {
|
||||
dismissPaymentMsg()
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
swapBalancePriority() {
|
||||
this.g.isFiatPriority = !this.g.isFiatPriority
|
||||
this.receive.unit = this.g.isFiatPriority
|
||||
? this.g.wallet.currency || 'sat'
|
||||
: 'sat'
|
||||
},
|
||||
handleFiatTracking() {
|
||||
this.g.fiatTracking = !this.g.fiatTracking
|
||||
if (!this.g.fiatTracking) {
|
||||
this.g.isFiatPriority = false
|
||||
this.update.currency = ''
|
||||
this.g.wallet.currency = ''
|
||||
this.updateWallet({currency: ''})
|
||||
} else {
|
||||
this.g.wallet.currency = this.update.currency
|
||||
this.updateWallet({currency: this.update.currency})
|
||||
this.updateFiatBalance(this.update.currency)
|
||||
}
|
||||
},
|
||||
createdTasks() {
|
||||
this.update.name = this.g.wallet.name
|
||||
this.receive.units = ['sat', ...(window.currencies || [])]
|
||||
if (this.g.wallet.currency != '' && LNBITS_DENOMINATION == 'sats') {
|
||||
this.g.fiatTracking = true
|
||||
this.updateFiatBalance(this.g.wallet.currency)
|
||||
} else {
|
||||
this.update.currency = ''
|
||||
this.g.fiatTracking = false
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -609,24 +734,15 @@ window.PageWallet = {
|
||||
this.decodeRequest()
|
||||
this.parse.show = true
|
||||
}
|
||||
const wallet = this.g.user.wallets.find(w => w.id === this.$route.params.id)
|
||||
if (wallet) {
|
||||
this.g.wallet = wallet
|
||||
this.g.lastActiveWallet = wallet.id
|
||||
this.$q.localStorage.setItem('lnbits.lastActiveWallet', wallet.id)
|
||||
this.$router.replace(`/wallet/${wallet.id}`)
|
||||
} else {
|
||||
this.g.errorCode = 404
|
||||
this.g.errorMessage = 'Wallet not found.'
|
||||
this.$router.push('/error')
|
||||
}
|
||||
this.createdTasks()
|
||||
},
|
||||
watch: {
|
||||
'g.updatePaymentsHash'() {
|
||||
this.receive.show = false
|
||||
},
|
||||
'g.updatePayments'() {
|
||||
'g.updatePayments'(newVal, oldVal) {
|
||||
this.parse.show = false
|
||||
if (this.receive.paymentHash === this.g.updatePaymentsHash) {
|
||||
this.receive.show = false
|
||||
this.receive.paymentHash = null
|
||||
}
|
||||
if (
|
||||
this.g.wallet.currency &&
|
||||
this.$q.localStorage.getItem(
|
||||
@@ -638,32 +754,18 @@ window.PageWallet = {
|
||||
)
|
||||
this.g.fiatBalance =
|
||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||
this.formatFiatAmount(this.g.fiatBalance, this.g.wallet.currency)
|
||||
}
|
||||
},
|
||||
'g.wallet'() {
|
||||
if (this.g.wallet.currency) {
|
||||
this.g.fiatTracking = true
|
||||
this.g.fiatBalance =
|
||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||
} else {
|
||||
this.g.fiatBalance = 0
|
||||
this.g.fiatTracking = false
|
||||
}
|
||||
},
|
||||
'g.isFiatPriority'() {
|
||||
this.receive.unit = this.g.isFiatPriority ? this.g.wallet.currency : 'sat'
|
||||
},
|
||||
'g.fiatBalance'() {
|
||||
this.formattedFiatAmount = LNbits.utils.formatCurrency(
|
||||
this.g.fiatBalance.toFixed(2),
|
||||
this.g.wallet.currency
|
||||
)
|
||||
},
|
||||
'g.exchangeRate'() {
|
||||
if (this.g.fiatTracking && this.g.wallet.currency) {
|
||||
this.g.fiatBalance =
|
||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||
}
|
||||
'g.wallet': {
|
||||
handler() {
|
||||
try {
|
||||
this.createdTasks()
|
||||
} catch (error) {
|
||||
console.warn(`Chart creation failed: ${error}`)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,131 @@
|
||||
window.windowMixin = {}
|
||||
window.windowMixin = {
|
||||
i18n: window.i18n,
|
||||
data() {
|
||||
return {
|
||||
api: window._lnbitsApi,
|
||||
utils: window._lnbitsUtils,
|
||||
g: window.g,
|
||||
...WINDOW_SETTINGS
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openNewWalletDialog(walletType = 'lightning') {
|
||||
this.g.newWalletType = walletType
|
||||
this.g.showNewWalletDialog = true
|
||||
},
|
||||
flipWallets(smallScreen) {
|
||||
this.g.walletFlip = !this.g.walletFlip
|
||||
if (this.g.walletFlip && smallScreen) {
|
||||
this.g.visibleDrawer = false
|
||||
}
|
||||
this.$q.localStorage.set('lnbits.walletFlip', this.g.walletFlip)
|
||||
},
|
||||
goToWallets() {
|
||||
this.$router.push({
|
||||
path: '/wallets'
|
||||
})
|
||||
},
|
||||
paymentEvents() {
|
||||
this.g.walletEventListeners = this.g.walletEventListeners || []
|
||||
this.g.user.wallets.forEach(wallet => {
|
||||
if (!this.g.walletEventListeners.includes(wallet.id)) {
|
||||
this.g.walletEventListeners.push(wallet.id)
|
||||
LNbits.events.onInvoicePaid(wallet, data => {
|
||||
const walletIndex = this.g.user.wallets.findIndex(
|
||||
w => w.id === wallet.id
|
||||
)
|
||||
if (walletIndex !== -1) {
|
||||
//needed for balance being deducted
|
||||
let satBalance = data.wallet_balance
|
||||
if (data.payment.amount < 0) {
|
||||
satBalance = data.wallet_balance += data.payment.amount / 1000
|
||||
}
|
||||
//update the wallet
|
||||
Object.assign(this.g.user.wallets[walletIndex], {
|
||||
sat: satBalance,
|
||||
msat: data.wallet_balance * 1000,
|
||||
fsat: data.wallet_balance.toLocaleString()
|
||||
})
|
||||
//update the current wallet
|
||||
if (this.g.wallet.id === data.payment.wallet_id) {
|
||||
Object.assign(this.g.wallet, this.g.user.wallets[walletIndex])
|
||||
|
||||
//if on the wallet page and payment is incoming trigger the eventReaction
|
||||
if (
|
||||
data.payment.amount > 0 &&
|
||||
window.location.pathname === '/wallet'
|
||||
) {
|
||||
eventReaction(data.wallet_balance * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.g.updatePaymentsHash = data.payment.payment_hash
|
||||
this.g.updatePayments = !this.g.updatePayments
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
selectWallet(wallet) {
|
||||
this.g.wallet = wallet
|
||||
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})
|
||||
})
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
if (window.user) {
|
||||
this.g.user = Vue.reactive(window.LNbits.map.user(window.user))
|
||||
}
|
||||
if (window.wallet) {
|
||||
this.g.wallet = Vue.reactive(window.LNbits.map.wallet(window.wallet))
|
||||
}
|
||||
if (window.extensions) {
|
||||
this.g.extensions = Vue.reactive(window.extensions)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.g.user) {
|
||||
this.paymentEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@use 'sass:color';
|
||||
@use 'themes';
|
||||
|
||||
@each $theme, $colors in themes.$themes {
|
||||
@each $theme, $colors in $themes {
|
||||
body[data-theme='#{$theme}'] {
|
||||
@each $name, $color in $colors {
|
||||
@if $name== 'dark' {
|
||||
@@ -63,6 +62,6 @@ body.body--dark {
|
||||
.q-drawer {
|
||||
--q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)};
|
||||
background-color: var(--q-dark);
|
||||
backdrop-filter: blur(6px) brightness(0.8);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use 'themes';
|
||||
@use 'borders';
|
||||
@use 'background';
|
||||
@use 'home';
|
||||
@import 'themes';
|
||||
@import 'borders';
|
||||
@import 'background';
|
||||
@import 'home';
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
@@ -197,12 +197,3 @@ 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,6 +1,4 @@
|
||||
@use 'themes';
|
||||
|
||||
@each $theme, $colors in themes.$themes {
|
||||
@each $theme, $colors in $themes {
|
||||
@each $name, $color in $colors {
|
||||
@if $name== 'primary' {
|
||||
body[data-theme='#{$theme}'].neon-border {
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
"js/base.js",
|
||||
"js/windowMixin.js",
|
||||
"js/event-reactions.js",
|
||||
"js/bolt11-decoder.js"
|
||||
"js/bolt11-decoder.js",
|
||||
"js/lnurl.js"
|
||||
],
|
||||
"components": [
|
||||
"js/pages/error.js",
|
||||
"js/pages/home.js",
|
||||
"js/pages/extensions_builder.js",
|
||||
"js/pages/extensions.js",
|
||||
@@ -51,7 +51,6 @@
|
||||
"js/pages/node.js",
|
||||
"js/pages/node-public.js",
|
||||
"js/pages/audit.js",
|
||||
"js/pages/wallet.js",
|
||||
"js/pages/wallets.js",
|
||||
"js/pages/users.js",
|
||||
"js/pages/account.js",
|
||||
@@ -74,9 +73,7 @@
|
||||
"js/components/lnbits-wallet-new.js",
|
||||
"js/components/lnbits-wallet-share.js",
|
||||
"js/components/lnbits-wallet-paylinks.js",
|
||||
"js/components/lnbits-wallet-extra.js",
|
||||
"js/components/lnbits-home-logos.js",
|
||||
"js/components/lnbits-error.js",
|
||||
"js/components/lnbits-qrcode.js",
|
||||
"js/components/lnbits-qrcode-lnurl.js",
|
||||
"js/components/lnbits-disclaimer.js",
|
||||
@@ -85,7 +82,6 @@
|
||||
"js/components/lnbits-header-wallets.js",
|
||||
"js/components/lnbits-drawer.js",
|
||||
"js/components/lnbits-theme.js",
|
||||
"js/components/lnbits-qrcode-scanner.js",
|
||||
"js/components/lnbits-manage-extension-list.js",
|
||||
"js/components/lnbits-manage-wallet-list.js",
|
||||
"js/components/lnbits-language-dropdown.js",
|
||||
|
||||
Vendored
+85
-157
@@ -1,4 +1,4 @@
|
||||
/*! Axios v1.13.2 Copyright (c) 2025 Matt Zabriskie and contributors */
|
||||
/*! Axios v1.12.0 Copyright (c) 2025 Matt Zabriskie and contributors */
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
@@ -668,13 +668,6 @@
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return function wrap() {
|
||||
return fn.apply(thisArg, arguments);
|
||||
@@ -1038,8 +1031,10 @@
|
||||
result[targetKey] = merge({}, val);
|
||||
} else if (isArray(val)) {
|
||||
result[targetKey] = val.slice();
|
||||
} else if (!skipUndefined || !isUndefined(val)) {
|
||||
result[targetKey] = val;
|
||||
} else {
|
||||
if (!skipUndefined || !isUndefined(val)) {
|
||||
result[targetKey] = val;
|
||||
}
|
||||
}
|
||||
};
|
||||
for (var i = 0, l = arguments.length; i < l; i++) {
|
||||
@@ -1838,7 +1833,7 @@
|
||||
*
|
||||
* @param {Number} id The ID that was returned by `use`
|
||||
*
|
||||
* @returns {void}
|
||||
* @returns {Boolean} `true` if the interceptor was removed, `false` otherwise
|
||||
*/
|
||||
}, {
|
||||
key: "eject",
|
||||
@@ -2709,33 +2704,20 @@
|
||||
var cookies = platform.hasStandardBrowserEnv ?
|
||||
// Standard browser envs support document.cookie
|
||||
{
|
||||
write: function write(name, value, expires, path, domain, secure, sameSite) {
|
||||
if (typeof document === 'undefined') return;
|
||||
var cookie = ["".concat(name, "=").concat(encodeURIComponent(value))];
|
||||
if (utils$1.isNumber(expires)) {
|
||||
cookie.push("expires=".concat(new Date(expires).toUTCString()));
|
||||
}
|
||||
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));
|
||||
}
|
||||
write: function write(name, value, expires, path, domain, secure) {
|
||||
var cookie = [name + '=' + encodeURIComponent(value)];
|
||||
utils$1.isNumber(expires) && cookie.push('expires=' + new Date(expires).toGMTString());
|
||||
utils$1.isString(path) && cookie.push('path=' + path);
|
||||
utils$1.isString(domain) && cookie.push('domain=' + domain);
|
||||
secure === true && cookie.push('secure');
|
||||
document.cookie = cookie.join('; ');
|
||||
},
|
||||
read: function read(name) {
|
||||
if (typeof document === 'undefined') return null;
|
||||
var match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
var match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'));
|
||||
return match ? decodeURIComponent(match[3]) : null;
|
||||
},
|
||||
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.
|
||||
@@ -3371,9 +3353,11 @@
|
||||
var DEFAULT_CHUNK_SIZE = 64 * 1024;
|
||||
var isFunction = utils$1.isFunction;
|
||||
var globalFetchAPI = function (_ref) {
|
||||
var Request = _ref.Request,
|
||||
var fetch = _ref.fetch,
|
||||
Request = _ref.Request,
|
||||
Response = _ref.Response;
|
||||
return {
|
||||
fetch: fetch,
|
||||
Request: Request,
|
||||
Response: Response
|
||||
};
|
||||
@@ -3392,14 +3376,11 @@
|
||||
}
|
||||
};
|
||||
var factory = function factory(env) {
|
||||
env = utils$1.merge.call({
|
||||
skipUndefined: true
|
||||
}, globalFetchAPI, env);
|
||||
var _env = env,
|
||||
envFetch = _env.fetch,
|
||||
Request = _env.Request,
|
||||
Response = _env.Response;
|
||||
var isFetchSupported = envFetch ? isFunction(envFetch) : typeof fetch === 'function';
|
||||
var _Object$assign = Object.assign({}, globalFetchAPI, env),
|
||||
fetch = _Object$assign.fetch,
|
||||
Request = _Object$assign.Request,
|
||||
Response = _Object$assign.Response;
|
||||
var isFetchSupported = isFunction(fetch);
|
||||
var isRequestSupported = isFunction(Request);
|
||||
var isResponseSupported = isFunction(Response);
|
||||
if (!isFetchSupported) {
|
||||
@@ -3540,32 +3521,31 @@
|
||||
}();
|
||||
return /*#__PURE__*/function () {
|
||||
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, _fetch, 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, 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) {
|
||||
while (1) switch (_context4.prev = _context4.next) {
|
||||
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;
|
||||
_fetch = envFetch || fetch;
|
||||
responseType = responseType ? (responseType + '').toLowerCase() : 'text';
|
||||
composedSignal = composeSignals$1([signal, cancelToken && cancelToken.toAbortSignal()], timeout);
|
||||
request = null;
|
||||
unsubscribe = composedSignal && composedSignal.unsubscribe && function () {
|
||||
composedSignal.unsubscribe();
|
||||
};
|
||||
_context4.prev = 6;
|
||||
_context4.prev = 5;
|
||||
_context4.t0 = onUploadProgress && supportsRequestStream && method !== 'get' && method !== 'head';
|
||||
if (!_context4.t0) {
|
||||
_context4.next = 13;
|
||||
_context4.next = 12;
|
||||
break;
|
||||
}
|
||||
_context4.next = 11;
|
||||
_context4.next = 10;
|
||||
return resolveBodyLength(headers, data);
|
||||
case 11:
|
||||
case 10:
|
||||
_context4.t1 = requestContentLength = _context4.sent;
|
||||
_context4.t0 = _context4.t1 !== 0;
|
||||
case 13:
|
||||
case 12:
|
||||
if (!_context4.t0) {
|
||||
_context4.next = 17;
|
||||
_context4.next = 16;
|
||||
break;
|
||||
}
|
||||
_request = new Request(url, {
|
||||
@@ -3580,7 +3560,7 @@
|
||||
_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);
|
||||
}
|
||||
case 17:
|
||||
case 16:
|
||||
if (!utils$1.isString(withCredentials)) {
|
||||
withCredentials = withCredentials ? 'include' : 'omit';
|
||||
}
|
||||
@@ -3597,9 +3577,9 @@
|
||||
credentials: isCredentialsSupported ? withCredentials : undefined
|
||||
});
|
||||
request = isRequestSupported && new Request(url, resolvedOptions);
|
||||
_context4.next = 23;
|
||||
return isRequestSupported ? _fetch(request, fetchOptions) : _fetch(url, resolvedOptions);
|
||||
case 23:
|
||||
_context4.next = 22;
|
||||
return isRequestSupported ? fetch(request, fetchOptions) : fetch(url, resolvedOptions);
|
||||
case 22:
|
||||
response = _context4.sent;
|
||||
isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response');
|
||||
if (supportsResponseStream && (onDownloadProgress || isStreamResponse && unsubscribe)) {
|
||||
@@ -3615,12 +3595,12 @@
|
||||
}), options);
|
||||
}
|
||||
responseType = responseType || 'text';
|
||||
_context4.next = 29;
|
||||
_context4.next = 28;
|
||||
return resolvers[utils$1.findKey(resolvers, responseType) || 'text'](response, config);
|
||||
case 29:
|
||||
case 28:
|
||||
responseData = _context4.sent;
|
||||
!isStreamResponse && unsubscribe && unsubscribe();
|
||||
_context4.next = 33;
|
||||
_context4.next = 32;
|
||||
return new Promise(function (resolve, reject) {
|
||||
settle(resolve, reject, {
|
||||
data: responseData,
|
||||
@@ -3631,26 +3611,26 @@
|
||||
request: request
|
||||
});
|
||||
});
|
||||
case 33:
|
||||
case 32:
|
||||
return _context4.abrupt("return", _context4.sent);
|
||||
case 36:
|
||||
_context4.prev = 36;
|
||||
_context4.t2 = _context4["catch"](6);
|
||||
case 35:
|
||||
_context4.prev = 35;
|
||||
_context4.t2 = _context4["catch"](5);
|
||||
unsubscribe && unsubscribe();
|
||||
if (!(_context4.t2 && _context4.t2.name === 'TypeError' && /Load failed|fetch/i.test(_context4.t2.message))) {
|
||||
_context4.next = 41;
|
||||
_context4.next = 40;
|
||||
break;
|
||||
}
|
||||
throw Object.assign(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request), {
|
||||
cause: _context4.t2.cause || _context4.t2
|
||||
});
|
||||
case 41:
|
||||
case 40:
|
||||
throw AxiosError.from(_context4.t2, _context4.t2 && _context4.t2.code, config, request);
|
||||
case 42:
|
||||
case 41:
|
||||
case "end":
|
||||
return _context4.stop();
|
||||
}
|
||||
}, _callee4, null, [[6, 36]]);
|
||||
}, _callee4, null, [[5, 35]]);
|
||||
}));
|
||||
return function (_x5) {
|
||||
return _ref5.apply(this, arguments);
|
||||
@@ -3659,7 +3639,9 @@
|
||||
};
|
||||
var seedCache = new Map();
|
||||
var getFetch = function getFetch(config) {
|
||||
var env = config && config.env || {};
|
||||
var env = utils$1.merge.call({
|
||||
skipUndefined: true
|
||||
}, globalFetchAPI, config ? config.env : null);
|
||||
var fetch = env.fetch,
|
||||
Request = env.Request,
|
||||
Response = env.Response;
|
||||
@@ -3679,15 +3661,6 @@
|
||||
};
|
||||
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 = {
|
||||
http: httpAdapter,
|
||||
xhr: xhrAdapter,
|
||||
@@ -3695,8 +3668,6 @@
|
||||
get: getFetch
|
||||
}
|
||||
};
|
||||
|
||||
// Assign adapter names for easier debugging and identification
|
||||
utils$1.forEach(knownAdapters, function (fn, value) {
|
||||
if (fn) {
|
||||
try {
|
||||
@@ -3711,85 +3682,47 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Render a rejection reason string for unknown or unsupported adapters
|
||||
*
|
||||
* @param {string} reason
|
||||
* @returns {string}
|
||||
*/
|
||||
var renderReason = function renderReason(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) {
|
||||
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 = {
|
||||
/**
|
||||
* Resolve an adapter from a list of adapter names or functions.
|
||||
* @type {Function}
|
||||
*/
|
||||
getAdapter: getAdapter,
|
||||
/**
|
||||
* Exposes all known adapters
|
||||
* @type {Object<string, Function|Object>}
|
||||
*/
|
||||
getAdapter: 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;
|
||||
},
|
||||
adapters: knownAdapters
|
||||
};
|
||||
|
||||
@@ -3847,7 +3780,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
var VERSION = "1.13.2";
|
||||
var VERSION = "1.12.0";
|
||||
|
||||
var validators$1 = {};
|
||||
|
||||
@@ -4090,6 +4023,7 @@
|
||||
}
|
||||
len = requestInterceptorChain.length;
|
||||
var newConfig = config;
|
||||
i = 0;
|
||||
while (i < len) {
|
||||
var onFulfilled = requestInterceptorChain[i++];
|
||||
var onRejected = requestInterceptorChain[i++];
|
||||
@@ -4383,13 +4317,7 @@
|
||||
InsufficientStorage: 507,
|
||||
LoopDetected: 508,
|
||||
NotExtended: 510,
|
||||
NetworkAuthenticationRequired: 511,
|
||||
WebServerIsDown: 521,
|
||||
ConnectionTimedOut: 522,
|
||||
OriginIsUnreachable: 523,
|
||||
TimeoutOccurred: 524,
|
||||
SslHandshakeFailed: 525,
|
||||
InvalidSslCertificate: 526
|
||||
NetworkAuthenticationRequired: 511
|
||||
};
|
||||
Object.entries(HttpStatusCode).forEach(function (_ref) {
|
||||
var _ref2 = _slicedToArray(_ref, 2),
|
||||
|
||||
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+499
-1677
File diff suppressed because it is too large
Load Diff
+1048
-1208
File diff suppressed because it is too large
Load Diff
Vendored
+22
-38
@@ -1119,19 +1119,16 @@ body.desktop .q-checkbox--dense:not(.disabled):focus .q-checkbox__inner:before,
|
||||
.q-chip--colored .q-chip__icon, .q-chip--dark .q-chip__icon {
|
||||
color: inherit;
|
||||
}
|
||||
.q-chip--outline {
|
||||
background: transparent !important;
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
.q-chip .q-avatar {
|
||||
font-size: 2em;
|
||||
margin-left: -0.45em;
|
||||
margin-right: 0.2em;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.q-chip--outline {
|
||||
background: transparent !important;
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
.q-chip--outline .q-avatar {
|
||||
margin-left: calc(-0.45em - 1px);
|
||||
}
|
||||
.q-chip--selected .q-avatar {
|
||||
display: none;
|
||||
}
|
||||
@@ -1242,6 +1239,10 @@ body.desktop.body--dark .q-chip--clickable:focus {
|
||||
min-width: 180px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12);
|
||||
/* Saturation Tab */
|
||||
/* Tune Tab */
|
||||
/* Palette Tab */
|
||||
/* Generic */
|
||||
}
|
||||
.q-color-picker .q-tab {
|
||||
padding: 0 !important;
|
||||
@@ -1309,9 +1310,6 @@ body.desktop.body--dark .q-chip--clickable:focus {
|
||||
.q-color-picker__footer .q-tab--inactive {
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.15) 25%, rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
.q-color-picker {
|
||||
/* Saturation Tab */
|
||||
}
|
||||
.q-color-picker__spectrum {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -1362,9 +1360,6 @@ body.desktop.body--dark .q-chip--clickable:focus {
|
||||
.q-color-picker__sliders .q-slider--active path {
|
||||
stroke-width: 3px;
|
||||
}
|
||||
.q-color-picker {
|
||||
/* Tune Tab */
|
||||
}
|
||||
.q-color-picker__tune-tab .q-slider {
|
||||
margin-left: 18px;
|
||||
margin-right: 18px;
|
||||
@@ -1375,9 +1370,6 @@ body.desktop.body--dark .q-chip--clickable:focus {
|
||||
border-radius: 4px;
|
||||
width: 3.5em;
|
||||
}
|
||||
.q-color-picker {
|
||||
/* Palette Tab */
|
||||
}
|
||||
.q-color-picker__palette-tab {
|
||||
padding: 0 !important;
|
||||
}
|
||||
@@ -1388,9 +1380,6 @@ body.desktop.body--dark .q-chip--clickable:focus {
|
||||
padding-bottom: 10%;
|
||||
width: 10% !important;
|
||||
}
|
||||
.q-color-picker {
|
||||
/* Generic */
|
||||
}
|
||||
.q-color-picker input {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
@@ -2219,6 +2208,12 @@ body.q-ios-padding .q-dialog__inner > div {
|
||||
-webkit-animation-name: q-autofill;
|
||||
-webkit-animation-fill-mode: both;
|
||||
}
|
||||
.q-field__native:-webkit-autofill + .q-field__label, .q-field__input:-webkit-autofill + .q-field__label {
|
||||
transform: translateY(-40%) scale(0.75);
|
||||
}
|
||||
.q-field__native[type=color] + .q-field__label, .q-field__native[type=date] + .q-field__label, .q-field__native[type=datetime-local] + .q-field__label, .q-field__native[type=month] + .q-field__label, .q-field__native[type=time] + .q-field__label, .q-field__native[type=week] + .q-field__label, .q-field__input[type=color] + .q-field__label, .q-field__input[type=date] + .q-field__label, .q-field__input[type=datetime-local] + .q-field__label, .q-field__input[type=month] + .q-field__label, .q-field__input[type=time] + .q-field__label, .q-field__input[type=week] + .q-field__label {
|
||||
transform: translateY(-40%) scale(0.75);
|
||||
}
|
||||
.q-field__native:invalid, .q-field__input:invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -2278,15 +2273,6 @@ body.q-ios-padding .q-dialog__inner > div {
|
||||
transition: transform 0.36s cubic-bezier(0.4, 0, 0.2, 1), max-width 0.324s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.q-field__label:has(+ :is(.q-field__native, .q-field__input):is(:-webkit-autofill,
|
||||
[type=color],
|
||||
[type=date],
|
||||
[type=datetime-local],
|
||||
[type=month],
|
||||
[type=time],
|
||||
[type=week])) {
|
||||
transform: translateY(-40%) scale(0.75);
|
||||
}
|
||||
.q-field--float .q-field__label {
|
||||
max-width: 133%;
|
||||
transform: translateY(-40%) scale(0.75);
|
||||
@@ -2505,13 +2491,10 @@ body.q-ios-padding .q-dialog__inner > div {
|
||||
.q-field--dense.q-field--float .q-field__label {
|
||||
transform: translateY(-30%) scale(0.75);
|
||||
}
|
||||
.q-field--dense .q-field__label:has(+ :is(.q-field__native, .q-field__input):is(:-webkit-autofill,
|
||||
[type=color],
|
||||
[type=date],
|
||||
[type=datetime-local],
|
||||
[type=month],
|
||||
[type=time],
|
||||
[type=week])) {
|
||||
.q-field--dense .q-field__native:-webkit-autofill + .q-field__label, .q-field--dense .q-field__input:-webkit-autofill + .q-field__label {
|
||||
transform: translateY(-30%) scale(0.75);
|
||||
}
|
||||
.q-field--dense .q-field__native[type=color] + .q-field__label, .q-field--dense .q-field__native[type=date] + .q-field__label, .q-field--dense .q-field__native[type=datetime-local] + .q-field__label, .q-field--dense .q-field__native[type=month] + .q-field__label, .q-field--dense .q-field__native[type=time] + .q-field__label, .q-field--dense .q-field__native[type=week] + .q-field__label, .q-field--dense .q-field__input[type=color] + .q-field__label, .q-field--dense .q-field__input[type=date] + .q-field__label, .q-field--dense .q-field__input[type=datetime-local] + .q-field__label, .q-field--dense .q-field__input[type=month] + .q-field__label, .q-field--dense .q-field__input[type=time] + .q-field__label, .q-field--dense .q-field__input[type=week] + .q-field__label {
|
||||
transform: translateY(-30%) scale(0.75);
|
||||
}
|
||||
.q-field--borderless .q-field__bottom, .q-field--borderless.q-field--dense .q-field__control, .q-field--standard .q-field__bottom, .q-field--standard.q-field--dense .q-field__control {
|
||||
@@ -3401,7 +3384,7 @@ body.desktop .q-radio--dense:not(.disabled):focus .q-radio__inner:before, body.d
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
position: relative;
|
||||
opacity: 0.4;
|
||||
transition: transform 0.2s ease-in, opacity 0.2s ease-in, color 0.2s ease-in;
|
||||
transition: transform 0.2s ease-in, opacity 0.2s ease-in;
|
||||
}
|
||||
.q-rating__icon--hovered {
|
||||
transform: scale(1.3);
|
||||
@@ -4721,7 +4704,7 @@ body.desktop .q-slider.q-slider--enabled .q-slider__track-container:hover .q-sli
|
||||
|
||||
/* * On light background
|
||||
* */
|
||||
.q-table__bottom:not(.q-table__bottom--nodata) {
|
||||
.q-table__bottom {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
@@ -11332,7 +11315,8 @@ p {
|
||||
}
|
||||
.text-justify {
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
hyphens: auto;
|
||||
}
|
||||
.text-italic {
|
||||
font-style: italic;
|
||||
|
||||
+55
-55
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user