Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0ff997ec1 | ||
|
|
eb55170905 | ||
|
|
dc466ae738 | ||
|
|
dd5fe8b7a2 | ||
|
|
5196ee6a15 | ||
|
|
3498cfb5b8 | ||
|
|
30ffe437a0 | ||
|
|
574641d432 | ||
|
|
90d14629d9 | ||
|
|
3be06269c2 | ||
|
|
d7723696d8 | ||
|
|
91f7b7730d | ||
|
|
b2aaf97cf3 |
+1
-3
@@ -55,7 +55,7 @@ LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
|
|||||||
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
|
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
# which fundingsources are allowed in the admin ui
|
# which fundingsources are allowed in the admin ui
|
||||||
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet, SparkWallet, LightsparkSparkWallet"
|
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet"
|
||||||
|
|
||||||
# uvicorn variable, allow https behind a proxy
|
# uvicorn variable, allow https behind a proxy
|
||||||
# IMPORTANT: this also needs the webserver to be configured to forward the headers
|
# IMPORTANT: this also needs the webserver to be configured to forward the headers
|
||||||
@@ -187,8 +187,6 @@ BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
|||||||
# HEXSTRING instead of path also possible
|
# HEXSTRING instead of path also possible
|
||||||
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
||||||
|
|
||||||
# TODO: add Spark
|
|
||||||
|
|
||||||
# StrikeWallet
|
# StrikeWallet
|
||||||
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
||||||
STRIKE_API_KEY=YOUR_STRIKE_API_KEY
|
STRIKE_API_KEY=YOUR_STRIKE_API_KEY
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
|
|
||||||
|
test-frontend:
|
||||||
|
needs: [ lint ]
|
||||||
|
uses: ./.github/workflows/make.yml
|
||||||
|
with:
|
||||||
|
make: test-frontend
|
||||||
|
npm: true
|
||||||
|
|
||||||
test-api:
|
test-api:
|
||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: tests
|
name: tests pytest
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ dev:
|
|||||||
docker:
|
docker:
|
||||||
docker build -t lnbits/lnbits .
|
docker build -t lnbits/lnbits .
|
||||||
|
|
||||||
|
test-frontend:
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
|
|
||||||
test-wallets:
|
test-wallets:
|
||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
|
|||||||
| **LN.tips** | Custodial/Self-Custodial | Depends on provider | Medium | ❌ | Low | Provider-managed | Moderate | Low | Transaction fees may apply | Medium | Simple hosted service; use LN.tips API as your backend. |
|
| **LN.tips** | Custodial/Self-Custodial | Depends on provider | Medium | ❌ | Low | Provider-managed | Moderate | Low | Transaction fees may apply | Medium | Simple hosted service; use LN.tips API as your backend. |
|
||||||
| **Fake Wallet** | Testing (simulated) | ❌ | Low | ❌ | N/A | N/A | Easy | Low | None (test only) | N/A | For testing only; mints accounting units in LNbits (no real sats, unit name configurable). |
|
| **Fake Wallet** | Testing (simulated) | ❌ | Low | ❌ | N/A | N/A | Easy | Low | None (test only) | N/A | For testing only; mints accounting units in LNbits (no real sats, unit name configurable). |
|
||||||
|
|
||||||
## TODO: add Spark
|
---
|
||||||
|
|
||||||
### Notes for readers
|
### Notes for readers
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ A backend wallet is selected and configured entirely through LNbits environment
|
|||||||
|
|
||||||
### CLNRest (using [runes](https://docs.corelightning.org/reference/lightning-createrune))
|
### CLNRest (using [runes](https://docs.corelightning.org/reference/lightning-createrune))
|
||||||
|
|
||||||
[Core Lightning REST API docs](https://docs.corelightning.org/docs/rest)
|
[Core Lightning REST API docs](https://docs.corelightning.org/docs/rest)
|
||||||
Should also work with the [Rust version of CLNRest](https://github.com/daywalker90/clnrest-rs)
|
Should also work with the [Rust version of CLNRest](https://github.com/daywalker90/clnrest-rs)
|
||||||
|
|
||||||
**Environment variables**
|
**Environment variables**
|
||||||
@@ -338,8 +338,6 @@ Configure in the admin UI or via env vars:
|
|||||||
|
|
||||||
<a id="strike"></a>
|
<a id="strike"></a>
|
||||||
|
|
||||||
## TODO: add Spark
|
|
||||||
|
|
||||||
## Strike (alpha)
|
## Strike (alpha)
|
||||||
|
|
||||||
Custodial provider integrated via **Strike OAuth Connect** (OAuth 2.0 / OIDC). Authenticate a Strike user in your app, then call Strike APIs on the user’s behalf once scopes are granted. Requires a Strike business account, registered OAuth client, minimal scopes, and login/logout redirect URLs.
|
Custodial provider integrated via **Strike OAuth Connect** (OAuth 2.0 / OIDC). Authenticate a Strike user in your app, then call Strike APIs on the user’s behalf once scopes are granted. Requires a Strike business account, registered OAuth client, minimal scopes, and login/logout redirect URLs.
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from datetime import datetime, timezone
|
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -305,16 +304,8 @@ async def update_payment_checking_id(
|
|||||||
checking_id: str, new_checking_id: str, conn: Connection | None = None
|
checking_id: str, new_checking_id: str, conn: Connection | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
"UPDATE apipayments SET checking_id = :new_id WHERE checking_id = :old_id",
|
||||||
UPDATE apipayments
|
{"new_id": new_checking_id, "old_id": checking_id},
|
||||||
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder('now')}
|
|
||||||
WHERE checking_id = :old_id
|
|
||||||
""", # noqa: S608
|
|
||||||
{
|
|
||||||
"new_id": new_checking_id,
|
|
||||||
"old_id": checking_id,
|
|
||||||
"now": int(time()),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -323,7 +314,6 @@ async def update_payment(
|
|||||||
new_checking_id: str | None = None,
|
new_checking_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
payment.updated_at = datetime.now(timezone.utc)
|
|
||||||
await (conn or db).update(
|
await (conn or db).update(
|
||||||
"apipayments", payment, "WHERE checking_id = :checking_id"
|
"apipayments", payment, "WHERE checking_id = :checking_id"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,13 +45,17 @@ async def update_wallet(
|
|||||||
|
|
||||||
|
|
||||||
async def delete_wallet(
|
async def delete_wallet(
|
||||||
|
*,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
deleted: bool = True,
|
deleted: bool = True,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
_clear_wallet_cache(wallet_id)
|
|
||||||
now = int(time())
|
now = int(time())
|
||||||
|
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
||||||
|
if cached_wallet:
|
||||||
|
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
||||||
|
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
||||||
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
@@ -65,7 +69,6 @@ async def delete_wallet(
|
|||||||
|
|
||||||
|
|
||||||
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
||||||
_clear_wallet_cache(wallet_id)
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE FROM wallets WHERE id = :wallet",
|
"DELETE FROM wallets WHERE id = :wallet",
|
||||||
{"wallet": wallet_id},
|
{"wallet": wallet_id},
|
||||||
@@ -75,7 +78,6 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
|
|||||||
async def delete_wallet_by_id(
|
async def delete_wallet_by_id(
|
||||||
wallet_id: str, conn: Connection | None = None
|
wallet_id: str, conn: Connection | None = None
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
_clear_wallet_cache(wallet_id)
|
|
||||||
now = int(time())
|
now = int(time())
|
||||||
result = await (conn or db).execute(
|
result = await (conn or db).execute(
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
@@ -292,10 +294,3 @@ async def get_total_balance(conn: Connection | None = None):
|
|||||||
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
||||||
row = result.mappings().first()
|
row = result.mappings().first()
|
||||||
return row.get("balance", 0) or 0
|
return row.get("balance", 0) or 0
|
||||||
|
|
||||||
|
|
||||||
def _clear_wallet_cache(wallet_id):
|
|
||||||
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
|
||||||
if cached_wallet:
|
|
||||||
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
|
||||||
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from typing import Any
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
download_url,
|
download_url,
|
||||||
@@ -269,30 +269,10 @@ class ExtensionRelease(BaseModel):
|
|||||||
async def get_github_releases(cls, org: str, repo: str) -> list[ExtensionRelease]:
|
async def get_github_releases(cls, org: str, repo: str) -> list[ExtensionRelease]:
|
||||||
try:
|
try:
|
||||||
github_releases = await cls.fetch_github_releases(org, repo)
|
github_releases = await cls.fetch_github_releases(org, repo)
|
||||||
extension_releases = [
|
return [
|
||||||
ExtensionRelease.from_github_release(f"{org}/{repo}", r)
|
ExtensionRelease.from_github_release(f"{org}/{repo}", r)
|
||||||
for r in github_releases
|
for r in github_releases
|
||||||
]
|
]
|
||||||
for release in extension_releases:
|
|
||||||
if not release.details_link:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
config = await ExtensionConfig.fetch_github_release_config(
|
|
||||||
org, repo, release.version
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(e)
|
|
||||||
config = None
|
|
||||||
if not config:
|
|
||||||
continue
|
|
||||||
|
|
||||||
release.min_lnbits_version = config.min_lnbits_version
|
|
||||||
release.max_lnbits_version = config.max_lnbits_version
|
|
||||||
release.is_version_compatible = config.is_version_compatible()
|
|
||||||
|
|
||||||
release.icon = icon_to_github_url(f"{org}/{repo}", config.tile)
|
|
||||||
|
|
||||||
return extension_releases
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
return []
|
return []
|
||||||
@@ -797,32 +777,6 @@ class ExtensionDetailsRequest(BaseModel):
|
|||||||
version: str
|
version: str
|
||||||
|
|
||||||
|
|
||||||
class ExtensionReviewsStatus(BaseModel):
|
|
||||||
tag: str
|
|
||||||
avg_rating: float
|
|
||||||
review_count: int
|
|
||||||
|
|
||||||
|
|
||||||
class CreateExtensionReview(BaseModel):
|
|
||||||
tag: str
|
|
||||||
name: str | None = Field(None)
|
|
||||||
rating: int = Field(..., ge=0, le=1000)
|
|
||||||
comment: str | None = Field(None)
|
|
||||||
|
|
||||||
|
|
||||||
class ExtensionReviewPaymentRequest(BaseModel):
|
|
||||||
payment_hash: str
|
|
||||||
payment_request: str
|
|
||||||
|
|
||||||
|
|
||||||
class ExtensionReview(BaseModel):
|
|
||||||
id: str
|
|
||||||
name: str | None = Field(default=None)
|
|
||||||
tag: str | None = Field(default=None)
|
|
||||||
rating: int = Field(default=0, ge=0, le=1000)
|
|
||||||
comment: str | None = Field(default=None)
|
|
||||||
|
|
||||||
|
|
||||||
async def github_api_get(url: str, error_msg: str | None) -> Any:
|
async def github_api_get(url: str, error_msg: str | None) -> Any:
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
if settings.lnbits_ext_github_token:
|
if settings.lnbits_ext_github_token:
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import random
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
@@ -57,29 +55,6 @@ class DataField(BaseModel):
|
|||||||
field_type += ' = "sat"'
|
field_type += ' = "sat"'
|
||||||
return f"{field_name}: {field_type}"
|
return f"{field_name}: {field_type}"
|
||||||
|
|
||||||
def field_to_random_value(self) -> str:
|
|
||||||
field_name = camel_to_snake(self.name)
|
|
||||||
field_value: Any = f'"{self.type}"'
|
|
||||||
if self.type == "json":
|
|
||||||
field_value = '"{}"'
|
|
||||||
elif self.type == "wallet":
|
|
||||||
field_value = f'"{uuid.uuid4()}"'
|
|
||||||
elif self.type == "currency":
|
|
||||||
field_value = '"sat"'
|
|
||||||
elif self.type in ["str", "text"]:
|
|
||||||
field_value = f'"{field_name}_{urlsafe_short_hash()}"'
|
|
||||||
elif self.type == "int":
|
|
||||||
field_value = random.randint(1, 100) # noqa: S311
|
|
||||||
elif self.type == "float":
|
|
||||||
field_value = random.uniform(1.0, 100.0) # noqa: S311
|
|
||||||
elif self.type == "bool":
|
|
||||||
field_value = random.choice([True, False]) # noqa: S311
|
|
||||||
elif self.type == "datetime":
|
|
||||||
random_days = random.randint(-30, 30) # noqa: S311
|
|
||||||
random_date = datetime.now(timezone.utc) - timedelta(days=random_days)
|
|
||||||
field_value = f"""datetime.fromisoformat("{random_date.isoformat()}")"""
|
|
||||||
return f"{field_name} = {field_value},"
|
|
||||||
|
|
||||||
def field_to_js(self) -> str:
|
def field_to_js(self) -> str:
|
||||||
field_name = camel_to_snake(self.name)
|
field_name = camel_to_snake(self.name)
|
||||||
default_value = "null"
|
default_value = "null"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import io
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
from loguru import logger
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from lnbits.core.crud.assets import create_asset, get_user_assets_count
|
from lnbits.core.crud.assets import create_asset, get_user_assets_count
|
||||||
@@ -30,7 +29,17 @@ async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) ->
|
|||||||
f"File limit of {settings.lnbits_max_asset_size_mb}MB exceeded."
|
f"File limit of {settings.lnbits_max_asset_size_mb}MB exceeded."
|
||||||
)
|
)
|
||||||
|
|
||||||
thumb_buffer = thumbnail_from_bytes(contents)
|
image = Image.open(io.BytesIO(contents))
|
||||||
|
|
||||||
|
thumbnail_width = min(256, settings.lnbits_asset_thumbnail_width)
|
||||||
|
thumbnail_height = min(256, settings.lnbits_asset_thumbnail_height)
|
||||||
|
image.thumbnail((thumbnail_width, thumbnail_height))
|
||||||
|
|
||||||
|
# Save thumbnail to an in-memory buffer
|
||||||
|
thumb_buffer = io.BytesIO()
|
||||||
|
thumbnail_format = settings.lnbits_asset_thumbnail_format or "PNG"
|
||||||
|
image.save(thumb_buffer, format=thumbnail_format)
|
||||||
|
thumb_buffer.seek(0)
|
||||||
|
|
||||||
asset = Asset(
|
asset = Asset(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
@@ -39,32 +48,9 @@ async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) ->
|
|||||||
is_public=is_public,
|
is_public=is_public,
|
||||||
name=file.filename or "unnamed",
|
name=file.filename or "unnamed",
|
||||||
size_bytes=len(contents),
|
size_bytes=len(contents),
|
||||||
thumbnail_base64=(
|
thumbnail_base64=base64.b64encode(thumb_buffer.getvalue()).decode("utf-8"),
|
||||||
base64.b64encode(thumb_buffer.getvalue()).decode("utf-8")
|
|
||||||
if thumb_buffer
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
data=contents,
|
data=contents,
|
||||||
)
|
)
|
||||||
|
|
||||||
await create_asset(asset)
|
await create_asset(asset)
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
|
||||||
def thumbnail_from_bytes(contents: bytes) -> io.BytesIO | None:
|
|
||||||
try:
|
|
||||||
image = Image.open(io.BytesIO(contents))
|
|
||||||
|
|
||||||
thumbnail_width = min(256, settings.lnbits_asset_thumbnail_width)
|
|
||||||
thumbnail_height = min(256, settings.lnbits_asset_thumbnail_height)
|
|
||||||
image.thumbnail((thumbnail_width, thumbnail_height))
|
|
||||||
|
|
||||||
# Save thumbnail to an in-memory buffer
|
|
||||||
thumb_buffer = io.BytesIO()
|
|
||||||
thumbnail_format = settings.lnbits_asset_thumbnail_format or "PNG"
|
|
||||||
image.save(thumb_buffer, format=thumbnail_format)
|
|
||||||
thumb_buffer.seek(0)
|
|
||||||
return thumb_buffer
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning(f"Failed to create thumbnail: {exc}")
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from lnbits.helpers import (
|
|||||||
camel_to_words,
|
camel_to_words,
|
||||||
download_url,
|
download_url,
|
||||||
lowercase_first_letter,
|
lowercase_first_letter,
|
||||||
snake_to_camel,
|
|
||||||
)
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
@@ -196,29 +195,13 @@ def _copy_ext_stub_to_build_dir(
|
|||||||
ext_build_dir = Path(working_dir, new_ext_id, working_dir_name, new_ext_id)
|
ext_build_dir = Path(working_dir, new_ext_id, working_dir_name, new_ext_id)
|
||||||
shutil.rmtree(ext_build_dir, True)
|
shutil.rmtree(ext_build_dir, True)
|
||||||
|
|
||||||
shutil.copytree(
|
shutil.copytree(ext_stub_cache_dir, ext_build_dir)
|
||||||
ext_stub_cache_dir,
|
|
||||||
ext_build_dir,
|
|
||||||
ignore=shutil.ignore_patterns(
|
|
||||||
"__pycache__",
|
|
||||||
".git",
|
|
||||||
".env",
|
|
||||||
".venv",
|
|
||||||
"*.env",
|
|
||||||
"*.log",
|
|
||||||
"node_modules",
|
|
||||||
".mypy_cache",
|
|
||||||
".pytest_cache",
|
|
||||||
".ruff_cache",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return ext_build_dir
|
return ext_build_dir
|
||||||
|
|
||||||
|
|
||||||
def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None:
|
def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None:
|
||||||
parsed_data = _parse_extension_data(data)
|
parsed_data = _parse_extension_data(data)
|
||||||
test_files = [f"tests/{p.name}" for p in Path(ext_stub_dir, "tests").glob("*.py")]
|
for py_file in py_files:
|
||||||
for py_file in py_files + test_files:
|
|
||||||
template_path = Path(ext_stub_dir, py_file).as_posix()
|
template_path = Path(ext_stub_dir, py_file).as_posix()
|
||||||
rederer = _render_file(template_path, parsed_data)
|
rederer = _render_file(template_path, parsed_data)
|
||||||
with open(template_path, "w", encoding="utf-8") as f:
|
with open(template_path, "w", encoding="utf-8") as f:
|
||||||
@@ -226,7 +209,7 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
|||||||
|
|
||||||
_remove_lines_with_string(template_path, remove_line_marker)
|
_remove_lines_with_string(template_path, remove_line_marker)
|
||||||
|
|
||||||
template_path = Path(ext_stub_dir, "static", "index.js").as_posix()
|
template_path = Path(ext_stub_dir, "static", "js", "index.js").as_posix()
|
||||||
rederer = _render_file(
|
rederer = _render_file(
|
||||||
template_path, {"preview": data.preview_action, **parsed_data}
|
template_path, {"preview": data.preview_action, **parsed_data}
|
||||||
)
|
)
|
||||||
@@ -251,7 +234,9 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
|||||||
"settingsFormDialog.data",
|
"settingsFormDialog.data",
|
||||||
ext_stub_dir,
|
ext_stub_dir,
|
||||||
)
|
)
|
||||||
template_path = Path(ext_stub_dir, "static", "index.vue").as_posix()
|
template_path = Path(
|
||||||
|
ext_stub_dir, "templates", "extension_builder_stub", "index.html"
|
||||||
|
).as_posix()
|
||||||
rederer = _render_file(
|
rederer = _render_file(
|
||||||
template_path,
|
template_path,
|
||||||
{
|
{
|
||||||
@@ -278,12 +263,12 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
|||||||
"publicClientData",
|
"publicClientData",
|
||||||
ext_stub_dir,
|
ext_stub_dir,
|
||||||
)
|
)
|
||||||
public_template_path = Path(ext_stub_dir, "static", "public_page.vue")
|
public_template_path = Path(
|
||||||
public_component_path = Path(ext_stub_dir, "static", "public_page.js")
|
ext_stub_dir, "templates", "extension_builder_stub", "public_page.html"
|
||||||
|
)
|
||||||
template_path = public_template_path.as_posix()
|
template_path = public_template_path.as_posix()
|
||||||
if not data.public_page.has_public_page:
|
if not data.public_page.has_public_page:
|
||||||
public_template_path.unlink(missing_ok=True)
|
public_template_path.unlink(missing_ok=True)
|
||||||
public_component_path.unlink(missing_ok=True)
|
|
||||||
else:
|
else:
|
||||||
rederer = _render_file(
|
rederer = _render_file(
|
||||||
template_path,
|
template_path,
|
||||||
@@ -323,10 +308,8 @@ def zip_directory(source_dir, zip_path):
|
|||||||
|
|
||||||
def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) -> None:
|
def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) -> None:
|
||||||
extension_dir_path = extension_dir.as_posix()
|
extension_dir_path = extension_dir.as_posix()
|
||||||
# the order of fields is important, do not chage
|
|
||||||
rename_values = {
|
rename_values = {
|
||||||
"extension_builder_stub_name": data.name,
|
"extension_builder_stub_name": data.name,
|
||||||
"extension_builder_stub_camel_name": snake_to_camel(data.id, True),
|
|
||||||
"extension_builder_stub_short_description": data.short_description or "",
|
"extension_builder_stub_short_description": data.short_description or "",
|
||||||
"extension_builder_stub": data.id,
|
"extension_builder_stub": data.id,
|
||||||
"OwnerData": data.owner_data.name,
|
"OwnerData": data.owner_data.name,
|
||||||
@@ -345,7 +328,7 @@ def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) ->
|
|||||||
directory=extension_dir_path,
|
directory=extension_dir_path,
|
||||||
old_text=old_text,
|
old_text=old_text,
|
||||||
new_text=new_text,
|
new_text=new_text,
|
||||||
file_extensions=[".py", ".js", ".vue", ".html", ".md", ".json", ".toml"],
|
file_extensions=[".py", ".js", ".html", ".md", ".json", ".toml"],
|
||||||
)
|
)
|
||||||
|
|
||||||
_rename_files_and_dirs_in_directory(
|
_rename_files_and_dirs_in_directory(
|
||||||
@@ -423,7 +406,6 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
|||||||
"owner_data": {
|
"owner_data": {
|
||||||
"name": data.owner_data.name,
|
"name": data.owner_data.name,
|
||||||
"editable": data.owner_data.editable,
|
"editable": data.owner_data.editable,
|
||||||
"fields": [field.name for field in data.owner_data.fields],
|
|
||||||
"js_fields": [
|
"js_fields": [
|
||||||
field.field_to_js()
|
field.field_to_js()
|
||||||
for field in data.owner_data.fields
|
for field in data.owner_data.fields
|
||||||
@@ -450,18 +432,6 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
|||||||
],
|
],
|
||||||
"db_fields": [field.field_to_db() for field in data.owner_data.fields],
|
"db_fields": [field.field_to_db() for field in data.owner_data.fields],
|
||||||
"all_fields": [field.field_to_py() for field in data.owner_data.fields],
|
"all_fields": [field.field_to_py() for field in data.owner_data.fields],
|
||||||
"random_fields_values": [
|
|
||||||
field.field_to_random_value() for field in data.owner_data.fields
|
|
||||||
],
|
|
||||||
"public_fields": [
|
|
||||||
field.field_to_py()
|
|
||||||
for field in data.owner_data.fields
|
|
||||||
if field.name
|
|
||||||
in [
|
|
||||||
data.public_page.owner_data_fields.name,
|
|
||||||
data.public_page.owner_data_fields.description,
|
|
||||||
]
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"client_data": {
|
"client_data": {
|
||||||
"name": data.client_data.name,
|
"name": data.client_data.name,
|
||||||
@@ -487,9 +457,6 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
|||||||
],
|
],
|
||||||
"db_fields": [field.field_to_db() for field in data.client_data.fields],
|
"db_fields": [field.field_to_db() for field in data.client_data.fields],
|
||||||
"all_fields": [field.field_to_py() for field in data.client_data.fields],
|
"all_fields": [field.field_to_py() for field in data.client_data.fields],
|
||||||
"random_fields_values": [
|
|
||||||
field.field_to_random_value() for field in data.client_data.fields
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"settings_data": {
|
"settings_data": {
|
||||||
"enabled": data.settings_data.enabled,
|
"enabled": data.settings_data.enabled,
|
||||||
@@ -569,7 +536,7 @@ def _rename_files_and_dirs_in_directory(directory, old_text, new_text):
|
|||||||
logger.warning(f"Failed to rename directory {old_dir_path}: {e}")
|
logger.warning(f"Failed to rename directory {old_dir_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _is_excluded_dir(path: str) -> bool:
|
def _is_excluded_dir(path):
|
||||||
for excluded_dir in excluded_dirs:
|
for excluded_dir in excluded_dirs:
|
||||||
if path.startswith(excluded_dir):
|
if path.startswith(excluded_dir):
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ async def api_get_asset(
|
|||||||
|
|
||||||
|
|
||||||
@asset_router.get(
|
@asset_router.get(
|
||||||
"/{asset_id}/data",
|
"/{asset_id}/binary",
|
||||||
name="Get user asset data",
|
name="Get user asset binary",
|
||||||
summary="Get user asset data data by ID",
|
summary="Get user asset binary data by ID",
|
||||||
)
|
)
|
||||||
async def api_get_asset_data(
|
async def api_get_asset_binary(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
user_id: str | None = Depends(optional_user_id),
|
user_id: str | None = Depends(optional_user_id),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import json
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Annotated
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from fastapi.responses import JSONResponse, RedirectResponse
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
from fastapi_sso.sso.base import OpenID, SSOBase
|
from fastapi_sso.sso.base import OpenID, SSOBase
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -30,7 +29,6 @@ from lnbits.core.services.users import update_user_account
|
|||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
access_token_payload,
|
access_token_payload,
|
||||||
check_account_exists,
|
check_account_exists,
|
||||||
check_admin,
|
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
@@ -122,63 +120,6 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
|||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.post("/impersonate", description="Login via the User ID of another user")
|
|
||||||
async def impersonate_user(
|
|
||||||
data: LoginUsr,
|
|
||||||
user: User = Depends(check_admin),
|
|
||||||
cookie_access_token: Annotated[str | None, Cookie()] = None,
|
|
||||||
) -> JSONResponse:
|
|
||||||
if not cookie_access_token:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.UNAUTHORIZED, "Only cookie based impersonation is allowed."
|
|
||||||
)
|
|
||||||
if data.usr == user.id:
|
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "You cannot impersonate yourself.")
|
|
||||||
if settings.is_admin_user(data.usr):
|
|
||||||
# this check includes the superuser
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.FORBIDDEN, "You cannot impersonate another admin user."
|
|
||||||
)
|
|
||||||
|
|
||||||
account = await get_account(data.usr)
|
|
||||||
if not account:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
|
|
||||||
|
|
||||||
response = _auth_success_response(account.username, account.id, account.email)
|
|
||||||
|
|
||||||
max_age = settings.auth_token_expire_minutes * 60
|
|
||||||
response.set_cookie(
|
|
||||||
"admin_access_token", cookie_access_token, httponly=True, max_age=max_age
|
|
||||||
)
|
|
||||||
response.set_cookie("is_lnbits_user_impersonated", "true", max_age=max_age)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.delete(
|
|
||||||
"/impersonate", description="Stop impersonation and go back to admin"
|
|
||||||
)
|
|
||||||
async def stop_impersonate_user(
|
|
||||||
user: User = Depends(check_user_exists),
|
|
||||||
admin_access_token: Annotated[str | None, Cookie()] = None,
|
|
||||||
) -> JSONResponse:
|
|
||||||
if not admin_access_token:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.UNAUTHORIZED,
|
|
||||||
"No admin access token found to stop impersonation.",
|
|
||||||
)
|
|
||||||
response = JSONResponse(
|
|
||||||
{"access_token": admin_access_token, "token_type": "bearer"}
|
|
||||||
)
|
|
||||||
max_age = settings.auth_token_expire_minutes * 60
|
|
||||||
response.set_cookie(
|
|
||||||
"cookie_access_token", admin_access_token, httponly=True, max_age=max_age
|
|
||||||
)
|
|
||||||
response.delete_cookie("admin_access_token")
|
|
||||||
response.delete_cookie("is_access_token_expired")
|
|
||||||
response.delete_cookie("is_lnbits_user_impersonated")
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.get("/acl")
|
@auth_router.get("/acl")
|
||||||
async def api_get_user_acls(
|
async def api_get_user_acls(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import sys
|
|||||||
import traceback
|
import traceback
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
import httpx
|
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi.requests import Request
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_extensions
|
from lnbits.core.crud.extensions import get_user_extensions
|
||||||
@@ -16,14 +14,10 @@ from lnbits.core.models import (
|
|||||||
)
|
)
|
||||||
from lnbits.core.models.extensions import (
|
from lnbits.core.models.extensions import (
|
||||||
CreateExtension,
|
CreateExtension,
|
||||||
CreateExtensionReview,
|
|
||||||
Extension,
|
Extension,
|
||||||
ExtensionConfig,
|
ExtensionConfig,
|
||||||
ExtensionMeta,
|
ExtensionMeta,
|
||||||
ExtensionRelease,
|
ExtensionRelease,
|
||||||
ExtensionReview,
|
|
||||||
ExtensionReviewPaymentRequest,
|
|
||||||
ExtensionReviewsStatus,
|
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
PayToEnableInfo,
|
PayToEnableInfo,
|
||||||
ReleasePaymentInfo,
|
ReleasePaymentInfo,
|
||||||
@@ -40,7 +34,6 @@ from lnbits.core.services.extensions import (
|
|||||||
install_extension,
|
install_extension,
|
||||||
uninstall_extension,
|
uninstall_extension,
|
||||||
)
|
)
|
||||||
from lnbits.db import Page
|
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
check_account_exists,
|
check_account_exists,
|
||||||
check_account_id_exists,
|
check_account_id_exists,
|
||||||
@@ -596,49 +589,3 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
for ext in installable_exts
|
for ext in installable_exts
|
||||||
]
|
]
|
||||||
return extension_data
|
return extension_data
|
||||||
|
|
||||||
|
|
||||||
@extension_router.get(
|
|
||||||
"/reviews/tags",
|
|
||||||
dependencies=[Depends(check_account_exists)],
|
|
||||||
)
|
|
||||||
async def get_extension_reviews_tags() -> list[ExtensionReviewsStatus]:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
resp = await client.get(settings.lnbits_extensions_reviews_url + "/tags")
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
|
||||||
return [ExtensionReviewsStatus(**item) for item in data]
|
|
||||||
|
|
||||||
|
|
||||||
@extension_router.get(
|
|
||||||
"/reviews/{ext_id}",
|
|
||||||
dependencies=[Depends(check_account_exists)],
|
|
||||||
)
|
|
||||||
async def get_extension_reviews(ext_id: str, request: Request) -> Page[ExtensionReview]:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
query_string = str(request.query_params)
|
|
||||||
resp = await client.get(
|
|
||||||
settings.lnbits_extensions_reviews_url + f"/reviews/{ext_id}?{query_string}"
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
reviews = resp.json()
|
|
||||||
return Page(
|
|
||||||
data=[ExtensionReview(**item) for item in reviews["data"]],
|
|
||||||
total=reviews["total"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@extension_router.put(
|
|
||||||
"/reviews",
|
|
||||||
dependencies=[Depends(check_account_exists)],
|
|
||||||
)
|
|
||||||
async def create_extension_review(
|
|
||||||
data: CreateExtensionReview,
|
|
||||||
) -> ExtensionReviewPaymentRequest:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
resp = await client.post(
|
|
||||||
settings.lnbits_extensions_reviews_url + "/reviews", json=data.dict()
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
payment_request = resp.json()
|
|
||||||
return ExtensionReviewPaymentRequest(**payment_request)
|
|
||||||
|
|||||||
@@ -42,38 +42,54 @@ async def robots():
|
|||||||
|
|
||||||
|
|
||||||
@generic_router.get(
|
@generic_router.get(
|
||||||
"/extensions/builder/preview/{ext_id}/{resource}",
|
"/extensions/builder/preview/{ext_id}",
|
||||||
name="extensions builder",
|
name="extensions builder",
|
||||||
dependencies=[Depends(check_extension_builder)],
|
dependencies=[Depends(check_extension_builder)],
|
||||||
)
|
)
|
||||||
async def extensions_builder_preview(
|
async def extensions_builder_preview(
|
||||||
|
request: Request,
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
resource: str | None = None,
|
|
||||||
page_name: str | None = None,
|
page_name: str | None = None,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
) -> FileResponse:
|
) -> HTMLResponse:
|
||||||
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
||||||
|
html_file_name = "index.html"
|
||||||
file_name = "index"
|
|
||||||
if page_name == "public_page":
|
if page_name == "public_page":
|
||||||
file_name = "public_page"
|
html_file_name = "public_page.html"
|
||||||
|
|
||||||
resource_path = Path(
|
html_file_path = Path(
|
||||||
settings.extension_builder_working_dir_path,
|
|
||||||
"extension_builder_stub",
|
"extension_builder_stub",
|
||||||
ext_id,
|
ext_id,
|
||||||
working_dir_name,
|
working_dir_name,
|
||||||
ext_id,
|
ext_id,
|
||||||
"static",
|
"templates",
|
||||||
|
ext_id,
|
||||||
|
html_file_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
file_ext = ".vue" if resource == "template" else ".js"
|
html_file_full_path = Path(
|
||||||
file_full_path = Path(resource_path, file_name + file_ext)
|
settings.extension_builder_working_dir_path, html_file_path
|
||||||
|
)
|
||||||
|
|
||||||
if not file_full_path.is_file():
|
if not html_file_full_path.is_file():
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
return template_renderer().TemplateResponse(
|
||||||
|
request,
|
||||||
|
"error.html",
|
||||||
|
{
|
||||||
|
"status_code": 404,
|
||||||
|
"message": f"Extension {ext_id} not found, refresh Preview.",
|
||||||
|
},
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = template_renderer().TemplateResponse(
|
||||||
|
request,
|
||||||
|
html_file_path.as_posix(),
|
||||||
|
{
|
||||||
|
"user": user.json(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
response = FileResponse(file_full_path.absolute().as_posix())
|
|
||||||
response.headers["Content-Security-Policy"] = (
|
response.headers["Content-Security-Policy"] = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
"style-src 'self' 'unsafe-inline'; "
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
@@ -192,9 +208,6 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
|||||||
@generic_router.get(
|
@generic_router.get(
|
||||||
"/extensions/builder", dependencies=[Depends(check_extension_builder)]
|
"/extensions/builder", dependencies=[Depends(check_extension_builder)]
|
||||||
)
|
)
|
||||||
@generic_router.get(
|
|
||||||
"/extensions/builder/preview", dependencies=[Depends(check_extension_builder)]
|
|
||||||
)
|
|
||||||
async def index(
|
async def index(
|
||||||
request: Request, user: User = Depends(check_user_exists)
|
request: Request, user: User = Depends(check_user_exists)
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from lnbits.core.crud import (
|
|||||||
update_admin_settings,
|
update_admin_settings,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.wallets import delete_wallet_by_id
|
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccountFilters,
|
AccountFilters,
|
||||||
AccountOverview,
|
AccountOverview,
|
||||||
@@ -158,8 +157,11 @@ async def api_users_delete_user(
|
|||||||
user_id: str, account: Account = Depends(check_admin)
|
user_id: str, account: Account = Depends(check_admin)
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
wallets = await get_wallets(user_id, deleted=False)
|
wallets = await get_wallets(user_id, deleted=False)
|
||||||
for wallet in wallets:
|
if len(wallets) > 0:
|
||||||
await delete_wallet_by_id(wallet.id)
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Cannot delete user with wallets.",
|
||||||
|
)
|
||||||
|
|
||||||
if user_id == settings.super_user:
|
if user_id == settings.super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -222,7 +224,7 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
|||||||
|
|
||||||
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
||||||
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
||||||
return await get_wallets(user_id, deleted=None)
|
return await get_wallets(user_id)
|
||||||
|
|
||||||
|
|
||||||
@users_router.post("/user/{user_id}/wallet", name="Create a new wallet for user")
|
@users_router.post("/user/{user_id}/wallet", name="Create a new wallet for user")
|
||||||
@@ -245,7 +247,7 @@ async def api_users_create_user_wallet(
|
|||||||
"/user/{user_id}/wallet/{wallet}/undelete", name="Reactivate deleted wallet"
|
"/user/{user_id}/wallet/{wallet}/undelete", name="Reactivate deleted wallet"
|
||||||
)
|
)
|
||||||
async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
|
async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
|
||||||
wal = await get_wallet(wallet, deleted=True)
|
wal = await get_wallet(wallet)
|
||||||
if not wal:
|
if not wal:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
|||||||
+2
-7
@@ -337,8 +337,7 @@ class Database(Compat):
|
|||||||
f = "%Y-%m-%d %H:%M:%S.%f"
|
f = "%Y-%m-%d %H:%M:%S.%f"
|
||||||
if "." not in value:
|
if "." not in value:
|
||||||
f = "%Y-%m-%d %H:%M:%S"
|
f = "%Y-%m-%d %H:%M:%S"
|
||||||
# Parse and add UTC timezone info
|
return datetime.strptime(value, f)
|
||||||
return datetime.strptime(value, f).replace(tzinfo=timezone.utc)
|
|
||||||
|
|
||||||
dbapi_connection.run_async(
|
dbapi_connection.run_async(
|
||||||
lambda connection: connection.set_type_codec(
|
lambda connection: connection.set_type_codec(
|
||||||
@@ -757,11 +756,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
|
|||||||
if DB_TYPE == SQLITE:
|
if DB_TYPE == SQLITE:
|
||||||
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
||||||
else:
|
else:
|
||||||
# Ensure PostgreSQL datetime values have timezone info
|
_dict[key] = value
|
||||||
if isinstance(value, datetime) and value.tzinfo is None:
|
|
||||||
_dict[key] = value.replace(tzinfo=timezone.utc)
|
|
||||||
else:
|
|
||||||
_dict[key] = value
|
|
||||||
continue
|
continue
|
||||||
if issubclass(type_, BaseModel):
|
if issubclass(type_, BaseModel):
|
||||||
_dict[key] = dict_to_submodel(type_, value)
|
_dict[key] = dict_to_submodel(type_, value)
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def register_exception_handlers(app: FastAPI): # noqa: C901
|
|||||||
logger.error(f"RequestValidationError: {exc!s}")
|
logger.error(f"RequestValidationError: {exc!s}")
|
||||||
return render_html_error(request, exc) or JSONResponse(
|
return render_html_error(request, exc) or JSONResponse(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
content={"detail": [dict(e) for e in exc.errors()]},
|
content={"detail": str(exc)},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.exception_handler(HTTPException)
|
@app.exception_handler(HTTPException)
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||||
"LNBITS_EXTENSIONS_REVIEWS_URL": settings.lnbits_extensions_reviews_url,
|
|
||||||
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
||||||
"has_holdinvoice": settings.has_holdinvoice,
|
"has_holdinvoice": settings.has_holdinvoice,
|
||||||
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
||||||
@@ -391,14 +390,6 @@ def camel_to_snake(name: str) -> str:
|
|||||||
return name.lower()
|
return name.lower()
|
||||||
|
|
||||||
|
|
||||||
def snake_to_camel(name: str, capitalize_first: bool | None = False) -> str:
|
|
||||||
components = name.split("_")
|
|
||||||
camel = components[0] + "".join(x.capitalize() for x in components[1:])
|
|
||||||
if capitalize_first:
|
|
||||||
camel = camel[0].upper() + camel[1:]
|
|
||||||
return camel
|
|
||||||
|
|
||||||
|
|
||||||
def is_camel_case(v: str) -> bool:
|
def is_camel_case(v: str) -> bool:
|
||||||
return re.match(r"^[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*$", v) is not None
|
return re.match(r"^[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*$", v) is not None
|
||||||
|
|
||||||
|
|||||||
@@ -52,13 +52,6 @@ class ExtensionsSettings(LNbitsSettings):
|
|||||||
lnbits_user_default_extensions: list[str] = Field(default=[])
|
lnbits_user_default_extensions: list[str] = Field(default=[])
|
||||||
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
||||||
lnbits_extensions_builder_activate_non_admins: bool = Field(default=False)
|
lnbits_extensions_builder_activate_non_admins: bool = Field(default=False)
|
||||||
lnbits_extensions_reviews_url: str = Field(
|
|
||||||
default="https://demo.lnbits.com/paidreviews/api/v1/AdFzLjzuKFLsdk4Bcnff6r",
|
|
||||||
description="""
|
|
||||||
URL for the paid reviews.
|
|
||||||
Regular users can view this URL (not secret).
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
lnbits_extensions_manifests: list[str] = Field(
|
lnbits_extensions_manifests: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
"https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json"
|
"https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json"
|
||||||
@@ -306,10 +299,6 @@ class AssetSettings(LNbitsSettings):
|
|||||||
"heic",
|
"heic",
|
||||||
"heif",
|
"heif",
|
||||||
"heics",
|
"heics",
|
||||||
"text/plain",
|
|
||||||
"text/json" "text/xml",
|
|
||||||
"application/json",
|
|
||||||
"application/pdf",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
lnbits_asset_thumbnail_width: int = Field(default=128, ge=0)
|
lnbits_asset_thumbnail_width: int = Field(default=128, ge=0)
|
||||||
@@ -589,17 +578,6 @@ class SparkFundingSource(LNbitsSettings):
|
|||||||
spark_token: str | None = Field(default=None)
|
spark_token: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class SparkL2FundingSource(LNbitsSettings):
|
|
||||||
spark_l2_mnemonic: str | None = Field(default=None)
|
|
||||||
spark_l2_network: str = Field(default="MAINNET")
|
|
||||||
spark_l2_internal_sidecar_version: str | None = Field(default="0.1.1")
|
|
||||||
spark_l2_external_endpoint: str | None = Field(default=None)
|
|
||||||
spark_l2_external_api_key: str | None = Field(default=None)
|
|
||||||
spark_l2_pay_wait_ms: int = Field(default=4000, ge=0)
|
|
||||||
spark_l2_pay_poll_ms: int = Field(default=500, ge=0)
|
|
||||||
spark_l2_stream_keepalive_ms: int = Field(default=15000, ge=0)
|
|
||||||
|
|
||||||
|
|
||||||
class LnTipsFundingSource(LNbitsSettings):
|
class LnTipsFundingSource(LNbitsSettings):
|
||||||
lntips_api_endpoint: str | None = Field(default=None)
|
lntips_api_endpoint: str | None = Field(default=None)
|
||||||
lntips_api_key: str | None = Field(default=None)
|
lntips_api_key: str | None = Field(default=None)
|
||||||
@@ -705,7 +683,6 @@ class FundingSourcesSettings(
|
|||||||
PhoenixdFundingSource,
|
PhoenixdFundingSource,
|
||||||
OpenNodeFundingSource,
|
OpenNodeFundingSource,
|
||||||
SparkFundingSource,
|
SparkFundingSource,
|
||||||
SparkL2FundingSource,
|
|
||||||
LnTipsFundingSource,
|
LnTipsFundingSource,
|
||||||
NWCFundingSource,
|
NWCFundingSource,
|
||||||
BreezSdkFundingSource,
|
BreezSdkFundingSource,
|
||||||
@@ -1033,7 +1010,6 @@ class SuperUserSettings(LNbitsSettings):
|
|||||||
"FakeWallet",
|
"FakeWallet",
|
||||||
"LNPayWallet",
|
"LNPayWallet",
|
||||||
"LNbitsWallet",
|
"LNbitsWallet",
|
||||||
"LightsparkSparkWallet",
|
|
||||||
"LnTipsWallet",
|
"LnTipsWallet",
|
||||||
"LndRestWallet",
|
"LndRestWallet",
|
||||||
"LndWallet",
|
"LndWallet",
|
||||||
|
|||||||
+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
+29
-3
@@ -1,5 +1,30 @@
|
|||||||
window.app.component('lnbits-error', {
|
<template>
|
||||||
template: '#lnbits-error',
|
<div class="text-center q-pa-md flex flex-center">
|
||||||
|
<div>
|
||||||
|
<div class="error-code" v-text="code"></div>
|
||||||
|
<div class="error-message" v-text="message"></div>
|
||||||
|
<div class="q-mx-auto q-mt-lg justify-center" style="width: max-content">
|
||||||
|
<q-btn
|
||||||
|
v-if="isExtension"
|
||||||
|
color="primary"
|
||||||
|
@click="goToExtension()"
|
||||||
|
label="Go To Extension"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-else-if="g.isUserAuthorized"
|
||||||
|
color="primary"
|
||||||
|
@click="goToWallet()"
|
||||||
|
label="Go to Wallet"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn v-else color="primary" @click="goBack()" label="Go Back"></q-btn>
|
||||||
|
<span class="q-mx-md">OR</span>
|
||||||
|
<q-btn color="secondary" @click="goHome()" label="Go Home"></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
props: ['dynamic', 'code', 'message'],
|
props: ['dynamic', 'code', 'message'],
|
||||||
computed: {
|
computed: {
|
||||||
isExtension() {
|
isExtension() {
|
||||||
@@ -49,4 +74,5 @@ window.app.component('lnbits-error', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import LnbitsError from './lnbits-error.vue'
|
||||||
|
|
||||||
|
window.LnbitsError = LnbitsError
|
||||||
@@ -187,25 +187,6 @@ window.localisation.en = {
|
|||||||
'Only admin accounts can create extensions',
|
'Only admin accounts can create extensions',
|
||||||
admin_only: 'Admin Only',
|
admin_only: 'Admin Only',
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
reviews_url: 'Reviews URL',
|
|
||||||
reviews_url_label: 'Reviews server URL',
|
|
||||||
reviews_url_hint:
|
|
||||||
'Full PaidReviews URL including the settings id (e.g. https://example.com/paidreviews/SETTINGS_ID)',
|
|
||||||
reviews_open: 'View reviews',
|
|
||||||
reviews_leave: 'Leave a review',
|
|
||||||
reviews_name: 'Your name',
|
|
||||||
reviews_comment: 'Your review',
|
|
||||||
reviews_rating: 'Rating',
|
|
||||||
reviews_submit: 'Submit review',
|
|
||||||
reviews_loading: 'Loading reviews...',
|
|
||||||
reviews_refresh: 'Refresh reviews',
|
|
||||||
reviews_error_load: 'Could not load reviews',
|
|
||||||
reviews_url_not_configured: 'Reviews URL not configured',
|
|
||||||
reviews_pay_invoice: 'Pay invoice',
|
|
||||||
reviews_invoice_paid: 'Invoice paid',
|
|
||||||
reviews_invoice_title: 'Pay this invoice to submit your review',
|
|
||||||
reviews_count: 'Reviews',
|
|
||||||
no_reviews: 'No reviews yet',
|
|
||||||
extension_has_free_release: 'Has free releases',
|
extension_has_free_release: 'Has free releases',
|
||||||
extension_has_paid_release: 'Has paid releases',
|
extension_has_paid_release: 'Has paid releases',
|
||||||
extension_depends_on: 'Depends on:',
|
extension_depends_on: 'Depends on:',
|
||||||
@@ -695,8 +676,6 @@ window.localisation.en = {
|
|||||||
allow_creation_user: 'Allow creation of new users',
|
allow_creation_user: 'Allow creation of new users',
|
||||||
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
||||||
new_user_not_allowed: 'Registration is disabled.',
|
new_user_not_allowed: 'Registration is disabled.',
|
||||||
start_user_impersonation: 'Impersonate this user',
|
|
||||||
stop_user_impersonation: 'Stop User Impersonation',
|
|
||||||
components: 'Components',
|
components: 'Components',
|
||||||
long_running_endpoints: 'Top 5 Long Running Endpoints',
|
long_running_endpoints: 'Top 5 Long Running Endpoints',
|
||||||
http_request_methods: 'HTTP Request Methods',
|
http_request_methods: 'HTTP Request Methods',
|
||||||
|
|||||||
@@ -123,19 +123,6 @@ window._lnbitsApi = {
|
|||||||
url: '/api/v1/auth/logout'
|
url: '/api/v1/auth/logout'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
impersonateUser(usr) {
|
|
||||||
return axios({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/api/v1/auth/impersonate',
|
|
||||||
data: {usr}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
stopImpersonation() {
|
|
||||||
return axios({
|
|
||||||
method: 'DELETE',
|
|
||||||
url: '/api/v1/auth/impersonate'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getAuthenticatedUser() {
|
getAuthenticatedUser() {
|
||||||
return this.request('get', '/api/v1/auth')
|
return this.request('get', '/api/v1/auth')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ window.LNbits = {
|
|||||||
wallets: data.wallets,
|
wallets: data.wallets,
|
||||||
fiat_providers: data.fiat_providers || [],
|
fiat_providers: data.fiat_providers || [],
|
||||||
super_user: data.super_user,
|
super_user: data.super_user,
|
||||||
extra: data.extra ?? {},
|
extra: data.extra ?? {}
|
||||||
hasPassword: data.has_password ?? false
|
|
||||||
}
|
}
|
||||||
const mapWallet = this.wallet
|
const mapWallet = this.wallet
|
||||||
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
||||||
|
|||||||
@@ -3,37 +3,26 @@ window.app.component(QrcodeVue)
|
|||||||
window.app.component('lnbits-extension-rating', {
|
window.app.component('lnbits-extension-rating', {
|
||||||
template: '#lnbits-extension-rating',
|
template: '#lnbits-extension-rating',
|
||||||
name: 'lnbits-extension-rating',
|
name: 'lnbits-extension-rating',
|
||||||
|
props: ['rating']
|
||||||
|
})
|
||||||
|
|
||||||
|
window.app.component('lnbits-fsat', {
|
||||||
|
template: '<span>{{ fsat }}</span>',
|
||||||
props: {
|
props: {
|
||||||
rating: {
|
amount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
|
||||||
count: {
|
|
||||||
type: Number,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
clickable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
displayRating() {
|
fsat() {
|
||||||
return Math.round((this.rating || 0) * 2) / 2
|
return LNbits.utils.formatSat(this.amount)
|
||||||
},
|
|
||||||
hasData() {
|
|
||||||
return this.count !== null && this.count !== undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleClick() {
|
|
||||||
if (!this.clickable) return
|
|
||||||
this.$emit('click')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
window.app.component('lnbits-manage', {
|
window.app.component('lnbits-manage', {
|
||||||
|
mixins: [window.windowMixin],
|
||||||
template: '#lnbits-manage',
|
template: '#lnbits-manage',
|
||||||
computed: {
|
computed: {
|
||||||
showAdmin() {
|
showAdmin() {
|
||||||
@@ -65,8 +54,10 @@ window.app.component('lnbits-manage', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
window.app.component('lnbits-payment-details', {
|
window.app.component('lnbits-payment-details', {
|
||||||
|
mixins: [window.windowMixin],
|
||||||
template: '#lnbits-payment-details',
|
template: '#lnbits-payment-details',
|
||||||
props: ['payment'],
|
props: ['payment'],
|
||||||
|
mixins: [window.windowMixin],
|
||||||
computed: {
|
computed: {
|
||||||
hasPreimage() {
|
hasPreimage() {
|
||||||
return (
|
return (
|
||||||
@@ -110,6 +101,7 @@ window.app.component('lnbits-payment-details', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
window.app.component('lnbits-lnurlpay-success-action', {
|
window.app.component('lnbits-lnurlpay-success-action', {
|
||||||
|
mixins: [window.windowMixin],
|
||||||
template: '#lnbits-lnurlpay-success-action',
|
template: '#lnbits-lnurlpay-success-action',
|
||||||
props: ['payment', 'success_action'],
|
props: ['payment', 'success_action'],
|
||||||
data() {
|
data() {
|
||||||
@@ -129,6 +121,7 @@ window.app.component('lnbits-lnurlpay-success-action', {
|
|||||||
|
|
||||||
window.app.component('lnbits-notifications-btn', {
|
window.app.component('lnbits-notifications-btn', {
|
||||||
template: '#lnbits-notifications-btn',
|
template: '#lnbits-notifications-btn',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
props: ['pubkey'],
|
props: ['pubkey'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -299,6 +292,7 @@ window.app.component('lnbits-notifications-btn', {
|
|||||||
|
|
||||||
window.app.component('lnbits-dynamic-fields', {
|
window.app.component('lnbits-dynamic-fields', {
|
||||||
template: '#lnbits-dynamic-fields',
|
template: '#lnbits-dynamic-fields',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
props: ['options', 'modelValue'],
|
props: ['options', 'modelValue'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -331,6 +325,7 @@ window.app.component('lnbits-dynamic-fields', {
|
|||||||
|
|
||||||
window.app.component('lnbits-dynamic-chips', {
|
window.app.component('lnbits-dynamic-chips', {
|
||||||
template: '#lnbits-dynamic-chips',
|
template: '#lnbits-dynamic-chips',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
props: ['modelValue'],
|
props: ['modelValue'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -361,6 +356,7 @@ window.app.component('lnbits-dynamic-chips', {
|
|||||||
|
|
||||||
window.app.component('lnbits-update-balance', {
|
window.app.component('lnbits-update-balance', {
|
||||||
template: '#lnbits-update-balance',
|
template: '#lnbits-update-balance',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
props: ['wallet_id', 'small_btn'],
|
props: ['wallet_id', 'small_btn'],
|
||||||
computed: {
|
computed: {
|
||||||
admin() {
|
admin() {
|
||||||
@@ -399,6 +395,7 @@ window.app.component('lnbits-update-balance', {
|
|||||||
|
|
||||||
window.app.component('user-id-only', {
|
window.app.component('user-id-only', {
|
||||||
template: '#user-id-only',
|
template: '#user-id-only',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
props: {
|
props: {
|
||||||
allowed_new_users: Boolean,
|
allowed_new_users: Boolean,
|
||||||
authAction: String,
|
authAction: String,
|
||||||
@@ -442,6 +439,7 @@ window.app.component('user-id-only', {
|
|||||||
|
|
||||||
window.app.component('username-password', {
|
window.app.component('username-password', {
|
||||||
template: '#username-password',
|
template: '#username-password',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
props: {
|
props: {
|
||||||
allowed_new_users: Boolean,
|
allowed_new_users: Boolean,
|
||||||
authMethods: Array,
|
authMethods: Array,
|
||||||
@@ -679,6 +677,7 @@ window.app.component('lnbits-stat', {
|
|||||||
|
|
||||||
window.app.component('lnbits-node-qrcode', {
|
window.app.component('lnbits-node-qrcode', {
|
||||||
props: ['info'],
|
props: ['info'],
|
||||||
|
mixins: [window.windowMixin],
|
||||||
template: `
|
template: `
|
||||||
<q-card class="my-card">
|
<q-card class="my-card">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@@ -757,6 +756,7 @@ window.app.component('lnbits-node-info', {
|
|||||||
showDialog: false
|
showDialog: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mixins: [window.windowMixin],
|
||||||
methods: {
|
methods: {
|
||||||
shortenNodeId(nodeId) {
|
shortenNodeId(nodeId) {
|
||||||
return nodeId
|
return nodeId
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-admin-assets-config', {
|
window.app.component('lnbits-admin-assets-config', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-assets-config',
|
template: '#lnbits-admin-assets-config',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
newAllowedAssetMimeType: '',
|
newAllowedAssetMimeType: '',
|
||||||
@@ -16,7 +17,6 @@ window.app.component('lnbits-admin-assets-config', {
|
|||||||
this.newAllowedAssetMimeType
|
this.newAllowedAssetMimeType
|
||||||
)
|
)
|
||||||
this.newAllowedAssetMimeType = ''
|
this.newAllowedAssetMimeType = ''
|
||||||
this.formData.touch = null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeAllowedAssetMimeType(type) {
|
removeAllowedAssetMimeType(type) {
|
||||||
@@ -24,21 +24,18 @@ window.app.component('lnbits-admin-assets-config', {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.formData.lnbits_assets_allowed_mime_types.splice(index, 1)
|
this.formData.lnbits_assets_allowed_mime_types.splice(index, 1)
|
||||||
}
|
}
|
||||||
this.formData.touch = null
|
|
||||||
},
|
},
|
||||||
addNewNoLimitUser() {
|
addNewNoLimitUser() {
|
||||||
if (this.newNoLimitUser) {
|
if (this.newNoLimitUser) {
|
||||||
this.removeNoLimitUser(this.newNoLimitUser)
|
this.removeNoLimitUser(this.newNoLimitUser)
|
||||||
this.formData.lnbits_assets_no_limit_users.push(this.newNoLimitUser)
|
this.formData.lnbits_assets_no_limit_users.push(this.newNoLimitUser)
|
||||||
this.newNoLimitUser = ''
|
this.newNoLimitUser = ''
|
||||||
this.formData.touch = null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeNoLimitUser(user) {
|
removeNoLimitUser(user) {
|
||||||
if (user) {
|
if (user) {
|
||||||
this.formData.lnbits_assets_no_limit_users =
|
this.formData.lnbits_assets_no_limit_users =
|
||||||
this.formData.lnbits_assets_no_limit_users.filter(u => u !== user)
|
this.formData.lnbits_assets_no_limit_users.filter(u => u !== user)
|
||||||
this.formData.touch = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-admin-audit', {
|
window.app.component('lnbits-admin-audit', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-audit',
|
template: '#lnbits-admin-audit',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddIncludePath: '',
|
formAddIncludePath: '',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-admin-exchange-providers', {
|
window.app.component('lnbits-admin-exchange-providers', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-exchange-providers',
|
template: '#lnbits-admin-exchange-providers',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
exchangeData: {
|
exchangeData: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-admin-extensions', {
|
window.app.component('lnbits-admin-extensions', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-extensions',
|
template: '#lnbits-admin-extensions',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddExtensionsManifest: ''
|
formAddExtensionsManifest: ''
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-admin-fiat-providers', {
|
window.app.component('lnbits-admin-fiat-providers', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-fiat-providers',
|
template: '#lnbits-admin-fiat-providers',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddStripeUser: '',
|
formAddStripeUser: '',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-admin-funding-sources', {
|
window.app.component('lnbits-admin-funding-sources', {
|
||||||
template: '#lnbits-admin-funding-sources',
|
template: '#lnbits-admin-funding-sources',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
props: ['form-data', 'allowed-funding-sources'],
|
props: ['form-data', 'allowed-funding-sources'],
|
||||||
methods: {
|
methods: {
|
||||||
getFundingSourceLabel(item) {
|
getFundingSourceLabel(item) {
|
||||||
@@ -228,51 +229,6 @@ window.app.component('lnbits-admin-funding-sources', {
|
|||||||
spark_token: 'Token'
|
spark_token: 'Token'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'LightsparkSparkWallet',
|
|
||||||
'Spark (L2)',
|
|
||||||
{
|
|
||||||
spark_l2_internal_sidecar_version: {
|
|
||||||
label: 'Internal Sidecar Version (eg: 0.1.1).',
|
|
||||||
hint: 'If specified then the sidecar will be downloaded. Alternatively you can specify an External Sidecar endpoint in the Advanced section.',
|
|
||||||
value: ''
|
|
||||||
},
|
|
||||||
spark_l2_mnemonic: {
|
|
||||||
label: 'Mnemonic',
|
|
||||||
hint: 'Only required if Interna Sidecar version is specified.'
|
|
||||||
},
|
|
||||||
spark_l2_network: {
|
|
||||||
label: 'Network',
|
|
||||||
value: 'MAINNET'
|
|
||||||
},
|
|
||||||
|
|
||||||
spark_l2_external_endpoint: {
|
|
||||||
label: 'External Sidecar Endpoint. ',
|
|
||||||
hint: 'If specified then this endpoint will be used instead of the internal sidecar. Make sure to also specify the API key if your sidecar requires authentication.',
|
|
||||||
value: '',
|
|
||||||
advanced: true
|
|
||||||
},
|
|
||||||
spark_l2_external_api_key: {
|
|
||||||
label: 'External Sidecar API Key. ',
|
|
||||||
hint: 'API Key for authenticating with the external sidecar if it requires authentication.',
|
|
||||||
value: '',
|
|
||||||
advanced: true
|
|
||||||
},
|
|
||||||
|
|
||||||
spark_l2_pay_wait_ms: {
|
|
||||||
label: 'Pay Wait Time (ms)',
|
|
||||||
advanced: true
|
|
||||||
},
|
|
||||||
spark_l2_pay_poll_ms: {
|
|
||||||
label: 'Pay Poll Time (ms)',
|
|
||||||
advanced: true
|
|
||||||
},
|
|
||||||
spark_l2_stream_keepalive_ms: {
|
|
||||||
label: 'Stream Keepalive Time (ms)',
|
|
||||||
advanced: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'NWCWallet',
|
'NWCWallet',
|
||||||
'Nostr Wallet Connect',
|
'Nostr Wallet Connect',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-admin-funding', {
|
window.app.component('lnbits-admin-funding', {
|
||||||
props: ['is-super-user', 'form-data', 'settings'],
|
props: ['is-super-user', 'form-data', 'settings'],
|
||||||
template: '#lnbits-admin-funding',
|
template: '#lnbits-admin-funding',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
auditData: []
|
auditData: []
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-admin-notifications', {
|
window.app.component('lnbits-admin-notifications', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-notifications',
|
template: '#lnbits-admin-notifications',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
nostrNotificationIdentifier: '',
|
nostrNotificationIdentifier: '',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-admin-security', {
|
window.app.component('lnbits-admin-security', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-security',
|
template: '#lnbits-admin-security',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
logs: [],
|
logs: [],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
window.app.component('lnbits-admin-server', {
|
window.app.component('lnbits-admin-server', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-server'
|
template: '#lnbits-admin-server',
|
||||||
|
mixins: [window.windowMixin]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-admin-site-customisation', {
|
window.app.component('lnbits-admin-site-customisation', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-site-customisation',
|
template: '#lnbits-admin-site-customisation',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lnbits_theme_options: [
|
lnbits_theme_options: [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-admin-users', {
|
window.app.component('lnbits-admin-users', {
|
||||||
props: ['form-data'],
|
props: ['form-data'],
|
||||||
template: '#lnbits-admin-users',
|
template: '#lnbits-admin-users',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddUser: '',
|
formAddUser: '',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-disclaimer', {
|
window.app.component('lnbits-disclaimer', {
|
||||||
template: '#lnbits-disclaimer',
|
template: '#lnbits-disclaimer',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
computed: {
|
computed: {
|
||||||
showDisclaimer() {
|
showDisclaimer() {
|
||||||
return !g.disclaimerShown && g.isUserAuthorized
|
return !g.disclaimerShown && g.isUserAuthorized
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
window.app.component('lnbits-drawer', {
|
window.app.component('lnbits-drawer', {
|
||||||
template: '#lnbits-drawer'
|
template: '#lnbits-drawer',
|
||||||
|
mixins: [window.windowMixin]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-footer', {
|
window.app.component('lnbits-footer', {
|
||||||
template: '#lnbits-footer',
|
template: '#lnbits-footer',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
computed: {
|
computed: {
|
||||||
version() {
|
version() {
|
||||||
return this.LNBITS_VERSION || 'unknown version'
|
return this.LNBITS_VERSION || 'unknown version'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
window.app.component('lnbits-header-wallets', {
|
window.app.component('lnbits-header-wallets', {
|
||||||
template: '#lnbits-header-wallets'
|
template: '#lnbits-header-wallets',
|
||||||
|
mixins: [window.windowMixin]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-header', {
|
window.app.component('lnbits-header', {
|
||||||
template: '#lnbits-header',
|
template: '#lnbits-header',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
computed: {
|
computed: {
|
||||||
hasServiceFeeMax() {
|
hasServiceFeeMax() {
|
||||||
return (
|
return (
|
||||||
@@ -60,17 +61,5 @@ window.app.component('lnbits-header', {
|
|||||||
return 'User'
|
return 'User'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async stopImpersonation() {
|
|
||||||
try {
|
|
||||||
await LNbits.api.stopImpersonation()
|
|
||||||
LNbits.utils.restoreLocalStorage('impersonation')
|
|
||||||
window.location = '/users'
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e)
|
|
||||||
LNbits.utils.notifyApiError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-home-logos', {
|
window.app.component('lnbits-home-logos', {
|
||||||
template: '#lnbits-home-logos',
|
template: '#lnbits-home-logos',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
logos: [
|
logos: [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-label-selector', {
|
window.app.component('lnbits-label-selector', {
|
||||||
template: '#lnbits-label-selector',
|
template: '#lnbits-label-selector',
|
||||||
props: ['labels'],
|
props: ['labels'],
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
labelFilter: '',
|
labelFilter: '',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-language-dropdown', {
|
window.app.component('lnbits-language-dropdown', {
|
||||||
template: '#lnbits-language-dropdown',
|
template: '#lnbits-language-dropdown',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
methods: {
|
methods: {
|
||||||
activeLanguage(lang) {
|
activeLanguage(lang) {
|
||||||
return window.i18n.global.locale === lang
|
return window.i18n.global.locale === lang
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
window.app.component('lnbits-manage-extension-list', {
|
window.app.component('lnbits-manage-extension-list', {
|
||||||
|
mixins: [window.windowMixin],
|
||||||
template: '#lnbits-manage-extension-list',
|
template: '#lnbits-manage-extension-list',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-manage-wallet-list', {
|
window.app.component('lnbits-manage-wallet-list', {
|
||||||
template: '#lnbits-manage-wallet-list',
|
template: '#lnbits-manage-wallet-list',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeWalletId: null
|
activeWalletId: null
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
window.app.component('lnbits-payment-list', {
|
window.app.component('lnbits-payment-list', {
|
||||||
template: '#lnbits-payment-list',
|
template: '#lnbits-payment-list',
|
||||||
props: ['wallet', 'paymentFilter'],
|
props: ['wallet', 'paymentFilter'],
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
payments: [],
|
payments: [],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-qrcode-lnurl', {
|
window.app.component('lnbits-qrcode-lnurl', {
|
||||||
template: '#lnbits-qrcode-lnurl',
|
template: '#lnbits-qrcode-lnurl',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
props: {
|
props: {
|
||||||
url: {
|
url: {
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
window.app.component('lnbits-qrcode', {
|
window.app.component('lnbits-qrcode', {
|
||||||
|
mixins: [window.windowMixin],
|
||||||
template: '#lnbits-qrcode',
|
template: '#lnbits-qrcode',
|
||||||
components: {
|
components: {
|
||||||
QrcodeVue: QrcodeVue.default
|
QrcodeVue: QrcodeVue.default
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
window.app.component('lnbits-theme', {
|
window.app.component('lnbits-theme', {
|
||||||
|
mixins: [window.windowMixin],
|
||||||
watch: {
|
watch: {
|
||||||
'g.walletFlip'(val) {
|
'g.walletFlip'(val) {
|
||||||
this.$q.localStorage.setItem('lnbits.walletFlip', val)
|
this.$q.localStorage.setItem('lnbits.walletFlip', val)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-wallet-api-docs', {
|
window.app.component('lnbits-wallet-api-docs', {
|
||||||
template: '#lnbits-wallet-api-docs',
|
template: '#lnbits-wallet-api-docs',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
methods: {
|
methods: {
|
||||||
resetKeys() {
|
resetKeys() {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-wallet-charts', {
|
window.app.component('lnbits-wallet-charts', {
|
||||||
template: '#lnbits-wallet-charts',
|
template: '#lnbits-wallet-charts',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
props: ['paymentFilter', 'chartConfig'],
|
props: ['paymentFilter', 'chartConfig'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
window.app.component('lnbits-wallet-extra', {
|
window.app.component('lnbits-wallet-extra', {
|
||||||
template: '#lnbits-wallet-extra',
|
template: '#lnbits-wallet-extra',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
props: ['chartConfig'],
|
props: ['chartConfig'],
|
||||||
computed: {
|
data() {
|
||||||
exportUrl() {
|
return {}
|
||||||
return `${window.location.origin}/wallet?usr=${this.g.user.id}&wal=${this.g.wallet.id}`
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleSendLnurl(lnurl) {
|
handleSendLnurl(lnurl) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-wallet-icon', {
|
window.app.component('lnbits-wallet-icon', {
|
||||||
template: '#lnbits-wallet-icon',
|
template: '#lnbits-wallet-icon',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
icon: {
|
icon: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-wallet-new', {
|
window.app.component('lnbits-wallet-new', {
|
||||||
template: '#lnbits-wallet-new',
|
template: '#lnbits-wallet-new',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
walletTypes: [{label: 'Lightning Wallet', value: 'lightning'}],
|
walletTypes: [{label: 'Lightning Wallet', value: 'lightning'}],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-wallet-paylinks', {
|
window.app.component('lnbits-wallet-paylinks', {
|
||||||
template: '#lnbits-wallet-paylinks',
|
template: '#lnbits-wallet-paylinks',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
storedPaylinks: []
|
storedPaylinks: []
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.app.component('lnbits-wallet-share', {
|
window.app.component('lnbits-wallet-share', {
|
||||||
template: '#lnbits-wallet-share',
|
template: '#lnbits-wallet-share',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
computed: {
|
computed: {
|
||||||
walletApprovedShares() {
|
walletApprovedShares() {
|
||||||
return this.g.wallet.extra.shared_with.filter(
|
return this.g.wallet.extra.shared_with.filter(
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ window.g = Vue.reactive({
|
|||||||
wallet: null,
|
wallet: null,
|
||||||
isPublicPage: true,
|
isPublicPage: true,
|
||||||
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
||||||
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
|
|
||||||
offline: !navigator.onLine,
|
offline: !navigator.onLine,
|
||||||
hasCamera: false,
|
hasCamera: false,
|
||||||
visibleDrawer: false,
|
visibleDrawer: false,
|
||||||
|
|||||||
@@ -106,11 +106,6 @@ const routes = [
|
|||||||
name: 'ExtensionsBuilder',
|
name: 'ExtensionsBuilder',
|
||||||
component: PageExtensionBuilder
|
component: PageExtensionBuilder
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/extensions/builder/preview',
|
|
||||||
name: 'ExtensionsBuilderPreview',
|
|
||||||
component: PageExtensionBuilderPreview
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/extensions',
|
path: '/extensions',
|
||||||
name: 'Extensions',
|
name: 'Extensions',
|
||||||
@@ -173,4 +168,8 @@ window.app.use(Quasar, quasarConfig)
|
|||||||
|
|
||||||
window.app.use(window.i18n)
|
window.app.use(window.i18n)
|
||||||
window.app.use(window.router)
|
window.app.use(window.router)
|
||||||
|
|
||||||
|
window.app.component(QrcodeVue)
|
||||||
|
window.app.component('lnbits-error', window.LnbitsError)
|
||||||
|
|
||||||
window.app.mount('#vue')
|
window.app.mount('#vue')
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageAccount = {
|
window.PageAccount = {
|
||||||
template: '#page-account',
|
template: '#page-account',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
untouchedUser: null,
|
untouchedUser: null,
|
||||||
@@ -609,7 +610,7 @@ window.PageAccount = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
copyAssetLinkToClipboard(asset) {
|
copyAssetLinkToClipboard(asset) {
|
||||||
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/data`
|
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/binary`
|
||||||
this.utils.copyText(assetUrl)
|
this.utils.copyText(assetUrl)
|
||||||
},
|
},
|
||||||
addUserLabel() {
|
addUserLabel() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageAdmin = {
|
window.PageAdmin = {
|
||||||
template: '#page-admin',
|
template: '#page-admin',
|
||||||
|
mixins: [windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tab: 'funding',
|
tab: 'funding',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageAudit = {
|
window.PageAudit = {
|
||||||
template: '#page-audit',
|
template: '#page-audit',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
chartsReady: false,
|
chartsReady: false,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
window.PageError = {
|
window.PageError = {
|
||||||
template: '#page-error'
|
template: '#page-error',
|
||||||
|
mixins: [window.windowMixin]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageExtensions = {
|
window.PageExtensions = {
|
||||||
template: '#page-extensions',
|
template: '#page-extensions',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
extbuilderEnabled: false,
|
extbuilderEnabled: false,
|
||||||
@@ -25,63 +26,7 @@ window.PageExtensions = {
|
|||||||
selectedRelease: null,
|
selectedRelease: null,
|
||||||
uninstallAndDropDb: false,
|
uninstallAndDropDb: false,
|
||||||
maxStars: 5,
|
maxStars: 5,
|
||||||
paylinkWebsocket: null,
|
paylinkWebsocket: null
|
||||||
searchToggle: false,
|
|
||||||
reviewsUrl: null,
|
|
||||||
reviewsDialog: {
|
|
||||||
show: false,
|
|
||||||
extension: null,
|
|
||||||
loading: false,
|
|
||||||
submitting: false,
|
|
||||||
form: {
|
|
||||||
name: '',
|
|
||||||
rating: 0,
|
|
||||||
comment: ''
|
|
||||||
},
|
|
||||||
error: null
|
|
||||||
},
|
|
||||||
reviews: [],
|
|
||||||
reviewsTable: {
|
|
||||||
loading: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('Name'),
|
|
||||||
field: 'name',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'comment',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('Comment'),
|
|
||||||
field: 'comment'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'created_at',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('Date'),
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rating',
|
|
||||||
align: 'right',
|
|
||||||
label: 'Rating',
|
|
||||||
field: 'rating'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 5,
|
|
||||||
sortBy: 'created_at',
|
|
||||||
descending: true,
|
|
||||||
page: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
paymentDialog: {
|
|
||||||
show: false,
|
|
||||||
invoice: '',
|
|
||||||
hash: ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -126,6 +71,7 @@ window.PageExtensions = {
|
|||||||
// the install logic has been triggered one way or another
|
// the install logic has been triggered one way or another
|
||||||
this.unsubscribeFromPaylinkWs()
|
this.unsubscribeFromPaylinkWs()
|
||||||
|
|
||||||
|
const extension = this.selectedExtension
|
||||||
this.selectedExtension.inProgress = true
|
this.selectedExtension.inProgress = true
|
||||||
this.showManageExtensionDialog = false
|
this.showManageExtensionDialog = false
|
||||||
release.payment_hash =
|
release.payment_hash =
|
||||||
@@ -339,7 +285,8 @@ window.PageExtensions = {
|
|||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
`/api/v1/extension/${extension.id}/releases`
|
`/api/v1/extension/${extension.id}/releases`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
|
|
||||||
this.selectedExtensionRepos = data.reduce((repos, release) => {
|
this.selectedExtensionRepos = data.reduce((repos, release) => {
|
||||||
@@ -377,9 +324,6 @@ window.PageExtensions = {
|
|||||||
if (!detailsLink) {
|
if (!detailsLink) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// keep a reference to the extension so we can show rating stats
|
|
||||||
this.selectedExtension =
|
|
||||||
this.extensions.find(ext => ext.id === extId) || this.selectedExtension
|
|
||||||
this.selectedExtensionDetails = null
|
this.selectedExtensionDetails = null
|
||||||
this.showExtensionDetailsDialog = true
|
this.showExtensionDetailsDialog = true
|
||||||
this.slide = 0
|
this.slide = 0
|
||||||
@@ -388,7 +332,8 @@ window.PageExtensions = {
|
|||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`
|
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`,
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
|
|
||||||
this.selectedExtensionDetails = data
|
this.selectedExtensionDetails = data
|
||||||
@@ -509,7 +454,7 @@ window.PageExtensions = {
|
|||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
`/api/v1/extension/${extId}/invoice/install`,
|
`/api/v1/extension/${extId}/invoice/install`,
|
||||||
null,
|
this.g.user.wallets[0].adminkey,
|
||||||
{
|
{
|
||||||
ext_id: extId,
|
ext_id: extId,
|
||||||
archive: release.archive,
|
archive: release.archive,
|
||||||
@@ -525,7 +470,7 @@ window.PageExtensions = {
|
|||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
`/api/v1/extension/${extId}/invoice/enable`,
|
`/api/v1/extension/${extId}/invoice/enable`,
|
||||||
null,
|
this.g.user.wallets[0].adminkey,
|
||||||
{
|
{
|
||||||
amount
|
amount
|
||||||
}
|
}
|
||||||
@@ -616,7 +561,8 @@ window.PageExtensions = {
|
|||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
`/api/v1/extension/release/${org}/${repo}/${release.version}`
|
`/api/v1/extension/release/${org}/${repo}/${release.version}`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
release.loaded = true
|
release.loaded = true
|
||||||
release.is_version_compatible = data.is_version_compatible
|
release.is_version_compatible = data.is_version_compatible
|
||||||
@@ -641,13 +587,18 @@ window.PageExtensions = {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ext.inProgress = true
|
ext.inProgress = true
|
||||||
await LNbits.api.request('POST', `/api/v1/extension`, null, {
|
await LNbits.api.request(
|
||||||
ext_id: ext.id,
|
'POST',
|
||||||
archive: ext.latestRelease.archive,
|
`/api/v1/extension`,
|
||||||
source_repo: ext.latestRelease.source_repo,
|
this.g.user.wallets[0].adminkey,
|
||||||
payment_hash: ext.latestRelease.payment_hash,
|
{
|
||||||
version: ext.latestRelease.version
|
ext_id: ext.id,
|
||||||
})
|
archive: ext.latestRelease.archive,
|
||||||
|
source_repo: ext.latestRelease.source_repo,
|
||||||
|
payment_hash: ext.latestRelease.payment_hash,
|
||||||
|
version: ext.latestRelease.version
|
||||||
|
}
|
||||||
|
)
|
||||||
count++
|
count++
|
||||||
ext.isAvailable = true
|
ext.isAvailable = true
|
||||||
ext.isInstalled = true
|
ext.isInstalled = true
|
||||||
@@ -672,163 +623,6 @@ window.PageExtensions = {
|
|||||||
})
|
})
|
||||||
this.showUpdateAllDialog = false
|
this.showUpdateAllDialog = false
|
||||||
},
|
},
|
||||||
formatAvg(raw) {
|
|
||||||
const val = Number(raw || 0)
|
|
||||||
return Math.round((val / 2 / 100) * 2) / 2
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadReviewStats() {
|
|
||||||
if (!this.reviewsUrl) {
|
|
||||||
console.info('Extension reviews are not configured')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/api/v1/extension/reviews/tags`
|
|
||||||
)
|
|
||||||
const map = {}
|
|
||||||
data.forEach(stat => {
|
|
||||||
map[stat.tag] = stat
|
|
||||||
})
|
|
||||||
this.extensions.forEach(ext => {
|
|
||||||
ext.reviewStats = map[ext.id] || null
|
|
||||||
})
|
|
||||||
this.filterExtensions(this.searchTerm, this.tab)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async openReviews(extension) {
|
|
||||||
const targetExtension =
|
|
||||||
extension ||
|
|
||||||
(this.selectedExtensionDetails
|
|
||||||
? this.extensions.find(e => e.id === this.selectedExtensionDetails.id)
|
|
||||||
: null)
|
|
||||||
if (!targetExtension) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!this.reviewsUrl) {
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'warning',
|
|
||||||
message: this.$t('reviews_url_not_configured')
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.reviewsDialog.extension = targetExtension
|
|
||||||
this.selectedExtension = extension
|
|
||||||
this.reviewsDialog.show = true
|
|
||||||
await this.getTagReviews()
|
|
||||||
},
|
|
||||||
async getTagReviews(props) {
|
|
||||||
if (!this.reviewsUrl) {
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'warning',
|
|
||||||
message: this.$t('reviews_url_not_configured')
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.reviewsTable.loading = true
|
|
||||||
const params = LNbits.utils.prepareFilterQuery(this.reviewsTable, props)
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/api/v1/extension/reviews/${this.selectedExtension.id}?${params}`
|
|
||||||
)
|
|
||||||
this.reviews = data.data
|
|
||||||
this.reviewsTable.pagination.rowsNumber = data.total
|
|
||||||
} catch (e) {
|
|
||||||
LNbits.utils.notifyApiError(e)
|
|
||||||
} finally {
|
|
||||||
this.reviewsTable.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
formatReviewDate(val) {
|
|
||||||
if (!val) return ''
|
|
||||||
const numeric = Number(val)
|
|
||||||
if (!Number.isNaN(numeric)) {
|
|
||||||
return this.utils.formatTimestamp(numeric)
|
|
||||||
}
|
|
||||||
return this.utils.formatDate(val)
|
|
||||||
},
|
|
||||||
async submitReview() {
|
|
||||||
if (!this.reviewsDialog.extension || !this.reviewsUrl) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.reviewsDialog.submitting = true
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
tag: this.reviewsDialog.extension.id,
|
|
||||||
name: this.reviewsDialog.form.name,
|
|
||||||
rating: this.reviewsDialog.form.rating * 100,
|
|
||||||
comment: this.reviewsDialog.form.comment
|
|
||||||
}
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
`/api/v1/extension/reviews`,
|
|
||||||
null,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
if (data.payment_request) {
|
|
||||||
this.openInvoiceDialog(data.payment_request, data.payment_hash)
|
|
||||||
} else {
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Review submitted'
|
|
||||||
})
|
|
||||||
this.resetReviewForm()
|
|
||||||
await this.getTagReviews()
|
|
||||||
await this.loadReviewStats()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
} finally {
|
|
||||||
this.reviewsDialog.submitting = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openInvoiceDialog(invoice, hash) {
|
|
||||||
this.paymentDialog.invoice = invoice
|
|
||||||
this.paymentDialog.hash = hash
|
|
||||||
this.paymentDialog.show = true
|
|
||||||
this.listenForPayment(hash)
|
|
||||||
},
|
|
||||||
resetReviewForm() {
|
|
||||||
this.reviewsDialog.form = {
|
|
||||||
name: '',
|
|
||||||
rating: 0,
|
|
||||||
comment: ''
|
|
||||||
}
|
|
||||||
this.paymentDialog = {
|
|
||||||
show: false,
|
|
||||||
invoice: '',
|
|
||||||
hash: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
listenForPayment(hash) {
|
|
||||||
try {
|
|
||||||
const base = new URL(this.reviewsUrl)
|
|
||||||
base.protocol = base.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
||||||
base.pathname = `/api/v1/ws/${hash}`
|
|
||||||
const ws = new WebSocket(base)
|
|
||||||
ws.addEventListener('message', async () => {
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'positive',
|
|
||||||
message: this.$t('reviews_invoice_paid')
|
|
||||||
})
|
|
||||||
this.paymentDialog.show = false
|
|
||||||
this.resetReviewForm()
|
|
||||||
setTimeout(async () => {
|
|
||||||
await this.getTagReviews()
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
await this.loadReviewStats()
|
|
||||||
ws.close()
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async fetchAllExtensions() {
|
async fetchAllExtensions() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
|
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
|
||||||
@@ -843,7 +637,6 @@ window.PageExtensions = {
|
|||||||
async created() {
|
async created() {
|
||||||
this.extensions = await this.fetchAllExtensions()
|
this.extensions = await this.fetchAllExtensions()
|
||||||
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
|
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
|
||||||
this.reviewsUrl = this.LNBITS_EXTENSIONS_REVIEWS_URL
|
|
||||||
|
|
||||||
if (this.g.user.extensions.length === 0) {
|
if (this.g.user.extensions.length === 0) {
|
||||||
this.tab = 'all'
|
this.tab = 'all'
|
||||||
@@ -860,8 +653,6 @@ window.PageExtensions = {
|
|||||||
this.hasNewVersion(ext)
|
this.hasNewVersion(ext)
|
||||||
)
|
)
|
||||||
|
|
||||||
await this.loadReviewStats()
|
|
||||||
|
|
||||||
this.filterExtensions(this.searchTerm, this.tab)
|
this.filterExtensions(this.searchTerm, this.tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageExtensionBuilder = {
|
window.PageExtensionBuilder = {
|
||||||
template: '#page-extension-builder',
|
template: '#page-extension-builder',
|
||||||
|
mixins: [windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
step: 1,
|
step: 1,
|
||||||
@@ -287,10 +288,11 @@ window.PageExtensionBuilder = {
|
|||||||
`/api/v1/extension/${stub_ext_id}/releases`
|
`/api/v1/extension/${stub_ext_id}/releases`
|
||||||
)
|
)
|
||||||
|
|
||||||
this.extensionStubVersions = data
|
this.extensionStubVersions = data.sort((a, b) =>
|
||||||
const validVersions = data.filter(v => v.is_version_compatible)
|
a.version < b.version ? 1 : -1
|
||||||
this.extensionData.stub_version = validVersions[0]
|
)
|
||||||
? validVersions[0].version
|
this.extensionData.stub_version = this.extensionStubVersions[0]
|
||||||
|
? this.extensionStubVersions[0].version
|
||||||
: ''
|
: ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
@@ -309,20 +311,7 @@ window.PageExtensionBuilder = {
|
|||||||
iframeDoc.body.style.transform = 'scale(0.8)'
|
iframeDoc.body.style.transform = 'scale(0.8)'
|
||||||
iframeDoc.body.style.transformOrigin = 'center top'
|
iframeDoc.body.style.transformOrigin = 'center top'
|
||||||
}
|
}
|
||||||
|
iframe.src = `/extensions/builder/preview/${this.extensionData.id}?page_name=${previewPageName}`
|
||||||
let componentName =
|
|
||||||
'Page' +
|
|
||||||
this.extensionData.id
|
|
||||||
.toLowerCase()
|
|
||||||
.split('_')
|
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
||||||
.join('')
|
|
||||||
if (previewPageName === 'public_page') componentName += 'Public'
|
|
||||||
iframe.src =
|
|
||||||
`/extensions/builder/preview?` +
|
|
||||||
`ext_id=${this.extensionData.id}` +
|
|
||||||
`&page=${previewPageName}` +
|
|
||||||
`&component=${componentName}`
|
|
||||||
},
|
},
|
||||||
initBasicData() {
|
initBasicData() {
|
||||||
this.extensionData.owner_data.fields = [
|
this.extensionData.owner_data.fields = [
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
window.PageExtensionBuilderPreview = {
|
|
||||||
template: '#page-extension-builder-preview',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
watch: {
|
|
||||||
name: 'reload'
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
extId: '',
|
|
||||||
pageName: '',
|
|
||||||
componentName: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async reload() {
|
|
||||||
await LNbits.utils.loadTemplate(
|
|
||||||
`/extensions/builder/preview/${this.extId}/template?page_name=${this.pageName}`
|
|
||||||
)
|
|
||||||
await LNbits.utils.loadScript(
|
|
||||||
`/extensions/builder/preview/${this.extId}/component?page_name=${this.pageName}`
|
|
||||||
)
|
|
||||||
|
|
||||||
this._component = window[this.componentName]
|
|
||||||
console.log(
|
|
||||||
'LNbits preview reloaded componentName:',
|
|
||||||
this.componentName,
|
|
||||||
!!this._component
|
|
||||||
)
|
|
||||||
this.$forceUpdate()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created() {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
|
||||||
this.extId = urlParams.get('ext_id') || ''
|
|
||||||
this.pageName = urlParams.get('page') || ''
|
|
||||||
this.componentName = urlParams.get('component') || ''
|
|
||||||
|
|
||||||
await this.reload()
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
if (this._component) {
|
|
||||||
return Vue.h(this._component)
|
|
||||||
}
|
|
||||||
return Vue.h('div', 'Loading...')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageFirstInstall = {
|
window.PageFirstInstall = {
|
||||||
template: '#page-first-install',
|
template: '#page-first-install',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loginData: {
|
loginData: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageHome = {
|
window.PageHome = {
|
||||||
template: '#page-home',
|
template: '#page-home',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lnurl: '',
|
lnurl: '',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageNodePublic = {
|
window.PageNodePublic = {
|
||||||
template: '#page-node-public',
|
template: '#page-node-public',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
window.PageNode = {
|
window.PageNode = {
|
||||||
|
mixins: [window.windowMixin],
|
||||||
template: '#page-node',
|
template: '#page-node',
|
||||||
config: {
|
config: {
|
||||||
globalProperties: {
|
globalProperties: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PagePayments = {
|
window.PagePayments = {
|
||||||
template: '#page-payments',
|
template: '#page-payments',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
payments: [],
|
payments: [],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageUsers = {
|
window.PageUsers = {
|
||||||
template: '#page-users',
|
template: '#page-users',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
paymentsWallet: {},
|
paymentsWallet: {},
|
||||||
@@ -438,20 +439,6 @@ window.PageUsers = {
|
|||||||
this.activeUser.show = false
|
this.activeUser.show = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async impersonateUser(user_id) {
|
|
||||||
try {
|
|
||||||
await LNbits.api.impersonateUser(user_id)
|
|
||||||
LNbits.utils.backupLocalStorage('impersonation', true)
|
|
||||||
this.$q.localStorage.setItem('lnbits.disclaimerShown', true)
|
|
||||||
window.location = '/wallet'
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Failed to impersonate user!'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async showWalletPayments(walletId) {
|
async showWalletPayments(walletId) {
|
||||||
this.activeUser.show = false
|
this.activeUser.show = false
|
||||||
await this.fetchWallets(this.users[0].id)
|
await this.fetchWallets(this.users[0].id)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageWallet = {
|
window.PageWallet = {
|
||||||
template: '#page-wallet',
|
template: '#page-wallet',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
parse: {
|
parse: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
window.PageWallets = {
|
window.PageWallets = {
|
||||||
template: '#page-wallets',
|
template: '#page-wallets',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: null,
|
user: null,
|
||||||
|
|||||||
+11
-52
@@ -67,29 +67,6 @@ window._lnbitsUtils = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
backupLocalStorage(backupKey, cleanup = false) {
|
|
||||||
const lnbitsEntries =
|
|
||||||
Object.entries(Quasar.LocalStorage.getAll()).filter(
|
|
||||||
([k, v]) => k.startsWith('lnbits.') && k !== `lnbits.${backupKey}`
|
|
||||||
) || []
|
|
||||||
|
|
||||||
Quasar.LocalStorage.setItem(`lnbits.${backupKey}`, lnbitsEntries)
|
|
||||||
if (cleanup) {
|
|
||||||
lnbitsEntries.forEach(([k, v]) => Quasar.LocalStorage.remove(k))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
restoreLocalStorage(backupKey) {
|
|
||||||
Object.entries(Quasar.LocalStorage.getAll())
|
|
||||||
.filter(
|
|
||||||
([k, v]) => k.startsWith('lnbits.') && k !== `lnbits.${backupKey}`
|
|
||||||
)
|
|
||||||
.forEach(([k, v]) => Quasar.LocalStorage.remove(k))
|
|
||||||
|
|
||||||
const lnbitsEntries =
|
|
||||||
Quasar.LocalStorage.getItem(`lnbits.${backupKey}`) || []
|
|
||||||
lnbitsEntries.forEach(([k, v]) => Quasar.LocalStorage.setItem(k, v))
|
|
||||||
Quasar.LocalStorage.remove(`lnbits.${backupKey}`)
|
|
||||||
},
|
|
||||||
async digestMessage(message) {
|
async digestMessage(message) {
|
||||||
const msgUint8 = new TextEncoder().encode(message)
|
const msgUint8 = new TextEncoder().encode(message)
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)
|
||||||
@@ -137,13 +114,6 @@ window._lnbitsUtils = {
|
|||||||
formatMsat(value) {
|
formatMsat(value) {
|
||||||
return this.formatSat(value / 1000)
|
return this.formatSat(value / 1000)
|
||||||
},
|
},
|
||||||
parseJSONSafe(str) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(str)
|
|
||||||
} catch (e) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
notifyApiError(error) {
|
notifyApiError(error) {
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
return console.error(error)
|
return console.error(error)
|
||||||
@@ -153,28 +123,17 @@ window._lnbitsUtils = {
|
|||||||
401: 'warning',
|
401: 'warning',
|
||||||
500: 'negative'
|
500: 'negative'
|
||||||
}
|
}
|
||||||
let messages = error.response.data.detail
|
Quasar.Notify.create({
|
||||||
if (messages) {
|
timeout: 5000,
|
||||||
messages = Array.isArray(messages)
|
type: types[error.response.status] || 'warning',
|
||||||
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
|
message:
|
||||||
: (messages = [messages])
|
error.response.data.message || error.response.data.detail || null,
|
||||||
} else {
|
caption:
|
||||||
messages = [error.response.data.message || error.response.data.detail]
|
[error.response.status, ' ', error.response.statusText]
|
||||||
}
|
.join('')
|
||||||
|
.toUpperCase() || null,
|
||||||
messages.forEach(message =>
|
icon: null
|
||||||
Quasar.Notify.create({
|
})
|
||||||
timeout: 5000,
|
|
||||||
type: types[error.response.status] || 'warning',
|
|
||||||
message,
|
|
||||||
caption:
|
|
||||||
[error.response.status, ' ', error.response.statusText]
|
|
||||||
.join('')
|
|
||||||
.toUpperCase() || null,
|
|
||||||
icon: null,
|
|
||||||
closeBtn: true
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
search(data, q, field, separator) {
|
search(data, q, field, separator) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -45,8 +45,6 @@
|
|||||||
"js/pages/error.js",
|
"js/pages/error.js",
|
||||||
"js/pages/home.js",
|
"js/pages/home.js",
|
||||||
"js/pages/extensions_builder.js",
|
"js/pages/extensions_builder.js",
|
||||||
"js/pages/extensions_builder.js",
|
|
||||||
"js/pages/extensions_builder_preview.js",
|
|
||||||
"js/pages/extensions.js",
|
"js/pages/extensions.js",
|
||||||
"js/pages/first-install.js",
|
"js/pages/first-install.js",
|
||||||
"js/pages/payments.js",
|
"js/pages/payments.js",
|
||||||
@@ -78,7 +76,6 @@
|
|||||||
"js/components/lnbits-wallet-paylinks.js",
|
"js/components/lnbits-wallet-paylinks.js",
|
||||||
"js/components/lnbits-wallet-extra.js",
|
"js/components/lnbits-wallet-extra.js",
|
||||||
"js/components/lnbits-home-logos.js",
|
"js/components/lnbits-home-logos.js",
|
||||||
"js/components/lnbits-error.js",
|
|
||||||
"js/components/lnbits-qrcode.js",
|
"js/components/lnbits-qrcode.js",
|
||||||
"js/components/lnbits-qrcode-lnurl.js",
|
"js/components/lnbits-qrcode-lnurl.js",
|
||||||
"js/components/lnbits-disclaimer.js",
|
"js/components/lnbits-disclaimer.js",
|
||||||
|
|||||||
@@ -94,6 +94,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- scripts from extensions -->
|
<!-- scripts from extensions -->
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
<!-- modules js -->
|
||||||
|
<script src="/static/modules/components.umd.js"></script>
|
||||||
<!-- components js -->
|
<!-- components js -->
|
||||||
{% for url in INCLUDED_COMPONENTS %}
|
{% for url in INCLUDED_COMPONENTS %}
|
||||||
<script src="{{ static_url_for('static', url) }}"></script>
|
<script src="{{ static_url_for('static', url) }}"></script>
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ include('components/lnbits-wallet-api-docs.vue') %} {%
|
|||||||
include('components/lnbits-wallet-share.vue') %} {%
|
include('components/lnbits-wallet-share.vue') %} {%
|
||||||
include('components/lnbits-wallet-charts.vue') %} {%
|
include('components/lnbits-wallet-charts.vue') %} {%
|
||||||
include('components/lnbits-wallet-paylinks.vue') %} {%
|
include('components/lnbits-wallet-paylinks.vue') %} {%
|
||||||
include('components/lnbits-wallet-extra.vue') %} {%
|
include('components/lnbits-wallet-extra.vue') %}
|
||||||
include('components/lnbits-error.vue') %}
|
|
||||||
|
|
||||||
<template id="lnbits-manage">
|
<template id="lnbits-manage">
|
||||||
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
|
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
|
||||||
@@ -561,32 +560,11 @@ include('components/lnbits-error.vue') %}
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="lnbits-extension-rating">
|
<template id="lnbits-extension-rating">
|
||||||
<div class="row items-center q-gutter-xs" style="margin-bottom: 3px">
|
<div style="margin-bottom: 3px">
|
||||||
<q-rating
|
<q-rating v-model="rating" size="1.5em" :max="5" color="primary"
|
||||||
:model-value="displayRating"
|
><q-tooltip>
|
||||||
size="1.4em"
|
<span v-text="$t('extension_rating_soon')"></span> </q-tooltip
|
||||||
:max="5"
|
></q-rating>
|
||||||
color="primary"
|
|
||||||
icon="star_border"
|
|
||||||
icon-selected="star"
|
|
||||||
icon-half="star_half"
|
|
||||||
readonly
|
|
||||||
:class="clickable ? 'cursor-pointer' : ''"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<q-tooltip v-if="!hasData">
|
|
||||||
<span v-text="$t('extension_rating_soon')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
<q-tooltip v-else-if="clickable">
|
|
||||||
<span v-text="$t('reviews_open')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-rating>
|
|
||||||
<div
|
|
||||||
class="text-caption text-grey"
|
|
||||||
v-if="count !== null && count !== undefined"
|
|
||||||
>
|
|
||||||
<span v-text="`(${count})`"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -137,19 +137,6 @@
|
|||||||
:hint="$t('extension_builder_manifest_url_hint')"
|
:hint="$t('extension_builder_manifest_url_hint')"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<p>
|
|
||||||
<span v-text="$t('reviews_url')"></span>
|
|
||||||
</p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_extensions_reviews_url"
|
|
||||||
:label="$t('reviews_url_label')"
|
|
||||||
:hint="$t('reviews_url_hint')"
|
|
||||||
type="url"
|
|
||||||
autocomplete="off"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<template id="lnbits-error">
|
|
||||||
<div class="text-center q-pa-md flex flex-center">
|
|
||||||
<div>
|
|
||||||
<div class="error-code" v-text="code"></div>
|
|
||||||
<div class="error-message" v-text="message"></div>
|
|
||||||
<div class="q-mx-auto q-mt-lg justify-center" style="width: max-content">
|
|
||||||
<q-btn
|
|
||||||
v-if="isExtension"
|
|
||||||
color="primary"
|
|
||||||
@click="goToExtension()"
|
|
||||||
label="Go To Extension"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-else-if="g.isUserAuthorized"
|
|
||||||
color="primary"
|
|
||||||
@click="goToWallet()"
|
|
||||||
label="Go to Wallet"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn v-else color="primary" @click="goBack()" label="Go Back"></q-btn>
|
|
||||||
<span class="q-mx-md">OR</span>
|
|
||||||
<q-btn color="secondary" @click="goHome()" label="Go Home"></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -72,6 +72,7 @@
|
|||||||
</q-badge>
|
</q-badge>
|
||||||
|
|
||||||
<lnbits-language-dropdown></lnbits-language-dropdown>
|
<lnbits-language-dropdown></lnbits-language-dropdown>
|
||||||
|
|
||||||
<q-btn-dropdown v-if="g.user" flat rounded size="sm" class="q-pl-sm">
|
<q-btn-dropdown v-if="g.user" flat rounded size="sm" class="q-pl-sm">
|
||||||
<template v-slot:label>
|
<template v-slot:label>
|
||||||
<q-avatar
|
<q-avatar
|
||||||
@@ -135,20 +136,6 @@
|
|||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
<q-btn
|
|
||||||
v-if="g.isUserImpersonated"
|
|
||||||
@click="stopImpersonation"
|
|
||||||
rounded
|
|
||||||
size="sm"
|
|
||||||
class="q-pl-sm"
|
|
||||||
color="negative"
|
|
||||||
icon="face_retouching_off"
|
|
||||||
label="Stop"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
><span v-text="$t('stop_user_impersonation')"></span
|
|
||||||
></q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -83,7 +83,9 @@
|
|||||||
class="text-center"
|
class="text-center"
|
||||||
v-text="$t('export_to_phone_desc')"
|
v-text="$t('export_to_phone_desc')"
|
||||||
></p>
|
></p>
|
||||||
<lnbits-qrcode :value="exportUrl"></lnbits-qrcode>
|
<lnbits-qrcode
|
||||||
|
:value="`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`"
|
||||||
|
></lnbits-qrcode>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
></q-input>
|
></q-input>
|
||||||
<q-input
|
<q-input
|
||||||
v-if="g.user.hasPassword"
|
v-if="g.user.has_password"
|
||||||
v-model="credentialsData.oldPassword"
|
v-model="credentialsData.oldPassword"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -941,7 +941,7 @@
|
|||||||
v-if="props.row.thumbnail_base64"
|
v-if="props.row.thumbnail_base64"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="color: inherit"
|
style="color: inherit"
|
||||||
:href="`/api/v1/assets/${props.row.id}/data`"
|
:href="`/api/v1/assets/${props.row.id}/binary`"
|
||||||
>
|
>
|
||||||
<q-img
|
<q-img
|
||||||
:src="
|
:src="
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
<q-space></q-space>
|
<q-space></q-space>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-if="$q.screen.gt.sm"
|
|
||||||
:label="$t('search_extensions')"
|
:label="$t('search_extensions')"
|
||||||
:dense="dense"
|
:dense="dense"
|
||||||
class="float-right q-pr-xl"
|
class="float-right q-pr-xl"
|
||||||
@@ -34,14 +33,6 @@
|
|||||||
</q-icon>
|
</q-icon>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
flat
|
|
||||||
icon="search"
|
|
||||||
@click="searchToggle = !searchToggle"
|
|
||||||
class="q-mr-md"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="g.user.admin && updatableExtensions?.length"
|
v-if="g.user.admin && updatableExtensions?.length"
|
||||||
@click="showUpdateAllDialog = true"
|
@click="showUpdateAllDialog = true"
|
||||||
@@ -77,25 +68,6 @@
|
|||||||
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
<div v-if="$q.screen.lt.sm && searchToggle" class="q-pa-sm">
|
|
||||||
<q-input
|
|
||||||
:label="$t('search_extensions')"
|
|
||||||
dense
|
|
||||||
class=""
|
|
||||||
v-model="searchTerm"
|
|
||||||
autofocus
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
v-if="searchTerm !== ''"
|
|
||||||
name="close"
|
|
||||||
@click="searchTerm = ''"
|
|
||||||
class="cursor-pointer"
|
|
||||||
>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -163,22 +135,7 @@
|
|||||||
v-text="extension.name"
|
v-text="extension.name"
|
||||||
></div>
|
></div>
|
||||||
<div style="justify-content: space-between; display: flex">
|
<div style="justify-content: space-between; display: flex">
|
||||||
<lnbits-extension-rating
|
<lnbits-extension-rating :rating="0" />
|
||||||
:rating="
|
|
||||||
formatAvg(
|
|
||||||
extension.reviewStats
|
|
||||||
? extension.reviewStats.avg_rating
|
|
||||||
: 0
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:count="
|
|
||||||
extension.reviewStats
|
|
||||||
? extension.reviewStats.review_count
|
|
||||||
: null
|
|
||||||
"
|
|
||||||
:clickable="!!reviewsUrl"
|
|
||||||
@click="openReviews(extension)"
|
|
||||||
/>
|
|
||||||
<q-btn-group size="xs" style="margin: 5px 0">
|
<q-btn-group size="xs" style="margin: 5px 0">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="extension.hasFreeRelease"
|
v-if="extension.hasFreeRelease"
|
||||||
@@ -1013,20 +970,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12 col-md-4 q-pl-sm">
|
<div class="col-sm-12 col-md-4 q-pl-sm">
|
||||||
<lnbits-extension-rating
|
<lnbits-extension-rating
|
||||||
:rating="
|
:rating="0"
|
||||||
formatAvg(
|
size="2.5em"
|
||||||
selectedExtension && selectedExtension.reviewStats
|
|
||||||
? selectedExtension.reviewStats.avg_rating
|
|
||||||
: 0
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:count="
|
|
||||||
selectedExtension && selectedExtension.reviewStats
|
|
||||||
? selectedExtension.reviewStats.review_count
|
|
||||||
: null
|
|
||||||
"
|
|
||||||
:clickable="!!reviewsUrl"
|
|
||||||
@click="openReviews(selectedExtension)"
|
|
||||||
></lnbits-extension-rating>
|
></lnbits-extension-rating>
|
||||||
<div class="q-mt-md">
|
<div class="q-mt-md">
|
||||||
<b>
|
<b>
|
||||||
@@ -1084,182 +1029,6 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
<q-dialog v-model="reviewsDialog.show" position="top">
|
|
||||||
<q-card
|
|
||||||
class="q-pa-md lnbits__dialog-card"
|
|
||||||
style="width: 900px; max-width: 95vw"
|
|
||||||
>
|
|
||||||
<q-card-section class="q-gutter-y-sm">
|
|
||||||
<div class="row items-center">
|
|
||||||
<div class="col">
|
|
||||||
<div
|
|
||||||
class="text-h6"
|
|
||||||
v-text="
|
|
||||||
(reviewsDialog.extension && reviewsDialog.extension.name) ||
|
|
||||||
(reviewsDialog.extension && reviewsDialog.extension.id)
|
|
||||||
"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="text-caption text-grey"
|
|
||||||
v-text="reviewsDialog.extension && reviewsDialog.extension.id"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<lnbits-extension-rating
|
|
||||||
:rating="
|
|
||||||
formatAvg(
|
|
||||||
reviewsDialog.extension && reviewsDialog.extension.reviewStats
|
|
||||||
? reviewsDialog.extension.reviewStats.avg_rating
|
|
||||||
: 0
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:count="
|
|
||||||
reviewsDialog.extension && reviewsDialog.extension.reviewStats
|
|
||||||
? reviewsDialog.extension.reviewStats.review_count
|
|
||||||
: null
|
|
||||||
"
|
|
||||||
></lnbits-extension-rating>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-card-section v-if="!reviewsUrl">
|
|
||||||
<div
|
|
||||||
class="text-negative"
|
|
||||||
v-text="$t('reviews_url_not_configured')"
|
|
||||||
></div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section v-else>
|
|
||||||
<div v-if="reviewsDialog.loading" class="row justify-center q-pa-lg">
|
|
||||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div v-if="reviewsDialog.error" class="text-negative q-mb-md">
|
|
||||||
<span v-text="reviewsDialog.error"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-form @submit="submitReview" class="q-gutter-md">
|
|
||||||
<div class="row q-col-gutter-lg">
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
v-model="reviewsDialog.form.name"
|
|
||||||
:label="$t('reviews_name')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-rating
|
|
||||||
v-model="reviewsDialog.form.rating"
|
|
||||||
max="10"
|
|
||||||
icon="star_border"
|
|
||||||
icon-selected="star"
|
|
||||||
icon-half="star_half"
|
|
||||||
color="primary"
|
|
||||||
class="float-right"
|
|
||||||
></q-rating>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
type="textarea"
|
|
||||||
autogrow
|
|
||||||
v-model="reviewsDialog.form.comment"
|
|
||||||
:label="$t('reviews_comment')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-btn
|
|
||||||
type="submit"
|
|
||||||
color="primary"
|
|
||||||
class="float-right"
|
|
||||||
unelevated
|
|
||||||
:loading="reviewsDialog.submitting"
|
|
||||||
:disable="!reviewsDialog.form.rating"
|
|
||||||
:label="$t('reviews_submit')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
<q-separator class="q-my-md"></q-separator>
|
|
||||||
<div class="row q-col-gutter-lg">
|
|
||||||
<div class="col-12">
|
|
||||||
<div v-if="reviews.length === 0" class="text-grey q-mb-md">
|
|
||||||
<span v-text="$t('no_reviews')"></span>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<q-table
|
|
||||||
:rows="reviews"
|
|
||||||
:columns="reviewsTable.columns"
|
|
||||||
v-model:pagination="reviewsTable.pagination"
|
|
||||||
@request="getTagReviews"
|
|
||||||
:loading="reviewsTable.loading"
|
|
||||||
row-key="name"
|
|
||||||
:filter="reviewsTable.search"
|
|
||||||
>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td key="name" :props="props">
|
|
||||||
<span v-text="props.row.name"></span>
|
|
||||||
</q-td>
|
|
||||||
<q-td key="comment" :props="props">
|
|
||||||
<span v-text="props.row.comment"></span>
|
|
||||||
</q-td>
|
|
||||||
<q-td key="created_at" :props="props">
|
|
||||||
<span
|
|
||||||
v-text="formatReviewDate(props.row.created_at)"
|
|
||||||
></span>
|
|
||||||
</q-td>
|
|
||||||
<q-td key="rating" :props="props">
|
|
||||||
<q-rating
|
|
||||||
class="float-right q-pt-xs"
|
|
||||||
readonly
|
|
||||||
icon="star_border"
|
|
||||||
icon-selected="star"
|
|
||||||
icon-half="star_half"
|
|
||||||
:model-value="formatAvg(props.row.rating)"
|
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
></q-rating>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator v-if="reviews.length"></q-separator>
|
|
||||||
<q-card-section>
|
|
||||||
<q-btn
|
|
||||||
@click="reviewsDialog.show = false"
|
|
||||||
color="grey"
|
|
||||||
outline
|
|
||||||
class="float-right"
|
|
||||||
:label="$t('close')"
|
|
||||||
></q-btn>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<q-dialog v-model="paymentDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-md lnbits__dialog-card">
|
|
||||||
<q-card-section>
|
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-xl">
|
|
||||||
<lnbits-qrcode
|
|
||||||
:value="paymentDialog.invoice"
|
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></lnbits-qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions align="between">
|
|
||||||
<q-btn v-close-popup flat color="grey" :label="$t('close')"></q-btn>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<q-dialog v-model="showUpdateAllDialog" position="top">
|
<q-dialog v-model="showUpdateAllDialog" position="top">
|
||||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -656,20 +656,7 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
<span v-text="shortify(props.row.id)"></span>
|
<span v-text="shortify(props.row.id)"></span>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>
|
<q-td v-text="props.row.username"></q-td>
|
||||||
<q-btn
|
|
||||||
icon="face"
|
|
||||||
size="sm"
|
|
||||||
flat
|
|
||||||
class="cursor-pointer q-mr-xs"
|
|
||||||
@click="impersonateUser(props.row.id)"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
><span v-text="$t('start_user_impersonation')"></span
|
|
||||||
></q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<span v-text="props.row.username"></span>
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td v-text="props.row.email"></q-td>
|
<q-td v-text="props.row.email"></q-td>
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from .corelightning import CoreLightningWallet as CLightningWallet
|
|||||||
from .corelightningrest import CoreLightningRestWallet
|
from .corelightningrest import CoreLightningRestWallet
|
||||||
from .eclair import EclairWallet
|
from .eclair import EclairWallet
|
||||||
from .fake import FakeWallet
|
from .fake import FakeWallet
|
||||||
from .lightspark import LightsparkSparkWallet
|
|
||||||
from .lnbits import LNbitsWallet
|
from .lnbits import LNbitsWallet
|
||||||
from .lndgrpc import LndWallet
|
from .lndgrpc import LndWallet
|
||||||
from .lndrest import LndRestWallet
|
from .lndrest import LndRestWallet
|
||||||
@@ -70,7 +69,6 @@ __all__ = [
|
|||||||
"FakeWallet",
|
"FakeWallet",
|
||||||
"LNPayWallet",
|
"LNPayWallet",
|
||||||
"LNbitsWallet",
|
"LNbitsWallet",
|
||||||
"LightsparkSparkWallet",
|
|
||||||
"LnTipsWallet",
|
"LnTipsWallet",
|
||||||
"LndRestWallet",
|
"LndRestWallet",
|
||||||
"LndWallet",
|
"LndWallet",
|
||||||
|
|||||||
@@ -1,417 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import uuid
|
|
||||||
from collections.abc import AsyncGenerator
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from bolt11 import decode as bolt11_decode
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.helpers import download_url, normalize_endpoint
|
|
||||||
from lnbits.settings import settings
|
|
||||||
|
|
||||||
from .base import (
|
|
||||||
InvoiceResponse,
|
|
||||||
PaymentFailedStatus,
|
|
||||||
PaymentPendingStatus,
|
|
||||||
PaymentResponse,
|
|
||||||
PaymentStatus,
|
|
||||||
PaymentSuccessStatus,
|
|
||||||
StatusResponse,
|
|
||||||
Wallet,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SparkSidecarError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LightsparkSparkWallet(Wallet):
|
|
||||||
"""
|
|
||||||
Spark L2 funding source via a local sidecar service.
|
|
||||||
|
|
||||||
Required settings/env:
|
|
||||||
- SPARK_L2_ENDPOINT (default http://127.0.0.1:8765)
|
|
||||||
Optional:
|
|
||||||
- SPARK_L2_API_KEY
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._status = "Initializing"
|
|
||||||
self._sidecar_path = Path(settings.lnbits_data_folder, "light_spark")
|
|
||||||
|
|
||||||
self.pending_invoices: list[str] = []
|
|
||||||
|
|
||||||
self.endpoint = "http://127.0.0.1:8765"
|
|
||||||
self._api_key = uuid.uuid4().hex
|
|
||||||
if settings.spark_l2_internal_sidecar_version:
|
|
||||||
self._sidecar_version = settings.spark_l2_internal_sidecar_version
|
|
||||||
self.sidecar_task = asyncio.create_task(self._start_sidecar())
|
|
||||||
logger.info(f"Internal Spark sidecar ({self._sidecar_version}).")
|
|
||||||
elif settings.spark_l2_external_endpoint:
|
|
||||||
self.endpoint = normalize_endpoint(
|
|
||||||
cast(str, settings.spark_l2_external_endpoint)
|
|
||||||
)
|
|
||||||
self._api_key = settings.spark_l2_external_api_key
|
|
||||||
logger.info(f"Using external Spark sidecar endpoint: {self.endpoint}")
|
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
"No Spark sidecar configuration found. Please set either "
|
|
||||||
"spark_l2_internal_sidecar_version or spark_l2_external_endpoint."
|
|
||||||
)
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent, "X-Api-Key": self._api_key}
|
|
||||||
|
|
||||||
self.client = httpx.AsyncClient(
|
|
||||||
base_url=self.endpoint,
|
|
||||||
headers=headers,
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def cleanup(self):
|
|
||||||
try:
|
|
||||||
await self.client.aclose()
|
|
||||||
self.sidecar_task.cancel()
|
|
||||||
except RuntimeError as e:
|
|
||||||
logger.warning(f"Error closing wallet connection: {e}")
|
|
||||||
|
|
||||||
async def _request(
|
|
||||||
self, method: str, path: str, json_data: dict[str, Any] | None = None
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
error_message = None
|
|
||||||
try:
|
|
||||||
r = await self.client.request(method, path, json=json_data)
|
|
||||||
r.raise_for_status()
|
|
||||||
j = r.json()
|
|
||||||
except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as exc:
|
|
||||||
if isinstance(exc, httpx.HTTPStatusError) and exc.response is not None:
|
|
||||||
try:
|
|
||||||
error_json = exc.response.json()
|
|
||||||
if "error" in error_json:
|
|
||||||
error_message = error_json["error"]
|
|
||||||
except Exception as json_exc:
|
|
||||||
logger.error(
|
|
||||||
f"Failed to parse Spark error response as JSON: {json_exc}"
|
|
||||||
)
|
|
||||||
raise SparkSidecarError(
|
|
||||||
error_message or f"Spark sidecar request error: '{exc}'"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
if error_message or j.get("error"):
|
|
||||||
raise SparkSidecarError(
|
|
||||||
error_message or f"Spark sidecar error: {j['error']}"
|
|
||||||
)
|
|
||||||
return j
|
|
||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
|
||||||
try:
|
|
||||||
res = await self._request("POST", "/v1/balance")
|
|
||||||
balance_msat = res.get("balance_msat")
|
|
||||||
if balance_msat is not None:
|
|
||||||
return StatusResponse(None, int(balance_msat))
|
|
||||||
balance_sats = res.get("balance_sats")
|
|
||||||
if balance_sats is None:
|
|
||||||
return StatusResponse("Spark sidecar: missing balance.", 0)
|
|
||||||
return StatusResponse(None, int(balance_sats) * 1000)
|
|
||||||
except Exception as e:
|
|
||||||
return StatusResponse(f"Spark sidecar status error: {e}", 0)
|
|
||||||
|
|
||||||
async def create_invoice(
|
|
||||||
self,
|
|
||||||
amount: int,
|
|
||||||
memo: str | None = None,
|
|
||||||
description_hash: bytes | None = None,
|
|
||||||
unhashed_description: bytes | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> InvoiceResponse:
|
|
||||||
expiry = kwargs.get("expiry")
|
|
||||||
expiry_secs = int(expiry) if expiry else None
|
|
||||||
|
|
||||||
description_hash_hex = None
|
|
||||||
if description_hash:
|
|
||||||
description_hash_hex = description_hash.hex()
|
|
||||||
elif unhashed_description:
|
|
||||||
description_hash_hex = hashlib.sha256(unhashed_description).hexdigest()
|
|
||||||
|
|
||||||
try:
|
|
||||||
payload = {
|
|
||||||
"amount_sats": int(amount),
|
|
||||||
"memo": (memo or "") if not description_hash_hex else None,
|
|
||||||
"description_hash": description_hash_hex,
|
|
||||||
"expiry_seconds": expiry_secs,
|
|
||||||
}
|
|
||||||
res = await self._request("POST", "/v1/invoices", payload)
|
|
||||||
bolt11 = res.get("payment_request")
|
|
||||||
checking_id = res.get("checking_id")
|
|
||||||
if not bolt11 or not checking_id:
|
|
||||||
raise SparkSidecarError(
|
|
||||||
"Spark sidecar invoice response missing fields."
|
|
||||||
)
|
|
||||||
self.pending_invoices.append(checking_id)
|
|
||||||
|
|
||||||
return InvoiceResponse(
|
|
||||||
ok=True,
|
|
||||||
payment_request=bolt11,
|
|
||||||
checking_id=checking_id,
|
|
||||||
preimage=res.get("preimage"),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return InvoiceResponse(ok=False, error_message=str(e))
|
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
|
||||||
try:
|
|
||||||
max_fee_sats = (int(fee_limit_msat) + 999) // 1000
|
|
||||||
|
|
||||||
payment_hash = None
|
|
||||||
try:
|
|
||||||
payment_hash = bolt11_decode(bolt11).payment_hash
|
|
||||||
except Exception:
|
|
||||||
payment_hash = None
|
|
||||||
payload = {
|
|
||||||
"bolt11": bolt11,
|
|
||||||
"max_fee_sats": max_fee_sats,
|
|
||||||
"payment_hash": payment_hash,
|
|
||||||
}
|
|
||||||
res = await self._request("POST", "/v1/payments", payload)
|
|
||||||
checking_id = payment_hash or res.get("checking_id")
|
|
||||||
if not checking_id:
|
|
||||||
raise SparkSidecarError(
|
|
||||||
"Spark sidecar payment response missing checking_id."
|
|
||||||
)
|
|
||||||
status = res.get("status")
|
|
||||||
fee_msat = res.get("fee_msat")
|
|
||||||
ok = None
|
|
||||||
if status:
|
|
||||||
ok = self._map_payment_ok(status)
|
|
||||||
return PaymentResponse(
|
|
||||||
ok=ok,
|
|
||||||
checking_id=checking_id,
|
|
||||||
fee_msat=int(fee_msat) if fee_msat is not None else None,
|
|
||||||
preimage=res.get("preimage"),
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return PaymentResponse(ok=False, error_message=str(e))
|
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
|
||||||
try:
|
|
||||||
res = await self._request("GET", f"/v1/invoices/{checking_id}")
|
|
||||||
status = res.get("status")
|
|
||||||
if not status:
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
return self._map_invoice_status(status)
|
|
||||||
except Exception:
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
|
||||||
try:
|
|
||||||
res = await self._request("GET", f"/v1/payments/{checking_id}")
|
|
||||||
status = res.get("status")
|
|
||||||
fee_msat = res.get("fee_msat")
|
|
||||||
preimage = res.get("preimage")
|
|
||||||
if not status:
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
mapped = self._map_payment_status(status)
|
|
||||||
if mapped.success:
|
|
||||||
return PaymentSuccessStatus(
|
|
||||||
fee_msat=int(fee_msat) if fee_msat is not None else None,
|
|
||||||
preimage=preimage,
|
|
||||||
)
|
|
||||||
if mapped.failed:
|
|
||||||
return PaymentFailedStatus()
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
except Exception:
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
|
|
||||||
def _map_invoice_status(self, status: str) -> PaymentStatus:
|
|
||||||
success = {
|
|
||||||
"LIGHTNING_PAYMENT_RECEIVED",
|
|
||||||
"TRANSFER_COMPLETED",
|
|
||||||
"PAYMENT_PREIMAGE_RECOVERED",
|
|
||||||
}
|
|
||||||
failed = {
|
|
||||||
"TRANSFER_FAILED",
|
|
||||||
"PAYMENT_PREIMAGE_RECOVERING_FAILED",
|
|
||||||
"REFUND_SIGNING_FAILED",
|
|
||||||
"REFUND_SIGNING_COMMITMENTS_QUERYING_FAILED",
|
|
||||||
"TRANSFER_CREATION_FAILED",
|
|
||||||
}
|
|
||||||
if status in success:
|
|
||||||
return PaymentSuccessStatus()
|
|
||||||
if status in failed:
|
|
||||||
return PaymentFailedStatus()
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
|
|
||||||
def _map_payment_status(self, status: str) -> PaymentStatus:
|
|
||||||
success = {
|
|
||||||
"LIGHTNING_PAYMENT_SUCCEEDED",
|
|
||||||
"TRANSFER_COMPLETED",
|
|
||||||
"PREIMAGE_PROVIDED",
|
|
||||||
}
|
|
||||||
failed = {
|
|
||||||
"LIGHTNING_PAYMENT_FAILED",
|
|
||||||
"TRANSFER_FAILED",
|
|
||||||
"PREIMAGE_PROVIDING_FAILED",
|
|
||||||
"USER_TRANSFER_VALIDATION_FAILED",
|
|
||||||
"USER_SWAP_RETURN_FAILED",
|
|
||||||
}
|
|
||||||
if status in success:
|
|
||||||
return PaymentSuccessStatus()
|
|
||||||
if status in failed:
|
|
||||||
return PaymentFailedStatus()
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
|
|
||||||
def _map_payment_ok(self, status: str) -> bool | None:
|
|
||||||
mapped = self._map_payment_status(status)
|
|
||||||
if mapped.success:
|
|
||||||
return True
|
|
||||||
if mapped.failed:
|
|
||||||
return False
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def _poll_pending_invoices(self) -> AsyncGenerator[str, None]:
|
|
||||||
while settings.lnbits_running:
|
|
||||||
for invoice in list(self.pending_invoices):
|
|
||||||
try:
|
|
||||||
status = await self.get_invoice_status(invoice)
|
|
||||||
if status.paid:
|
|
||||||
yield invoice
|
|
||||||
self.pending_invoices.remove(invoice)
|
|
||||||
elif status.failed:
|
|
||||||
self.pending_invoices.remove(invoice)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error(f"could not get status of invoice {invoice}: '{exc}' ")
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
|
||||||
stream_path = "/v1/invoices/stream"
|
|
||||||
while settings.lnbits_running:
|
|
||||||
try:
|
|
||||||
async with self.client.stream("GET", stream_path, timeout=None) as r:
|
|
||||||
if r.status_code in {404, 405}:
|
|
||||||
logger.warning(
|
|
||||||
"Spark sidecar invoice stream not available, "
|
|
||||||
"falling back to polling."
|
|
||||||
)
|
|
||||||
async for checking_id in self._poll_pending_invoices():
|
|
||||||
yield checking_id
|
|
||||||
return
|
|
||||||
r.raise_for_status()
|
|
||||||
logger.info("connected to Spark sidecar invoice stream.")
|
|
||||||
async for line in r.aiter_lines():
|
|
||||||
if not line or not line.startswith("data:"):
|
|
||||||
continue
|
|
||||||
data = json.loads(line[5:].strip())
|
|
||||||
checking_id = data.get("checking_id")
|
|
||||||
if not checking_id:
|
|
||||||
continue
|
|
||||||
yield checking_id
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error(
|
|
||||||
"lost connection to Spark sidecar invoice stream: "
|
|
||||||
f"'{exc}' retrying in 5 seconds"
|
|
||||||
)
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
|
|
||||||
async def _start_sidecar(self):
|
|
||||||
logger.info("Starting Spark sidecar")
|
|
||||||
node_path = shutil.which("node")
|
|
||||||
if not node_path:
|
|
||||||
logger.error("Node.js not found in PATH, cannot start Spark sidecar")
|
|
||||||
return
|
|
||||||
logger.info(f"Node.js found: {node_path}")
|
|
||||||
|
|
||||||
repo, version = "spark_sidecar", self._sidecar_version
|
|
||||||
node_modules_path = Path(self._sidecar_path, f"{repo}-{version}")
|
|
||||||
await self._prepare_sidecar(repo, version, node_modules_path)
|
|
||||||
|
|
||||||
await self._start_sidecar_process(node_path, node_modules_path)
|
|
||||||
|
|
||||||
async def _prepare_sidecar(self, repo: str, version: str, node_modules_path: Path):
|
|
||||||
if not Path(node_modules_path, "package.json").is_file():
|
|
||||||
await self._download_sidecar(repo, version)
|
|
||||||
else:
|
|
||||||
logger.info("Spark sidecar already downloaded.")
|
|
||||||
|
|
||||||
if not Path(node_modules_path, "node_modules").is_dir():
|
|
||||||
self._install_sidecar_packages(node_modules_path)
|
|
||||||
else:
|
|
||||||
logger.info("Spark sidecar npm dependencies already installed.")
|
|
||||||
|
|
||||||
def _install_sidecar_packages(self, node_modules_path: Path):
|
|
||||||
logger.info(f"Installing Spark sidecar npm dependencies {node_modules_path}")
|
|
||||||
npm_path = shutil.which("npm")
|
|
||||||
if not npm_path:
|
|
||||||
logger.error("npm not found in PATH, cannot start Spark sidecar")
|
|
||||||
return
|
|
||||||
logger.info(f"npm found: {npm_path}")
|
|
||||||
|
|
||||||
result = subprocess.run( # noqa: S603
|
|
||||||
[npm_path, "install"],
|
|
||||||
cwd=str(node_modules_path),
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
shell=False,
|
|
||||||
check=True, # raises an exception if npm fails
|
|
||||||
)
|
|
||||||
logger.info("Spark sidecar npm dependencies installed.")
|
|
||||||
logger.info("npm install output:")
|
|
||||||
logger.info(result.stdout)
|
|
||||||
logger.error(result.stderr)
|
|
||||||
|
|
||||||
async def _start_sidecar_process(self, node_path: str, node_modules_path: Path):
|
|
||||||
logger.info("Starting Spark sidecar node process.")
|
|
||||||
|
|
||||||
env = {
|
|
||||||
"SPARK_NETWORK": settings.spark_l2_network,
|
|
||||||
"SPARK_SIDECAR_API_KEY": self._api_key or "",
|
|
||||||
"SPARK_PAY_WAIT_MS": str(settings.spark_l2_pay_wait_ms),
|
|
||||||
"SPARK_MNEMONIC": str(settings.spark_l2_mnemonic),
|
|
||||||
}
|
|
||||||
|
|
||||||
process = subprocess.Popen( # noqa: S603
|
|
||||||
[node_path, "server.mjs"],
|
|
||||||
env=env,
|
|
||||||
cwd=str(node_modules_path),
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
shell=False,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Started Spark sidecar node process.")
|
|
||||||
await asyncio.to_thread(self._log_process_output, process)
|
|
||||||
|
|
||||||
async def _download_sidecar(self, repo: str, version: str):
|
|
||||||
zip_path = Path(self._sidecar_path, f"{repo}.zip")
|
|
||||||
logger.info(f"⏳ Downloading Spark sidecar to {zip_path}")
|
|
||||||
Path(zip_path).parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
await asyncio.to_thread(
|
|
||||||
download_url,
|
|
||||||
f"https://github.com/lnbits/{repo}/archive/refs/tags/v{version}.zip",
|
|
||||||
zip_path,
|
|
||||||
)
|
|
||||||
logger.info("✅ Downloaded Spark sidecar.")
|
|
||||||
|
|
||||||
logger.info("⏳ Extracting Spark sidecar.")
|
|
||||||
|
|
||||||
shutil.unpack_archive(
|
|
||||||
zip_path,
|
|
||||||
self._sidecar_path,
|
|
||||||
)
|
|
||||||
logger.info("✅ Extracted Spark sidecar.")
|
|
||||||
shutil.rmtree(zip_path, ignore_errors=True)
|
|
||||||
# todo: remove zip
|
|
||||||
|
|
||||||
def _log_process_output(self, process: subprocess.Popen):
|
|
||||||
if process.stdout:
|
|
||||||
for line in process.stdout:
|
|
||||||
logger.warning(f"[Lightspark]: {line}", end="")
|
|
||||||
else:
|
|
||||||
logger.error(" No output captured for Spark sidecar.")
|
|
||||||
@@ -178,7 +178,7 @@ class LndRestWallet(Wallet):
|
|||||||
"no_inflight_updates": True,
|
"no_inflight_updates": True,
|
||||||
}
|
}
|
||||||
if settings.lnd_rest_allow_self_payment:
|
if settings.lnd_rest_allow_self_payment:
|
||||||
req["allow_self_payment"] = True
|
req["allow_self_payment"] = 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
|
|||||||
Generated
+2899
-1
File diff suppressed because it is too large
Load Diff
+13
-5
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lnbits",
|
"name": "lnbits",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"sass": "./node_modules/.bin/sass ./lnbits/static/scss/base.scss > ./lnbits/static/css/base.css",
|
"sass": "./node_modules/.bin/sass ./lnbits/static/scss/base.scss > ./lnbits/static/css/base.css",
|
||||||
"vendor_copy": "node -e \"require('./package.json').vendor.forEach((file) => require('fs').copyFileSync(file, './lnbits/static/vendor/'+file.split('/').pop()))\"",
|
"vendor_copy": "node -e \"require('./package.json').vendor.forEach((file) => require('fs').copyFileSync(file, './lnbits/static/vendor/'+file.split('/').pop()))\"",
|
||||||
@@ -10,15 +11,25 @@
|
|||||||
"vendor_minify_css": "./node_modules/.bin/cleancss -o ./lnbits/static/bundle.min.css ./lnbits/static/bundle.css",
|
"vendor_minify_css": "./node_modules/.bin/cleancss -o ./lnbits/static/bundle.min.css ./lnbits/static/bundle.css",
|
||||||
"vendor_minify_js": "./node_modules/.bin/terser ./lnbits/static/bundle.js -o ./lnbits/static/bundle.min.js --compress --mangle",
|
"vendor_minify_js": "./node_modules/.bin/terser ./lnbits/static/bundle.js -o ./lnbits/static/bundle.min.js --compress --mangle",
|
||||||
"vendor_minify_components": "./node_modules/.bin/terser ./lnbits/static/bundle-components.js -o ./lnbits/static/bundle-components.min.js --compress --mangle",
|
"vendor_minify_components": "./node_modules/.bin/terser ./lnbits/static/bundle-components.js -o ./lnbits/static/bundle-components.min.js --compress --mangle",
|
||||||
"bundle": "npm run sass && npm run vendor_copy && npm run vendor_json && npm run vendor_bundle_css && npm run vendor_bundle_js && npm run vendor_bundle_components && npm run vendor_minify_css && npm run vendor_minify_js && npm run vendor_minify_components"
|
"bundle": "npm run sass && npm run vendor_copy && npm run vendor_json && npm run vendor_bundle_css && npm run vendor_bundle_js && npm run vendor_bundle_components && npm run vendor_minify_css && npm run vendor_minify_js && npm run vendor_minify_components",
|
||||||
|
"test": "npm run test:unit",
|
||||||
|
"test:unit": "vitest --watch=false tests/frontend/unit",
|
||||||
|
"test:unit:watch": "vitest tests/frontend/unit",
|
||||||
|
"build": "vite build",
|
||||||
|
"watch": "vite build --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@quasar/quasar-app-extension-testing-unit-vitest": "^1.2.4",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
|
"@vue/test-utils": "^2.4.6",
|
||||||
"clean-css-cli": "^5.6.3",
|
"clean-css-cli": "^5.6.3",
|
||||||
"concat": "^1.0.3",
|
"concat": "^1.0.3",
|
||||||
|
"jsdom": "^27.3.0",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"pyright": "1.1.289",
|
"pyright": "1.1.289",
|
||||||
"sass": "^1.94.2",
|
"sass": "^1.94.2",
|
||||||
"terser": "^5.44.1"
|
"terser": "^5.44.1",
|
||||||
|
"vitest": "^3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
@@ -98,8 +109,6 @@
|
|||||||
"js/pages/error.js",
|
"js/pages/error.js",
|
||||||
"js/pages/home.js",
|
"js/pages/home.js",
|
||||||
"js/pages/extensions_builder.js",
|
"js/pages/extensions_builder.js",
|
||||||
"js/pages/extensions_builder.js",
|
|
||||||
"js/pages/extensions_builder_preview.js",
|
|
||||||
"js/pages/extensions.js",
|
"js/pages/extensions.js",
|
||||||
"js/pages/first-install.js",
|
"js/pages/first-install.js",
|
||||||
"js/pages/payments.js",
|
"js/pages/payments.js",
|
||||||
@@ -131,7 +140,6 @@
|
|||||||
"js/components/lnbits-wallet-paylinks.js",
|
"js/components/lnbits-wallet-paylinks.js",
|
||||||
"js/components/lnbits-wallet-extra.js",
|
"js/components/lnbits-wallet-extra.js",
|
||||||
"js/components/lnbits-home-logos.js",
|
"js/components/lnbits-home-logos.js",
|
||||||
"js/components/lnbits-error.js",
|
|
||||||
"js/components/lnbits-qrcode.js",
|
"js/components/lnbits-qrcode.js",
|
||||||
"js/components/lnbits-qrcode-lnurl.js",
|
"js/components/lnbits-qrcode-lnurl.js",
|
||||||
"js/components/lnbits-disclaimer.js",
|
"js/components/lnbits-disclaimer.js",
|
||||||
|
|||||||
Generated
+963
-1030
File diff suppressed because it is too large
Load Diff
+60
-40
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.4.1"
|
version = "1.4.0"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||||
@@ -8,31 +8,31 @@ urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbit
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bech32==1.2.0",
|
"bech32==1.2.0",
|
||||||
"click==8.3.1",
|
"click==8.2.1",
|
||||||
"ecdsa==0.19.1",
|
"ecdsa==0.19.1",
|
||||||
"fastapi==0.116.1",
|
"fastapi==0.116.1",
|
||||||
"starlette==0.47.1",
|
"starlette==0.47.1",
|
||||||
"httpx==0.27.2",
|
"httpx==0.27.0",
|
||||||
"jinja2==3.1.6",
|
"jinja2==3.1.6",
|
||||||
"lnurl==0.8.3",
|
"lnurl==0.8.3",
|
||||||
"pydantic==1.10.26",
|
"pydantic==1.10.22",
|
||||||
"pyqrcode==1.2.1",
|
"pyqrcode==1.2.1",
|
||||||
"shortuuid==1.0.13",
|
"shortuuid==1.0.13",
|
||||||
"sse-starlette==2.3.6",
|
"sse-starlette==2.3.6",
|
||||||
"typing-extensions==4.15.0",
|
"typing-extensions==4.14.0",
|
||||||
"uvicorn==0.40.0",
|
"uvicorn==0.34.3",
|
||||||
"sqlalchemy==1.4.54",
|
"sqlalchemy==1.4.54",
|
||||||
"aiosqlite==0.22.1",
|
"aiosqlite==0.21.0",
|
||||||
"asyncpg==0.31.0",
|
"asyncpg==0.30.0",
|
||||||
"uvloop==0.22.1",
|
"uvloop==0.21.0",
|
||||||
"websockets==15.0.1",
|
"websockets==15.0.1",
|
||||||
"loguru==0.7.3",
|
"loguru==0.7.3",
|
||||||
"grpcio==1.76.0",
|
"grpcio==1.69.0",
|
||||||
"protobuf==6.33.2",
|
"protobuf==5.29.5",
|
||||||
"pyln-client==25.12",
|
"pyln-client==25.5",
|
||||||
"pywebpush==2.2.0",
|
"pywebpush==2.0.3",
|
||||||
"slowapi==0.1.9",
|
"slowapi==0.1.9",
|
||||||
"websocket-client==1.9.0",
|
"websocket-client==1.8.0",
|
||||||
"pycryptodomex==3.23.0",
|
"pycryptodomex==3.23.0",
|
||||||
"packaging==25.0",
|
"packaging==25.0",
|
||||||
"bolt11==2.1.1",
|
"bolt11==2.1.1",
|
||||||
@@ -42,16 +42,15 @@ dependencies = [
|
|||||||
# needed for boltz, lnurldevice, watchonly extensions
|
# needed for boltz, lnurldevice, watchonly extensions
|
||||||
"embit==0.8.0",
|
"embit==0.8.0",
|
||||||
# needed for scheduler extension
|
# needed for scheduler extension
|
||||||
"python-crontab==3.3.0",
|
"python-crontab==3.2.0",
|
||||||
"pynostr==0.7.0",
|
"pynostr==0.6.2",
|
||||||
"python-multipart==0.0.21",
|
"python-multipart==0.0.20",
|
||||||
"filetype==1.2.0",
|
"filetype==1.2.0",
|
||||||
"nostr-sdk==0.44.0",
|
"nostr-sdk==0.42.1",
|
||||||
"bcrypt==5.0.0",
|
"bcrypt==4.3.0",
|
||||||
"jsonpath-ng==1.7.0",
|
"jsonpath-ng==1.7.0",
|
||||||
"pillow>=12.1.0",
|
"pillow>=12.0.0",
|
||||||
"python-dotenv>=1.2.1",
|
"python-dotenv>=1.2.1",
|
||||||
"greenlet (>=3.3.0,<4.0.0)",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -59,31 +58,31 @@ lnbits = "lnbits.server:main"
|
|||||||
lnbits-cli = "lnbits.commands:main"
|
lnbits-cli = "lnbits.commands:main"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
breez = ["breez-sdk==0.8.0", "breez-sdk-liquid==0.11.11"]
|
breez = ["breez-sdk==0.8.0", "breez-sdk-liquid==0.9.1"]
|
||||||
liquid = ["wallycore==1.5.1"]
|
liquid = ["wallycore==1.4.0"]
|
||||||
migration = ["psycopg2-binary==2.9.11"]
|
migration = ["psycopg2-binary==2.9.10"]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"black>=25.12.0,<26.0.0",
|
"black>=25.1.0,<26.0.0",
|
||||||
"mypy==1.17.1",
|
"mypy==1.17.1",
|
||||||
"types-protobuf>=6.32.1.20251210,<7.0.0",
|
"types-protobuf>=6.30.2.20250516,<7.0.0",
|
||||||
"pre-commit>=4.5.1,<5.0.0",
|
"pre-commit>=4.2.0,<5.0.0",
|
||||||
"openapi-spec-validator>=0.7.2,<1.0.0",
|
"openapi-spec-validator>=0.7.1,<1.0.0",
|
||||||
"ruff>=0.14.10,<1.0.0",
|
"ruff>=0.12.0,<1.0.0",
|
||||||
"types-passlib>=1.7.7.20250602,<2.0.0",
|
"types-passlib>=1.7.7.20240327,<2.0.0",
|
||||||
"openai>=2.14.0",
|
"openai>=1.39.0,<2.0.0",
|
||||||
"json5>=0.13.0,<1.0.0",
|
"json5>=0.12.0,<1.0.0",
|
||||||
"asgi-lifespan>=2.1.0,<3.0.0",
|
"asgi-lifespan>=2.1.0,<3.0.0",
|
||||||
"anyio>=4.12.1",
|
"anyio>=4.7.0,<5.0.0",
|
||||||
"pytest>=9.0.2",
|
"pytest>=8.3.4,<9.0.0",
|
||||||
"pytest-cov>=7.0.0",
|
"pytest-cov>=6.0.0,<7.0.0",
|
||||||
"pytest-md>=0.2.0,<0.3.0",
|
"pytest-md>=0.2.0,<0.3.0",
|
||||||
"pytest-httpserver>=1.1.3,<2.0.0",
|
"pytest-httpserver>=1.1.0,<2.0.0",
|
||||||
"pytest-mock>=3.15.1,<4.0.0",
|
"pytest-mock>=3.14.0,<4.0.0",
|
||||||
"types-mock>=5.2.0.20250924,<6.0.0",
|
"types-mock>=5.1.0.20240425,<6.0.0",
|
||||||
"mock>=5.2.0,<6.0.0",
|
"mock>=5.1.0,<6.0.0",
|
||||||
"grpcio-tools>=1.76.0,<2.0.0"
|
"grpcio-tools>=1.69.0,<2.0.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
@@ -92,6 +91,27 @@ packages = [
|
|||||||
{include = "lnbits/py.typed"},
|
{include = "lnbits/py.typed"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
black = "^25.1.0"
|
||||||
|
mypy = "^1.17.1"
|
||||||
|
types-protobuf = "^6.30.2.20250516"
|
||||||
|
pre-commit = "^4.2.0"
|
||||||
|
openapi-spec-validator = "^0.7.1"
|
||||||
|
ruff = "^0.12.0"
|
||||||
|
types-passlib = "^1.7.7.20240327"
|
||||||
|
openai = "^1.39.0"
|
||||||
|
json5 = "^0.12.0"
|
||||||
|
asgi-lifespan = "^2.1.0"
|
||||||
|
anyio = "^4.7.0"
|
||||||
|
pytest = "^8.3.4"
|
||||||
|
pytest-cov = "^6.0.0"
|
||||||
|
pytest-md = "^0.2.0"
|
||||||
|
pytest-httpserver = "^1.1.0"
|
||||||
|
pytest-mock = "^3.14.0"
|
||||||
|
types-mock = "^5.1.0.20240425"
|
||||||
|
mock = "^5.1.0"
|
||||||
|
grpcio-tools = "^1.69.0"
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
include = [
|
include = [
|
||||||
"lnbits",
|
"lnbits",
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ async def test_create_invoice_validates_used_currency(
|
|||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
res_data = response.json()
|
res_data = response.json()
|
||||||
assert "The provided unit is not supported" in res_data["detail"][0]["msg"]
|
assert "The provided unit is not supported" in res_data["detail"]
|
||||||
|
|
||||||
|
|
||||||
# check POST /api/v1/payments: invoice creation for internal payments only
|
# check POST /api/v1/payments: invoice creation for internal payments only
|
||||||
@@ -823,7 +823,7 @@ async def test_api_payments_pay_lnurl(client, adminkey_headers_from):
|
|||||||
"/api/v1/payments/lnurl", json=lnurl_data, headers=adminkey_headers_from
|
"/api/v1/payments/lnurl", json=lnurl_data, headers=adminkey_headers_from
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert "invalid or missing URL scheme" in response.json()["detail"][0]["msg"]
|
assert "value_error.url.scheme" in response.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
################################ Labels ################################
|
################################ Labels ################################
|
||||||
|
|||||||
+1
-192
@@ -2044,196 +2044,5 @@ async def test_api_update_user_labels(http_client: AsyncClient):
|
|||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert (
|
assert (
|
||||||
"""string does not match regex "([A-Za-z0-9 ._-]{1,100}$)"""
|
"""string does not match regex "([A-Za-z0-9 ._-]{1,100}$)""" in data["detail"]
|
||||||
in data["detail"][0]["msg"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_impersonate_user_success(http_client: AsyncClient, admin_user: User):
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
user_id = uuid4().hex
|
|
||||||
account = Account(
|
|
||||||
id=user_id,
|
|
||||||
username=f"u_{tiny_id}",
|
|
||||||
email=f"u_{tiny_id}@lnbits.com",
|
|
||||||
)
|
|
||||||
account.hash_password("secret1234")
|
|
||||||
await create_user_account(account)
|
|
||||||
|
|
||||||
# Login as admin to get access token
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": admin_user.username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
admin_token = response.json()["access_token"]
|
|
||||||
|
|
||||||
# Impersonate the user
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/impersonate",
|
|
||||||
json={"usr": user_id},
|
|
||||||
headers={"Authorization": f"Bearer {admin_token}"},
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
data = response.json()
|
|
||||||
assert "access_token" in data
|
|
||||||
assert data["token_type"] == "bearer"
|
|
||||||
|
|
||||||
# Check impersonation cookies
|
|
||||||
assert "cookie_access_token" in response.cookies
|
|
||||||
assert "admin_access_token" in response.cookies
|
|
||||||
assert response.cookies.get("is_lnbits_user_impersonated") == "true"
|
|
||||||
|
|
||||||
response = await http_client.delete("/api/v1/auth/impersonate")
|
|
||||||
assert "cookie_access_token" in response.cookies
|
|
||||||
assert response.cookies.get("cookie_access_token") == admin_token
|
|
||||||
assert "admin_access_token" not in response.cookies
|
|
||||||
assert "is_lnbits_user_impersonated" not in response.cookies
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_impersonate_user_no_cookie(http_client: AsyncClient, admin_user: User):
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": admin_user.username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
admin_token = response.json()["access_token"]
|
|
||||||
user_id = uuid4().hex
|
|
||||||
http_client.cookies.clear()
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/impersonate",
|
|
||||||
json={"usr": user_id},
|
|
||||||
headers={"Authorization": f"Bearer {admin_token}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["detail"] == "Only cookie based impersonation is allowed."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_impersonate_user_self(http_client: AsyncClient, admin_user: User):
|
|
||||||
# Admin tries to impersonate themselves
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": admin_user.username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
admin_token = response.json()["access_token"]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/impersonate",
|
|
||||||
json={"usr": admin_user.id},
|
|
||||||
headers={"Authorization": f"Bearer {admin_token}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json()["detail"] == "You cannot impersonate yourself."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_impersonate_user_admin_target(
|
|
||||||
http_client: AsyncClient, admin_user: User, settings: Settings
|
|
||||||
):
|
|
||||||
other_admin_id = uuid4().hex
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
account = Account(
|
|
||||||
id=other_admin_id, username=f"u_{tiny_id}", email=f"u_{tiny_id}@lnbits.com"
|
|
||||||
)
|
|
||||||
account.hash_password("secret1234")
|
|
||||||
|
|
||||||
await create_user_account(account)
|
|
||||||
settings.lnbits_admin_users.append(other_admin_id)
|
|
||||||
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": admin_user.username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
admin_token = response.json()["access_token"]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/impersonate",
|
|
||||||
json={"usr": other_admin_id},
|
|
||||||
headers={"Authorization": f"Bearer {admin_token}"},
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json()["detail"] == "You cannot impersonate another admin user."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_impersonate_user_nonexistent(
|
|
||||||
http_client: AsyncClient, admin_user: User, settings: Settings
|
|
||||||
):
|
|
||||||
http_client.cookies.clear()
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": admin_user.username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
admin_token = response.json()["access_token"]
|
|
||||||
fake_id = "deadbeef"
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/impersonate",
|
|
||||||
json={"usr": fake_id},
|
|
||||||
headers={"Authorization": f"Bearer {admin_token}"},
|
|
||||||
)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["detail"] == "User ID does not exist."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_impersonate_user_invalid_data(
|
|
||||||
http_client: AsyncClient, admin_user: User
|
|
||||||
):
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": admin_user.username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
admin_token = response.json()["access_token"]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/impersonate",
|
|
||||||
json={"usr": None},
|
|
||||||
headers={"Authorization": f"Bearer {admin_token}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert "type_error.none.not_allowed" in str(response.json()["detail"])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_impersonate_user_missing_usr_field(
|
|
||||||
http_client: AsyncClient, admin_user: User
|
|
||||||
):
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": admin_user.username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
admin_token = response.json()["access_token"]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/impersonate",
|
|
||||||
json={},
|
|
||||||
headers={"Authorization": f"Bearer {admin_token}"},
|
|
||||||
)
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert "value_error.missing" in str(response.json()["detail"])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_impersonate_user_by_non_admin(http_client: AsyncClient, user_alan: User):
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": user_alan.username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
alan_token = response.json()["access_token"]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/impersonate",
|
|
||||||
json={},
|
|
||||||
headers={"Authorization": f"Bearer {alan_token}"},
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json()["detail"] == "User not authorized. No admin privileges."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_stop_impersonate_user_by_non_admin(
|
|
||||||
http_client: AsyncClient, user_alan: User
|
|
||||||
):
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": user_alan.username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await http_client.delete("/api/v1/auth/impersonate")
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert (
|
|
||||||
response.json()["detail"]
|
|
||||||
== "No admin access token found to stop impersonation."
|
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user