Compare commits

..
Author SHA1 Message Date
dni ⚡andGitHub 83334d67c8 HOTFIX: websocket with old balance was sent. (#3758) 2026-02-03 12:12:33 +01:00
107 changed files with 1936 additions and 2715 deletions
-4
View File
@@ -6,10 +6,6 @@
# The following settings are ONLY set in your .env file. # The following settings are ONLY set in your .env file.
# They are NOT managed by the Admin UI and are not stored in the database. # They are NOT managed by the Admin UI and are not stored in the database.
# === First Install Token ===
# if set the user is required to enter this token on the /first_install page
# FIRST_INSTALL_TOKEN="myaccesstoken"
# === Logging and Development === # === Logging and Development ===
DEBUG=False DEBUG=False
+7 -11
View File
@@ -44,19 +44,15 @@ jobs:
if: github.repository == 'lnbits/lnbits' if: github.repository == 'lnbits/lnbits'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Install dependencies for building secp256k1
run: |
sudo apt-get update
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.10 - name: Build and publish to pypi
uses: actions/setup-python@v5 uses: JRubics/poetry-publish@v1.15
with: with:
python-version: "3.10" pypi_token: ${{ secrets.PYPI_API_KEY }}
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Build the project
run: uv build
- name: Publish to pypi
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
run: uv publish
appimage: appimage:
needs: [ release ] needs: [ release ]
+7 -11
View File
@@ -56,19 +56,15 @@ jobs:
if: github.repository == 'lnbits/lnbits' if: github.repository == 'lnbits/lnbits'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Install dependencies for building secp256k1
run: |
sudo apt-get update
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.10 - name: Build and publish to pypi
uses: actions/setup-python@v5 uses: JRubics/poetry-publish@v1.15
with: with:
python-version: "3.10" pypi_token: ${{ secrets.PYPI_API_KEY }}
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Build the project
run: uv build
- name: Publish to pypi
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
run: uv publish
appimage: appimage:
needs: [ release ] needs: [ release ]
-1
View File
@@ -204,7 +204,6 @@ async def get_user_from_account(
super_user=account.is_super_user, super_user=account.is_super_user,
fiat_providers=account.fiat_providers, fiat_providers=account.fiat_providers,
has_password=account.password_hash is not None, has_password=account.password_hash is not None,
ui_customization=account.ui_customization or {},
) )
-8
View File
@@ -848,11 +848,3 @@ async def m042_index_accounts(db: Connection):
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}"); CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
""" """
) )
async def m043_add_ui_customization_to_accounts(db: Connection):
"""
Adds ui_customization column to accounts.
Used for server side persistence of UI customization settings.
"""
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
+1 -21
View File
@@ -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 []
-25
View File
@@ -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"
+53 -10
View File
@@ -6,16 +6,23 @@ from typing import Literal
from fastapi import Query from fastapi import Query
from lnurl import LnurlWithdrawResponse from lnurl import LnurlWithdrawResponse
from loguru import logger
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
from lnbits.db import FilterModel from lnbits.db import FilterModel
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import ( from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus, FiatPaymentStatus,
FiatPaymentSuccessStatus,
) )
from lnbits.utils.exchange_rates import allowed_currencies from lnbits.utils.exchange_rates import allowed_currencies
from lnbits.wallets import get_funding_source
from lnbits.wallets.base import ( from lnbits.wallets.base import (
PaymentFailedStatus,
PaymentPendingStatus,
PaymentStatus, PaymentStatus,
PaymentSuccessStatus,
) )
@@ -123,23 +130,59 @@ class Payment(BaseModel):
"fiat_" "fiat_"
) )
# DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_status( async def check_status(
self, skip_internal_payment_notifications: bool | None = False self, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus: ) -> PaymentStatus:
logger.warning("payment.check_status() is deprecated.") if self.is_internal:
from lnbits.core.services.payments import check_payment_status if self.success:
return PaymentSuccessStatus()
if self.failed:
return PaymentFailedStatus()
if self.is_in and self.fiat_provider:
fiat_status = await self.check_fiat_status(
skip_internal_payment_notifications
)
return PaymentStatus(paid=fiat_status.paid)
return PaymentPendingStatus()
funding_source = get_funding_source()
if self.is_out:
status = await funding_source.get_payment_status(self.checking_id)
else:
status = await funding_source.get_invoice_status(self.checking_id)
return status
return await check_payment_status(self, skip_internal_payment_notifications)
# DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_fiat_status( async def check_fiat_status(
self, skip_internal_payment_notifications: bool | None = False self, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus: ) -> FiatPaymentStatus:
logger.warning("payment.check_fiat_status() is deprecated.") if not self.is_internal:
from lnbits.core.services.fiat_providers import check_fiat_status return FiatPaymentPendingStatus()
if self.success:
return FiatPaymentSuccessStatus()
if self.failed:
return FiatPaymentFailedStatus()
return await check_fiat_status(self, skip_internal_payment_notifications) if not self.fiat_provider:
return FiatPaymentPendingStatus()
checking_id = self.extra.get("fiat_checking_id")
if not checking_id:
return FiatPaymentPendingStatus()
fiat_provider = await get_fiat_provider(self.fiat_provider)
if not fiat_provider:
return FiatPaymentPendingStatus()
fiat_status = await fiat_provider.get_invoice_status(checking_id)
if skip_internal_payment_notifications:
return fiat_status
if fiat_status.success:
# notify receivers asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(self.checking_id)
return fiat_status
class PaymentFilters(FilterModel): class PaymentFilters(FilterModel):
-3
View File
@@ -187,7 +187,6 @@ class Account(AccountId):
pubkey: str | None = None pubkey: str | None = None
email: str | None = None email: str | None = None
extra: UserExtra = UserExtra() extra: UserExtra = UserExtra()
ui_customization: dict = Field(default_factory=dict)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@@ -289,7 +288,6 @@ class User(BaseModel):
fiat_providers: list[str] = [] fiat_providers: list[str] = []
has_password: bool = False has_password: bool = False
extra: UserExtra = UserExtra() extra: UserExtra = UserExtra()
ui_customization: dict = Field(default_factory=dict)
@property @property
def wallet_ids(self) -> list[str]: def wallet_ids(self) -> list[str]:
@@ -358,7 +356,6 @@ class UpdateSuperuserPassword(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20) username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50) password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50) password_repeat: str = Query(default=..., min_length=8, max_length=50)
first_install_token: str | None = Query(None)
class LoginUsr(BaseModel): class LoginUsr(BaseModel):
-4
View File
@@ -1,4 +1,3 @@
from .fiat_providers import check_fiat_status
from .funding_source import ( from .funding_source import (
get_balance_delta, get_balance_delta,
switch_to_voidwallet, switch_to_voidwallet,
@@ -8,7 +7,6 @@ from .notifications import enqueue_admin_notification, send_payment_notification
from .payments import ( from .payments import (
calculate_fiat_amounts, calculate_fiat_amounts,
cancel_hold_invoice, cancel_hold_invoice,
check_payment_status,
check_transaction_status, check_transaction_status,
check_wallet_limits, check_wallet_limits,
create_fiat_invoice, create_fiat_invoice,
@@ -42,8 +40,6 @@ __all__ = [
"calculate_fiat_amounts", "calculate_fiat_amounts",
"cancel_hold_invoice", "cancel_hold_invoice",
"check_admin_settings", "check_admin_settings",
"check_fiat_status",
"check_payment_status",
"check_transaction_status", "check_transaction_status",
"check_wallet_limits", "check_wallet_limits",
"check_webpush_settings", "check_webpush_settings",
+12 -26
View File
@@ -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
+11 -44
View File
@@ -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
-40
View File
@@ -12,12 +12,6 @@ from lnbits.core.models import CreatePayment, Payment, PaymentState
from lnbits.core.models.misc import SimpleStatus from lnbits.core.models.misc import SimpleStatus
from lnbits.db import Connection from lnbits.db import Connection
from lnbits.fiat import get_fiat_provider from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
)
from lnbits.settings import settings from lnbits.settings import settings
@@ -35,40 +29,6 @@ async def handle_fiat_payment_confirmation(
logger.warning(e) logger.warning(e)
async def check_fiat_status(
payment: Payment, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
if not payment.is_internal:
return FiatPaymentPendingStatus()
if payment.success:
return FiatPaymentSuccessStatus()
if payment.failed:
return FiatPaymentFailedStatus()
if not payment.fiat_provider:
return FiatPaymentPendingStatus()
checking_id = payment.extra.get("fiat_checking_id")
if not checking_id:
return FiatPaymentPendingStatus()
fiat_provider = await get_fiat_provider(payment.fiat_provider)
if not fiat_provider:
return FiatPaymentPendingStatus()
fiat_status = await fiat_provider.get_invoice_status(checking_id)
if skip_internal_payment_notifications:
return fiat_status
if fiat_status.success:
# notify receivers asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(payment.checking_id)
return fiat_status
def check_stripe_signature( def check_stripe_signature(
payload: bytes, payload: bytes,
sig_header: str | None, sig_header: str | None,
+3 -27
View File
@@ -25,7 +25,6 @@ from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount
from lnbits.wallets import fake_wallet, get_funding_source from lnbits.wallets import fake_wallet, get_funding_source
from lnbits.wallets.base import ( from lnbits.wallets.base import (
InvoiceResponse, InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus, PaymentPendingStatus,
PaymentResponse, PaymentResponse,
PaymentStatus, PaymentStatus,
@@ -48,7 +47,6 @@ from ..models import (
PaymentState, PaymentState,
Wallet, Wallet,
) )
from .fiat_providers import check_fiat_status
from .notifications import send_payment_notification_in_background from .notifications import send_payment_notification_in_background
payment_lock = asyncio.Lock() payment_lock = asyncio.Lock()
@@ -366,7 +364,7 @@ async def update_pending_payments(wallet_id: str):
async def update_pending_payment( async def update_pending_payment(
payment: Payment, conn: Connection | None = None payment: Payment, conn: Connection | None = None
) -> Payment: ) -> Payment:
status = await check_payment_status(payment) status = await payment.check_status()
if status.failed: if status.failed:
payment.status = PaymentState.FAILED payment.status = PaymentState.FAILED
await update_payment(payment, conn=conn) await update_payment(payment, conn=conn)
@@ -620,29 +618,7 @@ async def check_transaction_status(
if payment.status == PaymentState.SUCCESS.value: if payment.status == PaymentState.SUCCESS.value:
return PaymentSuccessStatus(fee_msat=payment.fee) return PaymentSuccessStatus(fee_msat=payment.fee)
return await check_payment_status(payment) return await payment.check_status()
async def check_payment_status(
payment: Payment, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
if payment.is_internal:
if payment.success:
return PaymentSuccessStatus()
if payment.failed:
return PaymentFailedStatus()
if payment.is_in and payment.fiat_provider:
fiat_status = await check_fiat_status(
payment, skip_internal_payment_notifications
)
return PaymentStatus(paid=fiat_status.paid)
return PaymentPendingStatus()
funding_source = get_funding_source()
if payment.is_out:
status = await funding_source.get_payment_status(payment.checking_id)
else:
status = await funding_source.get_invoice_status(payment.checking_id)
return status
async def get_payments_daily_stats( async def get_payments_daily_stats(
@@ -893,7 +869,7 @@ async def _verify_external_payment(
raise PaymentError("Payment already paid.", status="success") raise PaymentError("Payment already paid.", status="success")
# payment failed # payment failed
status = await check_payment_status(payment) status = await payment.check_status()
if status.failed: if status.failed:
raise PaymentError( raise PaymentError(
"Payment is failed node, retrying is not possible.", status="failed" "Payment is failed node, retrying is not possible.", status="failed"
+4 -4
View File
@@ -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:
+2 -83
View File
@@ -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,
@@ -467,7 +408,7 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
return _auth_success_response(account.username, user_id, account.email) return _auth_success_response(account.username, user_id, account.email)
@auth_router.patch("") @auth_router.put("/update")
async def update( async def update(
data: UpdateUser, account: Account = Depends(check_account_exists) data: UpdateUser, account: Account = Depends(check_account_exists)
) -> User | None: ) -> User | None:
@@ -483,32 +424,10 @@ async def update(
return await get_user_from_account(account) return await get_user_from_account(account)
@auth_router.patch("/ui")
async def update_ui_customization(
req: Request, account: Account = Depends(check_account_exists)
) -> Account:
ui_customization = await req.json()
account.ui_customization = {**(account.ui_customization or {}), **ui_customization}
if len(account.ui_customization or {}) > 1000 * 1024:
raise HTTPException(
HTTPStatus.BAD_REQUEST, "UI customization too large. Drop some fields."
)
await update_user_account(account)
return account
@auth_router.put("/first_install") @auth_router.put("/first_install")
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse: async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
if not settings.first_install: if not settings.first_install:
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install") raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
if settings.first_install_token:
if not data.first_install_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.")
if settings.first_install_token != data.first_install_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid first_install_token.")
account = await get_account(settings.super_user) account = await get_account(settings.super_user)
if not account: if not account:
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.") raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
+5 -6
View File
@@ -9,7 +9,6 @@ from lnbits.core.crud.payments import (
from lnbits.core.models.misc import SimpleStatus from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.payments import CreateInvoice from lnbits.core.models.payments import CreateInvoice
from lnbits.core.services.fiat_providers import ( from lnbits.core.services.fiat_providers import (
check_fiat_status,
check_stripe_signature, check_stripe_signature,
verify_paypal_webhook, verify_paypal_webhook,
) )
@@ -88,7 +87,7 @@ async def _handle_stripe_intent_session_completed(event: dict):
if not payment: if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.") logger.warning(f"No payment found for hash: '{payment_hash}'.")
return return
await check_fiat_status(payment) await payment.check_fiat_status()
async def _handle_stripe_checkout_session_completed(event: dict): async def _handle_stripe_checkout_session_completed(event: dict):
@@ -111,7 +110,7 @@ async def _handle_stripe_checkout_session_completed(event: dict):
payment = await get_standalone_payment(payment_hash) payment = await get_standalone_payment(payment_hash)
if not payment: if not payment:
raise ValueError(f"No payment found for hash: '{payment_hash}'.") raise ValueError(f"No payment found for hash: '{payment_hash}'.")
await check_fiat_status(payment) await payment.check_fiat_status()
async def _handle_stripe_subscription_invoice_paid(event: dict): async def _handle_stripe_subscription_invoice_paid(event: dict):
@@ -156,7 +155,7 @@ async def _handle_stripe_subscription_invoice_paid(event: dict):
), ),
) )
await check_fiat_status(payment) await payment.check_fiat_status()
async def _get_stripe_subscription_payment_options( async def _get_stripe_subscription_payment_options(
@@ -193,7 +192,7 @@ async def handle_paypal_event(event: dict):
if not payment: if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.") logger.warning(f"No payment found for hash: '{payment_hash}'.")
return return
await check_fiat_status(payment) await payment.check_fiat_status()
return return
if event_type in ( if event_type in (
@@ -248,7 +247,7 @@ async def _handle_paypal_subscription_payment(resource: dict):
), ),
) )
await check_fiat_status(payment) await payment.check_fiat_status()
def _paypal_extract_payment_hash(resource: dict) -> str | None: def _paypal_extract_payment_hash(resource: dict) -> str | None:
+30 -17
View File
@@ -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:
+2 -7
View File
@@ -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)
+1 -1
View File
@@ -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)
+51 -16
View File
@@ -70,11 +70,57 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
t.env.globals["static_url_for"] = static_url_for t.env.globals["static_url_for"] = static_url_for
t.env.globals["normalize_path"] = normalize_path t.env.globals["normalize_path"] = normalize_path
# used in base.html window_settings = {
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title "AD_SPACE": settings.lnbits_ad_space,
t.env.globals["LNBITS_APPLE_TOUCH_ICON"] = settings.lnbits_apple_touch_icon "AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
t.env.globals["SETTINGS"] = settings.to_public().dict(by_alias=True) "AD_SPACE_TITLE": settings.lnbits_ad_space_title,
t.env.globals["CURRENCIES"] = list(currencies.keys()) "EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
"HIDE_API": settings.lnbits_hide_api,
"SITE_TITLE": settings.lnbits_site_title,
"SITE_TAGLINE": settings.lnbits_site_tagline,
"SITE_DESCRIPTION": settings.lnbits_site_description,
"LNBITS_ADMIN_UI": settings.lnbits_admin_ui,
"LNBITS_AUDIT_ENABLED": settings.lnbits_audit_enabled,
"LNBITS_AUTH_METHODS": settings.auth_allowed_methods,
"LNBITS_AUTH_KEYCLOAK_ORG": settings.keycloak_client_custom_org,
"LNBITS_AUTH_KEYCLOAK_ICON": settings.keycloak_client_custom_icon,
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
"LNBITS_APPLE_TOUCH_ICON": settings.lnbits_apple_touch_icon,
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
"LNBITS_VERSION": settings.version,
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
"LNBITS_EXTENSIONS_REVIEWS_URL": settings.lnbits_extensions_reviews_url,
"LNBITS_DENOMINATION": settings.lnbits_denomination,
"has_holdinvoice": settings.has_holdinvoice,
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
"LNBITS_CURRENCIES": list(currencies.keys()),
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
"CACHE_KEY": settings.server_startup_time,
}
t.env.globals["WINDOW_SETTINGS"] = window_settings
for key, value in window_settings.items():
t.env.globals[key] = value
if settings.bundle_assets: if settings.bundle_assets:
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"] t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
@@ -88,9 +134,6 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
t.env.globals["INCLUDED_CSS"] = vendor_files["css"] t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"] t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
# backwards compatibility for extensions (tpos)
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
return t return t
@@ -348,14 +391,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
-119
View File
@@ -252,11 +252,6 @@ class ThemesSettings(LNbitsSettings):
) )
lnbits_show_home_page_elements: bool = Field(default=True) lnbits_show_home_page_elements: bool = Field(default=True)
lnbits_default_wallet_name: str = Field(default="LNbits wallet") lnbits_default_wallet_name: str = Field(default="LNbits wallet")
lnbits_wallet_featured_button_label: str | None = Field(default=None)
lnbits_wallet_featured_button_url: str | None = Field(default=None)
lnbits_wallet_featured_button_icon: str | None = Field(default=None)
lnbits_custom_badge: str | None = Field(default=None) lnbits_custom_badge: str | None = Field(default=None)
lnbits_custom_badge_color: str = Field(default="warning") lnbits_custom_badge_color: str = Field(default="warning")
lnbits_theme_options: list[str] = Field( lnbits_theme_options: list[str] = Field(
@@ -311,10 +306,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)
@@ -983,7 +974,6 @@ class EnvSettings(LNbitsSettings):
enable_log_to_file: bool = Field(default=True) enable_log_to_file: bool = Field(default=True)
log_rotation: str = Field(default="100 MB") log_rotation: str = Field(default="100 MB")
log_retention: str = Field(default="3 months") log_retention: str = Field(default="3 months")
first_install_token: str | None = Field(default=None)
cleanup_wallets_days: int = Field(default=90, ge=0) cleanup_wallets_days: int = Field(default=90, ge=0)
funding_source_max_retries: int = Field(default=4, ge=0) funding_source_max_retries: int = Field(default=4, ge=0)
@@ -1132,115 +1122,6 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
and ext_id in self.lnbits_all_extensions_ids and ext_id in self.lnbits_all_extensions_ids
) )
def to_public(self) -> PublicSettings:
return PublicSettings.from_settings(self)
class PublicSettings(BaseModel):
"""
PublicSettings is used for templating and public information.
WARNING: do not expose private information, PublicSettings is exposed publicly.
"""
allow_register: bool = Field(alias="allowRegister")
qr_logo: str = Field(alias="qrLogo")
site_title: str = Field(alias="siteTitle")
site_tagline: str = Field(alias="siteTagline")
site_description: str | None = Field(alias="siteDescription")
auth_methods: list[str] = Field(alias="authMethods")
keycloak_org: str | None = Field(alias="keycloakOrg")
keycloak_icon: str | None = Field(alias="keycloakIcon")
has_holdinvoice: bool = Field(alias="hasHoldinvoice")
has_nodemanager: bool = Field(alias="hasNodemanager")
show_nodemanager: bool = Field(alias="showNodemanager")
custom_logo: str | None = Field(alias="customLogo")
show_voidwallet: bool = Field(alias="showVoidwallet")
version: str = Field(alias="version")
show_home_page_elements: bool = Field(alias="showHomePageElements")
hide_api: bool = Field(alias="hideApi")
cache_key: str = Field(alias="cacheKey")
webpush_pubkey: str | None = Field(alias="webpushPubkey")
show_extensions: bool = Field(alias="showExtensions")
show_audit: bool = Field(alias="showAudit")
show_admin: bool = Field(alias="showAdmin")
ad_space: list[list[str]] = Field(alias="adSpace")
ad_space_title: str = Field(alias="adSpaceTitle")
show_ad_space: bool = Field(alias="showAdSpace")
custom_image: str | None = Field(alias="customImage")
custom_badge: str | None = Field(alias="customBadge")
custom_badge_color: str | None = Field(alias="customBadgeColor")
service_fee: float = Field(alias="serviceFee")
service_fee_max: int = Field(alias="serviceFeeMax")
service_fee_wallet: str | None = Field(alias="serviceFeeWallet")
theme_options: list[str] = Field(alias="themeOptions")
default_reaction: str = Field(alias="defaultReaction")
default_theme: str = Field(alias="defaultTheme")
default_border: str = Field(alias="defaultBorder")
default_bgimage: str | None = Field(alias="defaultBgimage")
default_gradient: bool = Field(alias="defaultGradient")
denomination: str | None = Field()
extensions: list[str] = Field()
allowed_currencies: list[str] = Field(alias="allowedCurrencies")
extensions_reviews_url: str = Field(alias="extensionsReviewsUrl")
ext_builder: bool = Field(alias="extBuilder")
nostr_configured: bool = Field(alias="nostrConfigured")
telegram_configured: bool = Field(alias="telegramConfigured")
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
wallet_featured_button_icon: str | None = Field(alias="walletFeaturedButtonIcon")
@classmethod
def from_settings(cls, settings: Settings):
ads = [x.split(";") for x in settings.lnbits_ad_space.split(",")]
return cls(
adSpace=ads,
adSpaceTitle=settings.lnbits_ad_space_title,
showAdSpace=settings.lnbits_ad_space_enabled and len(ads) > 0,
allowRegister=settings.new_accounts_allowed,
qrLogo=settings.lnbits_qr_logo,
siteDescription=settings.lnbits_site_description,
siteTitle=settings.lnbits_site_title,
siteTagline=settings.lnbits_site_tagline,
authMethods=settings.auth_allowed_methods,
keycloakOrg=settings.keycloak_client_custom_org,
keycloakIcon=settings.keycloak_client_custom_icon,
hasHoldinvoice=settings.has_holdinvoice,
hasNodemanager=settings.has_nodemanager,
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
customLogo=settings.lnbits_custom_logo,
showVoidwallet=settings.lnbits_backend_wallet_class == "VoidWallet",
version=settings.version,
showHomePageElements=settings.lnbits_show_home_page_elements,
hideApi=settings.lnbits_hide_api,
cacheKey=str(settings.server_startup_time),
webpushPubkey=settings.lnbits_webpush_pubkey,
showExtensions=not settings.lnbits_extensions_deactivate_all,
showAudit=settings.lnbits_audit_enabled,
showAdmin=settings.lnbits_admin_ui,
customImage=settings.lnbits_custom_image,
customBadge=settings.lnbits_custom_badge,
customBadgeColor=settings.lnbits_custom_badge_color,
serviceFee=settings.lnbits_service_fee,
serviceFeeMax=settings.lnbits_service_fee_max,
serviceFeeWallet=settings.lnbits_service_fee_wallet,
themeOptions=settings.lnbits_theme_options,
defaultReaction=settings.lnbits_default_reaction,
defaultTheme=settings.lnbits_default_theme,
defaultBorder=settings.lnbits_default_border,
defaultGradient=settings.lnbits_default_gradient,
defaultBgimage=settings.lnbits_default_bgimage,
denomination=settings.lnbits_denomination,
extensions=list(settings.lnbits_installed_extensions_ids),
allowedCurrencies=settings.lnbits_allowed_currencies,
extensionsReviewsUrl=settings.lnbits_extensions_reviews_url,
extBuilder=settings.lnbits_extensions_builder_activate_non_admins,
nostrConfigured=settings.is_nostr_notifications_configured(),
telegramConfigured=settings.is_telegram_notifications_configured(),
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
)
class SuperSettings(EditableSettings): class SuperSettings(EditableSettings):
super_user: str super_user: str
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
-12
View File
@@ -518,7 +518,6 @@ window.localisation.en = {
hash: 'Hash: ', hash: 'Hash: ',
welcome_lnbits: 'Welcome to LNbits', welcome_lnbits: 'Welcome to LNbits',
setup_su_account: 'Set up the Superuser account below.', setup_su_account: 'Set up the Superuser account below.',
first_install_token: 'First Install Token (optional)',
create_ticker_converter: 'Create Currency Ticker Converter', create_ticker_converter: 'Create Currency Ticker Converter',
enable_audit: 'Enable Audit', enable_audit: 'Enable Audit',
recommended: 'Recommended', recommended: 'Recommended',
@@ -659,15 +658,6 @@ window.localisation.en = {
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML', ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
ui_default_wallet_name: 'Default Wallet Name', ui_default_wallet_name: 'Default Wallet Name',
ui_default_theme: 'Default Theme', ui_default_theme: 'Default Theme',
wallet_featured_button_label: 'Wallet Featured Button Label',
wallet_featured_button_label_hint:
'Show featured button on the wallet homepage',
wallet_featured_button_url: 'Featured Button URL',
wallet_featured_button_url_hint:
'On click the button will open this URL. Leave empty to hide the button.',
wallet_featured_button_icon: 'Featured Button Icon',
wallet_featured_button_icon_hint:
'Icon shown on the featured button (check quasar icons)',
lnbits_wallet: 'LNbits wallet', lnbits_wallet: 'LNbits wallet',
denomination: 'Denomination', denomination: 'Denomination',
denomination_hint: 'The name for the FakeWallet token', denomination_hint: 'The name for the FakeWallet token',
@@ -705,8 +695,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',
-16
View File
@@ -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')
}, },
@@ -154,9 +141,6 @@ window._lnbitsApi = {
name: name name: name
}) })
}, },
updateUiCustomization(data = {}) {
return this.request('patch', '/api/v1/auth/ui', null, data)
},
resetWalletKeys(wallet) { resetWalletKeys(wallet) {
return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then( return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then(
res => { res => {
+1 -2
View File
@@ -14,8 +14,7 @@ window.LNbits = {
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, hasPassword: data.has_password ?? false
uiCustomization: data.ui_customization || {}
} }
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) => {
+47 -1
View File
@@ -33,8 +33,41 @@ window.app.component('lnbits-extension-rating', {
} }
}) })
window.app.component('lnbits-fsat', {
template: '<span>{{ fsat }}</span>',
props: {
amount: {
type: Number,
default: 0
}
},
computed: {
fsat() {
return LNbits.utils.formatSat(this.amount)
}
}
})
window.app.component('lnbits-manage', { window.app.component('lnbits-manage', {
mixins: [window.windowMixin],
template: '#lnbits-manage', template: '#lnbits-manage',
computed: {
showAdmin() {
return this.LNBITS_ADMIN_UI
},
showUsers() {
return this.LNBITS_ADMIN_UI
},
showNode() {
return this.LNBITS_NODE_UI
},
showAudit() {
return this.LNBITS_AUDIT_ENABLED
},
showExtensions() {
return this.LNBITS_EXTENSIONS_DEACTIVATE_ALL === false
}
},
methods: { methods: {
isActive(path) { isActive(path) {
return window.location.pathname === path return window.location.pathname === path
@@ -48,8 +81,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 (
@@ -93,6 +128,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() {
@@ -112,6 +148,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 {
@@ -282,6 +319,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 {
@@ -314,6 +352,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 {
@@ -344,6 +383,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() {
@@ -382,6 +422,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,
@@ -425,6 +466,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,
@@ -445,7 +487,9 @@ window.app.component('username-password', {
username: this.userName, username: this.userName,
password: this.password_1, password: this.password_1,
passwordRepeat: this.password_2, passwordRepeat: this.password_2,
reset_key: this.resetKey reset_key: this.resetKey,
keycloakOrg: LNBITS_AUTH_KEYCLOAK_ORG || 'Keycloak',
keycloakIcon: LNBITS_AUTH_KEYCLOAK_ICON
} }
}, },
methods: { methods: {
@@ -660,6 +704,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>
@@ -738,6 +783,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) {
@@ -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
+2 -1
View File
@@ -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-error', { window.app.component('lnbits-error', {
template: '#lnbits-error', template: '#lnbits-error',
mixins: [window.windowMixin],
props: ['dynamic', 'code', 'message'], props: ['dynamic', 'code', 'message'],
computed: { computed: {
isExtension() { isExtension() {
+8 -4
View File
@@ -1,11 +1,15 @@
window.app.component('lnbits-footer', { window.app.component('lnbits-footer', {
template: '#lnbits-footer', template: '#lnbits-footer',
mixins: [window.windowMixin],
computed: { computed: {
title() {
return `${this.g.settings.siteTitle}, ${this.g.settings.siteTagline}`
},
version() { version() {
return this.$t('lnbits_version') + ': ' + this.g.settings.version return this.LNBITS_VERSION || 'unknown version'
},
title() {
return `${this.SITE_TITLE}, ${this.SITE_TAGLINE}`
},
showFooter() {
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
} }
} }
}) })
@@ -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]
}) })
+40 -23
View File
@@ -1,9 +1,49 @@
window.app.component('lnbits-header', { window.app.component('lnbits-header', {
template: '#lnbits-header', template: '#lnbits-header',
mixins: [window.windowMixin],
computed: { computed: {
hasServiceFeeMax() {
return (
this.g.user &&
this.LNBITS_SERVICE_FEE_MAX &&
this.LNBITS_SERVICE_FEE_MAX > 0
)
},
serviceFeeMax() {
return this.LNBITS_SERVICE_FEE_MAX || 0
},
hasServiceFee() {
return (
this.g.user && this.LNBITS_SERVICE_FEE && this.LNBITS_SERVICE_FEE > 0
)
},
serviceFee() {
return this.LNBITS_SERVICE_FEE || 0
},
hasCustomBadge() {
return this.LNBITS_CUSTOM_BADGE && this.LNBITS_CUSTOM_BADGE != ''
},
customBadge() {
return this.LNBITS_CUSTOM_BADGE || ''
},
customBadgeColor() {
return this.LNBITS_CUSTOM_BADGE_COLOR || ''
},
title() {
return this.SITE_TITLE
},
titleIsLnbits() {
return this.SITE_TITLE == 'LNbits'
},
customLogoUrl() {
return this.USE_CUSTOM_LOGO || null
},
showAdmin() { showAdmin() {
return this.g.user && (this.g.user.super_user || this.g.user.admin) return this.g.user && (this.g.user.super_user || this.g.user.admin)
}, },
showVoidwallet() {
return this.g.user && this.VOIDWALLET == true
},
displayName() { displayName() {
return ( return (
this.g.user?.extra?.display_name || this.g.user?.extra?.display_name ||
@@ -21,28 +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)
}
},
async handleLanguageChanged(lang) {
try {
await LNbits.api.updateUiCustomization({locale: lang.locale})
this.$q.notify({
type: 'positive',
message: 'Language Updated',
caption: lang.locale
})
} catch (e) {
LNbits.utils.notifyApiError(e)
}
}
} }
}) })
@@ -1,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: [
@@ -95,8 +96,8 @@ window.app.component('lnbits-home-logos', {
showLogos() { showLogos() {
return ( return (
this.g.isSatsDenomination && this.g.isSatsDenomination &&
this.g.settings.siteTitle == 'LNbits' && this.SITE_TITLE == 'LNbits' &&
this.g.settings.showHomePageElements == true this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
) )
} }
} }
@@ -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,16 +1,6 @@
window.app.component('lnbits-language-dropdown', { window.app.component('lnbits-language-dropdown', {
template: '#lnbits-language-dropdown', template: '#lnbits-language-dropdown',
computed: { mixins: [window.windowMixin],
currentLanguage() {
return (
this.langs.find(lang => lang.value === window.i18n.global.locale) || {
value: 'en',
label: 'English',
display: '🇬🇧 EN'
}
)
}
},
methods: { methods: {
activeLanguage(lang) { activeLanguage(lang) {
return window.i18n.global.locale === lang return window.i18n.global.locale === lang
@@ -19,7 +9,6 @@ window.app.component('lnbits-language-dropdown', {
this.g.locale = newValue this.g.locale = newValue
window.i18n.global.locale = newValue window.i18n.global.locale = newValue
this.$q.localStorage.set('lnbits.lang', newValue) this.$q.localStorage.set('lnbits.lang', newValue)
this.$emit('language-changed', newValue)
} }
}, },
data() { data() {
@@ -1,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,
+2 -1
View File
@@ -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
@@ -34,7 +35,7 @@ window.app.component('lnbits-qrcode', {
}, },
logo: { logo: {
type: String, type: String,
default: window.g.settings.qrLogo || null default: LNBITS_QR_LOGO
} }
}, },
data() { data() {
+1 -11
View File
@@ -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)
@@ -9,10 +10,6 @@ window.app.component('lnbits-theme', {
'g.disclaimerShown'(val) { 'g.disclaimerShown'(val) {
this.$q.localStorage.setItem('lnbits.disclaimerShown', val) this.$q.localStorage.setItem('lnbits.disclaimerShown', val)
}, },
'g.locale'(val) {
this.$q.localStorage.setItem('lnbits.lang', val)
window.i18n.global.locale = val
},
'g.isFiatPriority'(val) { 'g.isFiatPriority'(val) {
this.$q.localStorage.setItem('lnbits.isFiatPriority', val) this.$q.localStorage.setItem('lnbits.isFiatPriority', val)
}, },
@@ -127,13 +124,6 @@ window.app.component('lnbits-theme', {
if (this.g.mobileSimple === true) { if (this.g.mobileSimple === true) {
document.body.classList.add('mobile-simple') document.body.classList.add('mobile-simple')
} }
Object.entries(this.g.user?.uiCustomization || {}).forEach(
([key, value]) => {
if (key in this.g) {
this.g[key] = value
}
}
)
this.checkUrlParams() this.checkUrlParams()
} }
}) })
@@ -1,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,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(
+31 -27
View File
@@ -9,41 +9,16 @@ const localStore = (key, defaultValue) => {
} }
window.g = Vue.reactive({ window.g = Vue.reactive({
// vars from server
settings: SETTINGS,
currencies: CURRENCIES,
extensions: SETTINGS.extensions,
allowedCurrencies: SETTINGS.allowedCurrencies,
denomination: SETTINGS.denomination,
isSatsDenomination: SETTINGS.denomination == 'sats',
// local storage vars
themeChoice: localStore('lnbits.theme', SETTINGS.defaultTheme),
borderChoice: localStore('lnbits.border', SETTINGS.defaultBorder),
gradientChoice: localStore('lnbits.gradientBg', SETTINGS.defaultGradient),
reactionChoice: localStore('lnbits.reactions', SETTINGS.defaultReaction),
bgimageChoice: localStore(
'lnbits.backgroundImage',
SETTINGS.defaultBgimage || ''
),
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
disclaimerShown: localStore('lnbits.disclaimerShown', false),
isFiatPriority: localStore('lnbits.isFiatPriority', false),
mobileSimple: localStore('lnbits.mobileSimple', true),
walletFlip: localStore('lnbits.walletFlip', false),
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
darkChoice: localStore('lnbits.darkMode', true),
// cookie vars
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
// frontend vars
errorCode: null, errorCode: null,
errorMessage: null, errorMessage: null,
user: null, user: null,
wallet: null, wallet: null,
isPublicPage: true, isPublicPage: true,
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
offline: !navigator.onLine, offline: !navigator.onLine,
hasCamera: false, hasCamera: false,
visibleDrawer: false, visibleDrawer: false,
extensions: WINDOW_SETTINGS.EXTENSIONS,
fiatBalance: 0, fiatBalance: 0,
exchangeRate: 0, exchangeRate: 0,
fiatTracking: false, fiatTracking: false,
@@ -51,6 +26,35 @@ window.g = Vue.reactive({
walletEventListeners: [], walletEventListeners: [],
updatePayments: false, // used for updating the lnbits-payment-list updatePayments: false, // used for updating the lnbits-payment-list
updatePaymentsHash: false, // used for closing the receive dialog updatePaymentsHash: false, // used for closing the receive dialog
currencies: WINDOW_SETTINGS.LNBITS_CURRENCIES ?? [],
allowedCurrencies: WINDOW_SETTINGS.LNBITS_ALLOWED_CURRENCIES ?? [],
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
disclaimerShown: localStore('lnbits.disclaimerShown', false),
isFiatPriority: localStore('lnbits.isFiatPriority', false),
mobileSimple: localStore('lnbits.mobileSimple', true),
walletFlip: localStore('lnbits.walletFlip', false),
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
darkChoice: localStore('lnbits.darkMode', true),
themeChoice: localStore('lnbits.theme', WINDOW_SETTINGS.LNBITS_DEFAULT_THEME),
borderChoice: localStore(
'lnbits.border',
WINDOW_SETTINGS.LNBITS_DEFAULT_BORDER || 'hard-border'
),
gradientChoice: localStore(
'lnbits.gradientBg',
WINDOW_SETTINGS.LNBITS_DEFAULT_GRADIENT || false
),
reactionChoice: localStore(
'lnbits.reactions',
WINDOW_SETTINGS.LNBITS_DEFAULT_REACTION || 'confettiBothSides'
),
bgimageChoice: localStore(
'lnbits.backgroundImage',
WINDOW_SETTINGS.LNBITS_DEFAULT_BGIMAGE || ''
),
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION,
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats',
scanner: null, scanner: null,
newWalletType: null newWalletType: null
}) })
+2 -6
View File
@@ -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',
@@ -157,7 +152,8 @@ window.app.mixin({
return { return {
api: window._lnbitsApi, api: window._lnbitsApi,
utils: window._lnbitsUtils, utils: window._lnbitsUtils,
g: window.g g: window.g,
...WINDOW_SETTINGS
} }
}, },
// backwards compatibility for extensions, should not be used in the future // backwards compatibility for extensions, should not be used in the future
+17 -34
View File
@@ -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,
@@ -39,15 +40,6 @@ window.PageAccount = {
color: 'pink-3' color: 'pink-3'
} }
], ],
defaultSiteCustomisation: {
locale: 'en',
themeChoice: 'salvador',
bgimageChoice: '',
gradientChoice: true,
darkChoice: true,
borderChoice: 'hard-border',
reactionChoice: 'confettiBothSides'
},
reactionOptions: [ reactionOptions: [
'None', 'None',
'confettiBothSides', 'confettiBothSides',
@@ -222,18 +214,26 @@ window.PageAccount = {
} }
}, },
methods: { methods: {
activeLanguage(lang) {
return window.i18n.global.locale === lang
},
changeLanguage(newValue) { changeLanguage(newValue) {
window.i18n.global.locale = newValue window.i18n.global.locale = newValue
this.$q.localStorage.set('lnbits.lang', newValue) this.$q.localStorage.set('lnbits.lang', newValue)
}, },
async updateAccount() { async updateAccount() {
try { try {
const {data} = await LNbits.api.request('PATCH', '/api/v1/auth', null, { const {data} = await LNbits.api.request(
user_id: this.g.user.id, 'PUT',
username: this.g.user.username, '/api/v1/auth/update',
email: this.g.user.email, null,
extra: this.g.user.extra {
}) user_id: this.g.user.id,
username: this.g.user.username,
email: this.g.user.email,
extra: this.g.user.extra
}
)
this.untouchedUser = JSON.parse(JSON.stringify(this.g.user)) this.untouchedUser = JSON.parse(JSON.stringify(this.g.user))
this.hasUsername = !!data.username this.hasUsername = !!data.username
Quasar.Notify.create({ Quasar.Notify.create({
@@ -610,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() {
@@ -681,23 +681,6 @@ window.PageAccount = {
l => l.name !== label.name l => l.name !== label.name
) )
}) })
},
async siteCustomisationChanged(options = {}) {
try {
Object.entries(options || {}).forEach(([key, value]) => {
if (key in this.g) {
this.g[key] = value
}
})
await LNbits.api.updateUiCustomization(options)
this.$q.notify({
type: 'positive',
message: 'UI Customization updated.'
})
} catch (e) {
LNbits.utils.notifyApiError(e)
}
} }
}, },
@@ -711,7 +694,7 @@ window.PageAccount = {
await this.getUserAssets() await this.getUserAssets()
// filter out themes that are not allowed // filter out themes that are not allowed
this.themeOptions = this.themeOptions.filter(theme => this.themeOptions = this.themeOptions.filter(theme =>
this.g.settings.themeOptions.includes(theme.name) this.LNBITS_THEME_OPTIONS.includes(theme.name)
) )
} }
} }
+1
View File
@@ -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
View File
@@ -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,
+2 -1
View File
@@ -1,3 +1,4 @@
window.PageError = { window.PageError = {
template: '#page-error' template: '#page-error',
mixins: [window.windowMixin]
} }
+3 -2
View File
@@ -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,
@@ -842,8 +843,8 @@ window.PageExtensions = {
}, },
async created() { async created() {
this.extensions = await this.fetchAllExtensions() this.extensions = await this.fetchAllExtensions()
this.extbuilderEnabled = this.g.user.admin || this.g.settings.extBuilder this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
this.reviewsUrl = this.g.settings.extensionsReviewsUrl 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'
+7 -18
View File
@@ -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...')
}
}
+11 -16
View File
@@ -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: {
@@ -7,8 +8,7 @@ window.PageFirstInstall = {
isPwdRepeat: true, isPwdRepeat: true,
username: '', username: '',
password: '', password: '',
passwordRepeat: '', passwordRepeat: ''
firstInstallToken: ''
} }
} }
}, },
@@ -18,25 +18,20 @@ window.PageFirstInstall = {
} }
}, },
methods: { methods: {
setPassword() { async setPassword() {
LNbits.api try {
.request('PUT', `/api/v1/auth/first_install`, null, { await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
username: this.loginData.username, username: this.loginData.username,
password: this.loginData.password, password: this.loginData.password,
password_repeat: this.loginData.passwordRepeat, password_repeat: this.loginData.passwordRepeat
first_install_token: this.loginData.firstInstallToken
}) })
.then(async () => { window.location.href = '/admin'
const res = await LNbits.api.getAuthUser() } catch (e) {
this.g.user = LNbits.map.user(res.data) LNbits.utils.notifyApiError(e)
this.g.isPublicPage = false }
this.$router.push('/admin')
})
.catch(this.utils.notifyApiError)
} }
}, },
created() { created() {
const params = new URLSearchParams(window.location.search) document.title = 'First Install - LNbits'
this.loginData.firstInstallToken = params.get('token') || ''
} }
} }
+29 -3
View File
@@ -1,5 +1,6 @@
window.PageHome = { window.PageHome = {
template: '#page-home', template: '#page-home',
mixins: [window.windowMixin],
data() { data() {
return { return {
lnurl: '', lnurl: '',
@@ -19,15 +20,40 @@ window.PageHome = {
showClaimLnurl() { showClaimLnurl() {
return ( return (
this.lnurl !== '' && this.lnurl !== '' &&
this.g.settings.allowRegister && this.allowRegister &&
'user-id-only' in this.g.settings.authMethods 'user-id-only' in this.LNBITS_AUTH_METHODS
) )
}, },
formatDescription() { formatDescription() {
return LNbits.utils.convertMarkdown(this.g.settings.siteDescription) return LNbits.utils.convertMarkdown(this.SITE_DESCRIPTION)
}, },
isAccessTokenExpired() { isAccessTokenExpired() {
return this.$q.cookies.get('is_access_token_expired') return this.$q.cookies.get('is_access_token_expired')
},
allowRegister() {
return this.LNBITS_NEW_ACCOUNTS_ALLOWED
},
// TODO: this makes no sense
hasCustomImage() {
return this.LNBITS_CUSTOM_IMAGE
},
showHomepageElements() {
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS === true
},
siteTitle() {
return this.SITE_TITLE || ''
},
siteTagline() {
return this.SITE_TAGLINE || ''
},
adsEnabled() {
return this.AD_SPACE_ENABLED && this.AD_SPACE && this.AD_SPACE.length > 0
},
adsTitle() {
return this.AD_SPACE_TITLE || ''
},
ads() {
return this.AD_SPACE.map(ad => ad.split(';'))
} }
}, },
methods: { methods: {
+1
View File
@@ -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
View File
@@ -1,4 +1,5 @@
window.PageNode = { window.PageNode = {
mixins: [window.windowMixin],
template: '#page-node', template: '#page-node',
config: { config: {
globalProperties: { globalProperties: {
+1
View File
@@ -1,5 +1,6 @@
window.PagePayments = { window.PagePayments = {
template: '#page-payments', template: '#page-payments',
mixins: [window.windowMixin],
data() { data() {
return { return {
payments: [], payments: [],
+1 -14
View File
@@ -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
View File
@@ -1,5 +1,6 @@
window.PageWallet = { window.PageWallet = {
template: '#page-wallet', template: '#page-wallet',
mixins: [window.windowMixin],
data() { data() {
return { return {
parse: { parse: {
+1
View File
@@ -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,
+12 -53
View File
@@ -1,7 +1,7 @@
window._lnbitsUtils = { window._lnbitsUtils = {
url_for(url) { url_for(url) {
const _url = new URL(url, window.location.origin) const _url = new URL(url, window.location.origin)
_url.searchParams.set('v', window.g.settings.cacheKey) _url.searchParams.set('v', WINDOW_SETTINGS.CACHE_KEY)
return _url.toString() return _url.toString()
}, },
loadScript(src) { loadScript(src) {
@@ -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 {
-2
View File
@@ -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",
+1 -5
View File
@@ -178,11 +178,7 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.") logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
return return
from lnbits.core.services.payments import check_payment_status status = await payment.check_status(skip_internal_payment_notifications=True)
status = await check_payment_status(
payment, skip_internal_payment_notifications=True
)
payment.fee = status.fee_msat or payment.fee payment.fee = status.fee_msat or payment.fee
# only overwrite preimage if status.preimage provides it # only overwrite preimage if status.preimage provides it
payment.preimage = status.preimage or payment.preimage payment.preimage = status.preimage or payment.preimage
+4 -4
View File
@@ -70,10 +70,10 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
const RENDERED_ROUTE = '{{ normalize_path(request.path) }}' const RENDERED_ROUTE = '{{ normalize_path(request.path) }}'
const SETTINGS = {{ SETTINGS | tojson | safe }} const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
const CURRENCIES = {{ CURRENCIES | tojson | safe }} Object.keys(WINDOW_SETTINGS).forEach(key => {
// backwards compatibility for extensions (tpos) window[key] = WINDOW_SETTINGS[key]
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson | safe }} })
</script> </script>
<!-- vue templates --> <!-- vue templates -->
{% include('components.vue') %} {% include('pages.vue') %} {% include('components.vue') %} {% include('pages.vue') %}
+10 -10
View File
@@ -36,7 +36,7 @@ include('components/lnbits-error.vue') %}
<q-list v-if="g.user" dense class="lnbits-drawer__q-list"> <q-list v-if="g.user" dense class="lnbits-drawer__q-list">
<q-item-label header v-text="$t('manage')"></q-item-label> <q-item-label header v-text="$t('manage')"></q-item-label>
<div v-if="g.user.admin"> <div v-if="g.user.admin">
<q-item v-if="g.settings.showAdmin" to="/admin"> <q-item v-if="showAdmin" to="/admin">
<q-item-section side> <q-item-section side>
<q-icon <q-icon
name="settings" name="settings"
@@ -51,7 +51,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon> <q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item v-if="g.settings.showNodemanager" to="/node"> <q-item v-if="showNode" to="/node">
<q-item-section side> <q-item-section side>
<q-icon <q-icon
name="developer_board" name="developer_board"
@@ -66,7 +66,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon> <q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item v-if="g.settings.showAdmin" to="/users"> <q-item v-if="showUsers" to="/users">
<q-item-section side> <q-item-section side>
<q-icon <q-icon
name="groups" name="groups"
@@ -81,7 +81,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon> <q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item v-if="g.settings.showAudit" to="/audit"> <q-item v-if="showAudit" to="/audit">
<q-item-section side> <q-item-section side>
<q-icon <q-icon
name="playlist_add_check_circle" name="playlist_add_check_circle"
@@ -112,7 +112,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon> <q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item v-if="g.settings.showExtensions" to="/extensions"> <q-item v-if="showExtensions" to="/extensions">
<q-item-section side> <q-item-section side>
<q-icon <q-icon
name="extension" name="extension"
@@ -686,7 +686,7 @@ include('components/lnbits-error.vue') %}
dense dense
filled filled
v-model="walletName" v-model="walletName"
:label="$t('name_your_wallet', {name: g.settings.siteTitle + ' *'})" :label="$t('name_your_wallet', {name: SITE_TITLE + ' *'})"
></q-input> ></q-input>
<q-card-actions vertical align="center" class="q-pa-none"> <q-card-actions vertical align="center" class="q-pa-none">
<q-btn <q-btn
@@ -960,9 +960,9 @@ include('components/lnbits-error.vue') %}
<q-avatar size="32px" class="q-mr-md"> <q-avatar size="32px" class="q-mr-md">
<q-img <q-img
:src=" :src="
g.settings.keycloakIcon keycloakIcon
? g.settings.keycloakIcon ? keycloakIcon
: utils.url_for('lnbits/static/images/keycloak-logo.png') : 'lnbits/static/images/keycloak-logo.png'
" "
></q-img> ></q-img>
</q-avatar> </q-avatar>
@@ -970,7 +970,7 @@ include('components/lnbits-error.vue') %}
<span <span
v-text=" v-text="
$t('signin_with_custom_org', { $t('signin_with_custom_org', {
custom_org: g.settings.keycloakOrg || 'Keycloak' custom_org: keycloakOrg
}) })
" "
></span> ></span>
@@ -50,7 +50,7 @@
<br /> <br />
</div> </div>
<div class="col"> <div class="col">
<div v-if="g.settings.hasNodemanager"> <div v-if="LNBITS_NODE_UI">
<p> <p>
<span v-text="$t('node_management')"></span> <span v-text="$t('node_management')"></span>
</p> </p>
@@ -70,7 +70,7 @@
v-model="formData.lnbits_node_ui_transactions" v-model="formData.lnbits_node_ui_transactions"
></q-toggle> ></q-toggle>
</div> </div>
<p v-if="!g.settings.hasNodemanager"> <p v-if="!LNBITS_NODE_UI">
<span v-text="$t('node_management_not_supported')"></span> <span v-text="$t('node_management_not_supported')"></span>
</p> </p>
</div> </div>
@@ -86,42 +86,6 @@
></q-input> ></q-input>
</div> </div>
</div> </div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<p>
<span v-text="$t('wallet_featured_button_label')"></span>
</p>
<q-input
filled
type="text"
v-model="formData.lnbits_wallet_featured_button_label"
label="Loop to Onchain"
:hint="$t('wallet_featured_button_label_hint')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('wallet_featured_button_url')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_wallet_featured_button_url"
label="/boltz"
:hint="$t('wallet_featured_button_url_hint')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('wallet_featured_button_icon')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_wallet_featured_button_icon"
label="bolt"
:hint="$t('wallet_featured_button_icon_hint')"
></q-input>
</div>
</div>
<div class="row q-col-gutter-md q-mt-md"> <div class="row q-col-gutter-md q-mt-md">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_badge')"></span></p> <p><span v-text="$t('ui_custom_badge')"></span></p>
@@ -1,6 +1,6 @@
<template id="lnbits-footer"> <template id="lnbits-footer">
<q-footer <q-footer
v-if="g.settings.showHomePageElements" v-if="showFooter"
class="bg-transparent q-px-lg q-py-md" class="bg-transparent q-px-lg q-py-md"
:class="{'text-dark': !$q.dark.isActive}" :class="{'text-dark': !$q.dark.isActive}"
> >
@@ -9,7 +9,7 @@
<q-toolbar-title class="text-caption"> <q-toolbar-title class="text-caption">
<span v-text="title"></span> <span v-text="title"></span>
<br /> <br />
<small v-text="version"></small> <small v-text="$t('lnbits_version') + ': ' + version"></small>
</q-toolbar-title> </q-toolbar-title>
<q-space></q-space> <q-space></q-space>
<q-btn <q-btn
+15 -37
View File
@@ -1,10 +1,6 @@
<template id="lnbits-header"> <template id="lnbits-header">
<q-header bordered class="bg-marginal-bg"> <q-header bordered class="bg-marginal-bg">
<q-banner <q-banner v-if="showVoidwallet" class="bg-warning text-white" dense>
v-if="g.settings.showVoidwallet"
class="bg-warning text-white"
dense
>
<template v-slot:avatar> <template v-slot:avatar>
<q-icon name="warning" color="white" /> <q-icon name="warning" color="white" />
</template> </template>
@@ -28,15 +24,13 @@
<q-toolbar-title> <q-toolbar-title>
<q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/"> <q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
<q-img <q-img
v-if="customLogo" v-if="customLogoUrl"
height="30px" height="30px"
alt="Logo" alt="Logo"
:src="customLogo" :src="customLogoUrl"
></q-img> ></q-img>
<span v-else-if="g.settings.siteTitle == 'LNbits'" <span v-else-if="!titleIsLnbits"><strong>LN</strong>bits</span>
><strong>LN</strong>bits</span <span v-else v-text="title"></span>
>
<span v-else v-text="g.settings.siteTitle"></span>
</q-btn> </q-btn>
<q-badge v-if="g.user && g.user.super_user">Super User</q-badge> <q-badge v-if="g.user && g.user.super_user">Super User</q-badge>
<q-badge v-else-if="g.user && g.user.admin">Admin User</q-badge> <q-badge v-else-if="g.user && g.user.admin">Admin User</q-badge>
@@ -44,31 +38,31 @@
<q-badge <q-badge
class="q-mr-md" class="q-mr-md"
v-show="$q.screen.gt.sm" v-show="$q.screen.gt.sm"
v-if="g.settings.customBadge" v-if="hasCustomBadge"
:label="g.settings.customBadge" :label="customBadge"
:color="g.settings.customBadgeColor" :color="customBadgeColor"
> >
</q-badge> </q-badge>
<q-badge <q-badge
v-show="$q.screen.gt.sm" v-show="$q.screen.gt.sm"
v-if="g.user && g.settings.serviceFee > 0" v-if="hasServiceFee"
color="green" color="green"
class="q-mr-md" class="q-mr-md"
> >
<span <span
v-if="g.user && g.settings.serviceFeeMax > 0" v-if="hasServiceFeeMax"
v-text=" v-text="
$t('service_fee_max_badge', { $t('service_fee_max_badge', {
amount: g.settings.serviceFee, amount: serviceFee,
max: g.settings.serviceFeeMax, max: serviceFeeMax,
denom: g.denomination denom: g.denomination
}) })
" "
></span> ></span>
<span <span
v-else v-else
v-text="$t('service_fee_badge', {amount: g.settings.serviceFee})" v-text="$t('service_fee_badge', {amount: serviceFee})"
></span> ></span>
<q-tooltip><span v-text="$t('service_fee_tooltip')"></span></q-tooltip> <q-tooltip><span v-text="$t('service_fee_tooltip')"></span></q-tooltip>
</q-badge> </q-badge>
@@ -77,11 +71,9 @@
<span>OFFLINE</span> <span>OFFLINE</span>
</q-badge> </q-badge>
<lnbits-language-dropdown <lnbits-language-dropdown></lnbits-language-dropdown>
@language-changed="handleLanguageChanged({locale: $event})"
></lnbits-language-dropdown>
<q-btn-dropdown v-if="g.user" flat rounded size="md" 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
v-if="g.user?.extra?.picture && g.user?.extra?.picture !== ''" v-if="g.user?.extra?.picture && g.user?.extra?.picture !== ''"
@@ -144,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>
@@ -1,11 +1,5 @@
<template id="lnbits-language-dropdown"> <template id="lnbits-language-dropdown">
<q-btn-dropdown dense flat rounded size="md" class="q-pl-md"> <q-btn-dropdown dense flat rounded size="sm" icon="language" class="q-pl-md">
<template v-slot:label>
<q-item-section>
<q-item-label v-text="currentLanguage.display"></q-item-label>
<q-tooltip><span v-text="currentLanguage.label"></span></q-tooltip>
</q-item-section>
</template>
<q-list v-for="(lang, index) in langs" :key="index"> <q-list v-for="(lang, index) in langs" :key="index">
<q-item <q-item
clickable clickable
@@ -118,7 +118,7 @@
</q-list> </q-list>
</q-card-section> </q-card-section>
<q-expansion-item <q-expansion-item
v-if="!g.settings.hideApi" v-if="!HIDE_API"
group="api" group="api"
dense dense
expand-separator expand-separator
@@ -152,7 +152,7 @@
</q-expansion-item> </q-expansion-item>
<q-expansion-item <q-expansion-item
v-if="!g.settings.hideApi" v-if="!HIDE_API"
group="api" group="api"
dense dense
expand-separator expand-separator
@@ -197,7 +197,7 @@
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item <q-expansion-item
v-if="!g.settings.hideApi" v-if="!HIDE_API"
group="api" group="api"
dense dense
expand-separator expand-separator
@@ -244,7 +244,7 @@
</q-expansion-item> </q-expansion-item>
<q-expansion-item <q-expansion-item
v-if="!g.settings.hideApi" v-if="!HIDE_API"
group="api" group="api"
dense dense
expand-separator expand-separator
@@ -273,7 +273,7 @@
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item <q-expansion-item
v-if="!g.settings.hideApi" v-if="!HIDE_API"
group="api" group="api"
dense dense
expand-separator expand-separator
+30 -63
View File
@@ -258,20 +258,15 @@
class="q-mb-md" class="q-mb-md"
> >
</q-input> </q-input>
<div v-if="!g.user.email" class="row"></div>
<div v-if="!g.user.email" class="row"> <div v-if="!g.user.email" class="row">
<div {% if "google-auth" in LNBITS_AUTH_METHODS or
v-if=" "github-auth" in LNBITS_AUTH_METHODS %}
'google-auth' in g.settings.authMethods || <div class="col q-pa-sm text-h6">
'github-auth' in g.settings.authMethods
"
class="col q-pa-sm text-h6"
>
<span v-text="$t('verify_email')"></span>: <span v-text="$t('verify_email')"></span>:
</div> </div>
<div {%endif%} {% if "google-auth" in LNBITS_AUTH_METHODS %}
v-if="'google-auth' in g.settings.authMethods" <div class="col q-pa-sm">
class="col q-pa-sm"
>
<q-btn <q-btn
:href="`/api/v1/auth/google?user_id=${g.user.id}`" :href="`/api/v1/auth/google?user_id=${g.user.id}`"
type="a" type="a"
@@ -289,10 +284,8 @@
<div>Google</div> <div>Google</div>
</q-btn> </q-btn>
</div> </div>
<div {%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
v-if="'github-auth' in g.settings.authMethods" <div class="col q-pa-sm">
class="col q-pa-sm"
>
<q-btn <q-btn
:href="`/api/v1/auth/github?user_id=${g.user.id}`" :href="`/api/v1/auth/github?user_id=${g.user.id}`"
type="a" type="a"
@@ -310,6 +303,7 @@
<div>GitHub</div> <div>GitHub</div>
</q-btn> </q-btn>
</div> </div>
{%endif%}
</div> </div>
</q-card-section> </q-card-section>
@@ -339,16 +333,6 @@
class="q-mb-md" class="q-mb-md"
> >
</q-input> </q-input>
<q-input
v-model="g.user.extra.visible_wallet_count"
:label="$t('visible_wallet_count')"
filled
dense
type="number"
class="q-mb-md"
></q-input>
<q-input <q-input
v-model="g.user.external_id" v-model="g.user.external_id"
:label="$t('external_id')" :label="$t('external_id')"
@@ -396,11 +380,22 @@
<span v-text="$t('language')"></span> <span v-text="$t('language')"></span>
</div> </div>
<div class="col-8"> <div class="col-8">
<lnbits-language-dropdown <lnbits-language-dropdown />
@language-changed=" </div>
siteCustomisationChanged({locale: $event}) </div>
" <div class="row q-mb-md">
/> <div class="col-4">
<span v-text="$t('visible_wallet_count')"></span>
</div>
<div class="col-8">
<q-input
v-model="g.user.extra.visible_wallet_count"
:label="$t('visible_wallet_count')"
filled
dense
type="number"
class="q-mb-md"
></q-input>
</div> </div>
</div> </div>
@@ -412,9 +407,7 @@
<q-btn <q-btn
v-for="theme in themeOptions" v-for="theme in themeOptions"
:key="theme.name" :key="theme.name"
@click=" @click="g.themeChoice = theme.name"
siteCustomisationChanged({themeChoice: theme.name})
"
:color="theme.color" :color="theme.color"
dense dense
flat flat
@@ -434,9 +427,6 @@
<q-input <q-input
v-model="g.bgimageChoice" v-model="g.bgimageChoice"
:label="$t('background_image')" :label="$t('background_image')"
@update:model-value="
siteCustomisationChanged({bgimageChoice: $event})
"
> >
<q-tooltip <q-tooltip
><span v-text="$t('background_image')"></span ><span v-text="$t('background_image')"></span
@@ -455,9 +445,6 @@
round round
icon="gradient" icon="gradient"
v-model="g.gradientChoice" v-model="g.gradientChoice"
@update:model-value="
siteCustomisationChanged({gradientChoice: $event})
"
> >
<q-tooltip <q-tooltip
><span v-text="$t('toggle_gradient')"></span ><span v-text="$t('toggle_gradient')"></span
@@ -476,9 +463,6 @@
flat flat
round round
v-model="g.darkChoice" v-model="g.darkChoice"
@update:model-value="
siteCustomisationChanged({darkChoice: $event})
"
:icon="$q.dark.isActive ? 'brightness_3' : 'wb_sunny'" :icon="$q.dark.isActive ? 'brightness_3' : 'wb_sunny'"
size="sm" size="sm"
> >
@@ -497,9 +481,6 @@
v-model="g.borderChoice" v-model="g.borderChoice"
:options="borderOptions" :options="borderOptions"
label="Borders" label="Borders"
@update:model-value="
siteCustomisationChanged({borderChoice: $event})
"
> >
<q-tooltip <q-tooltip
><span v-text="$t('border_choices')"></span ><span v-text="$t('border_choices')"></span
@@ -514,7 +495,7 @@
<div class="col-8"> <div class="col-8">
<lnbits-notifications-btn <lnbits-notifications-btn
v-if="g.user" v-if="g.user"
pubkey="g.settings.webpushPubkey" pubkey="{{ WEBPUSH_PUBKEY }}"
></lnbits-notifications-btn> ></lnbits-notifications-btn>
</div> </div>
</div> </div>
@@ -527,9 +508,6 @@
v-model="g.reactionChoice" v-model="g.reactionChoice"
:options="reactionOptions" :options="reactionOptions"
label="Reactions" label="Reactions"
@update:model-value="
siteCustomisationChanged({reactionChoice: $event})
"
> >
<q-tooltip <q-tooltip
><span v-text="$t('payment_reactions')"></span ><span v-text="$t('payment_reactions')"></span
@@ -537,17 +515,6 @@
</q-select> </q-select>
</div> </div>
</div> </div>
<q-card-section>
<q-btn
@click="
siteCustomisationChanged(defaultSiteCustomisation)
"
:label="$t('reset_defaults')"
filled
color="primary"
class="float-right q-mb-md"
></q-btn>
</q-card-section>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="notifications"> <q-tab-panel name="notifications">
<q-card-section> <q-card-section>
@@ -558,7 +525,7 @@
></span> ></span>
<br /> <br />
<q-badge <q-badge
v-if="!g.settings.nostrConfigured" v-if="!LNBITS_NOSTR_CONFIGURED"
v-text="$t('not_connected')" v-text="$t('not_connected')"
></q-badge> ></q-badge>
</div> </div>
@@ -578,7 +545,7 @@
<span v-text="$t('notifications_chat_id')"></span> <span v-text="$t('notifications_chat_id')"></span>
<br /> <br />
<q-badge <q-badge
v-if="!g.settings.telegramConfigured" v-if="!LNBITS_TELEGRAM_CONFIGURED"
v-text="$t('not_connected')" v-text="$t('not_connected')"
></q-badge> ></q-badge>
</div> </div>
@@ -974,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="
-12
View File
@@ -53,18 +53,6 @@
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat" @click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
/> </template /> </template
></q-input> ></q-input>
<q-input
filled
v-model.trim="loginData.firstInstallToken"
:type="loginData.isPwd ? 'password' : 'text'"
:label="$t('first_install_token')"
><template v-slot:append>
<q-icon
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwd = !loginData.isPwd"
/> </template
></q-input>
<q-btn <q-btn
@click="setPassword()" @click="setPassword()"
unelevated unelevated
+15 -28
View File
@@ -2,14 +2,14 @@
<div class="home row justify-center items-center"> <div class="home row justify-center items-center">
<div <div
class="full-width content-center" class="full-width content-center"
:style="`max-width: ${g.settings.customImage ? '850' : '600'}px; min-height: 55vh;`" :style="`max-width: ${hasCustomImage ? '850' : '600'}px; min-height: 55vh;`"
> >
<div v-if="g.settings.showHomepageElements" class="row q-mb-md"> <div v-if="showHomepageElements" class="row q-mb-md">
<div class="col-12"> <div class="col-12">
<div> <div>
<h5 v-text="g.settings.siteTitle" class="q-my-none"></h5> <h5 v-text="siteTitle" class="q-my-none"></h5>
<template v-if="$q.screen.gt.sm"> <template v-if="$q.screen.gt.sm">
<h6 class="q-my-sm" v-text="g.settings.siteTagline"></h6> <h6 class="q-my-sm" v-text="siteTagline"></h6>
<p class="q-my-sm" v-html="formatDescription"></p> <p class="q-my-sm" v-html="formatDescription"></p>
</template> </template>
</div> </div>
@@ -31,10 +31,7 @@
<div class="row"> <div class="row">
<div <div
class="col-12" class="col-12"
:class="{ :class="{'col-sm-7': hasCustomImage, 'col-lg-6': hasCustomImage}"
'col-sm-7': g.settings.customImage,
'col-lg-6': g.settings.customImage
}"
> >
<div v-if="showClaimLnurl"> <div v-if="showClaimLnurl">
<q-card-section> <q-card-section>
@@ -57,8 +54,8 @@
<div v-else> <div v-else>
<username-password <username-password
v-if="authMethod != 'user-id-only'" v-if="authMethod != 'user-id-only'"
:allowed_new_users="g.settings.allowRegister" :allowed_new_users="allowRegister"
:auth-methods="g.settings.authMethods" :auth-methods="LNBITS_AUTH_METHODS"
:auth-action="authAction" :auth-action="authAction"
v-model:user-name="username" v-model:user-name="username"
v-model:password_1="password" v-model:password_1="password"
@@ -73,7 +70,7 @@
v-if="authAction !== 'reset'" v-if="authAction !== 'reset'"
> >
<p <p
v-if="authAction === 'login' && g.settings.allowRegister" v-if="authAction === 'login' && allowRegister"
class="q-mb-none" class="q-mb-none"
> >
Not registered? Not registered?
@@ -85,9 +82,7 @@
> >
</p> </p>
<p <p
v-else-if=" v-else-if="authAction === 'login' && !allowRegister"
authAction === 'login' && !g.settings.allowRegister
"
class="q-mb-none" class="q-mb-none"
> >
<span v-text="$t('new_user_not_allowed')"></span> <span v-text="$t('new_user_not_allowed')"></span>
@@ -106,7 +101,7 @@
</username-password> </username-password>
<user-id-only <user-id-only
v-if="authMethod == 'user-id-only'" v-if="authMethod == 'user-id-only'"
:allowed_new_users="g.settings.allowRegister" :allowed_new_users="allowRegister"
v-model:usr="usr" v-model:usr="usr"
v-model:wallet="walletName" v-model:wallet="walletName"
:auth-action="authAction" :auth-action="authAction"
@@ -119,20 +114,16 @@
</user-id-only> </user-id-only>
</div> </div>
</div> </div>
<div v-if="g.settings.customImage" class="col-sm-5 col-lg-6 gt-xs"> <div v-if="hasCustomImage" class="col-sm-5 col-lg-6 gt-xs">
<div class="full-height flex flex-center q-pa-lg"> <div class="full-height flex flex-center q-pa-lg">
<q-img <q-img :src="hasCustomImage" :ratio="1" width="250px"></q-img>
:src="g.settings.customImage"
:ratio="1"
width="250px"
></q-img>
</div> </div>
</div> </div>
</div> </div>
</q-card> </q-card>
</div> </div>
<div v-if="g.settings.showHomepageElements"> <div v-if="showHomepageElements">
<div class="flex flex-center q-gutter-md q-py-md"> <div class="flex flex-center q-gutter-md q-py-md">
<q-btn <q-btn
outline outline
@@ -157,14 +148,10 @@
</div> </div>
<div <div
v-if="g.settings.showAdSpace" v-if="adsEnabled"
class="justify-center col-10 q-my-lg row q-col-gutter-sm" class="justify-center col-10 q-my-lg row q-col-gutter-sm"
> >
<a <a :href="ad[0]" v-for="ad in g.ads" class="lnbits-ad col-12 col-md-4">
:href="ad[0]"
v-for="ad in g.settings.adSpace"
class="lnbits-ad col-12 col-md-4"
>
<q-img v-if="$q.dark.isActive" :src="ad[1]"></q-img> <q-img v-if="$q.dark.isActive" :src="ad[1]"></q-img>
<q-img v-else :src="ad[2]"></q-img> <q-img v-else :src="ad[2]"></q-img>
</a> </a>
+1 -14
View File
@@ -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>
+4 -17
View File
@@ -171,19 +171,6 @@
><span v-text="$t('camera_tooltip')"></span ><span v-text="$t('camera_tooltip')"></span
></q-tooltip> ></q-tooltip>
</q-btn> </q-btn>
<div
v-if="g.settings.walletFeaturedButtonUrl"
class="float-right q-mt-sm q-ml-sm"
>
<q-btn
color="primary"
:label="g.settings.walletFeaturedButtonLabel"
:icon="g.settings.walletFeaturedButtonIcon || undefined"
size="sm"
:to="g.settings.walletFeaturedButtonUrl"
>
</q-btn>
</div>
<lnbits-update-balance <lnbits-update-balance
v-if="$q.screen.gt.md" v-if="$q.screen.gt.md"
:wallet_id="this.g.wallet.id" :wallet_id="this.g.wallet.id"
@@ -212,10 +199,10 @@
@send-lnurl="handleSendLnurl" @send-lnurl="handleSendLnurl"
:chart-config="chartConfig" :chart-config="chartConfig"
></lnbits-wallet-extra> ></lnbits-wallet-extra>
<q-card class="lnbits-wallet-ads" v-if="g.settings.showAdSpace"> <q-card class="lnbits-wallet-ads" v-if="AD_SPACE_ENABLED">
<q-card-section class="text-subtitle1"> <q-card-section class="text-subtitle1">
<span v-text="g.settings.adSpaceTitle"></span> <span v-text="AD_SPACE_TITLE"></span>
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.settings.adSpace"> <a :href="ad[0]" class="lnbits-ad" v-for="ad in g.ads">
<q-img class="q-mb-xs" v-if="$q.dark.isActive" :src="ad[1]"></q-img> <q-img class="q-mb-xs" v-if="$q.dark.isActive" :src="ad[1]"></q-img>
<q-img class="q-mb-xs" v-else :src="ad[2]"></q-img> <q-img class="q-mb-xs" v-else :src="ad[2]"></q-img>
</a> </a>
@@ -289,7 +276,7 @@
></q-input> ></q-input>
</div> </div>
<q-input <q-input
v-if="g.settings.hasHoldinvoice" v-if="has_holdinvoice"
filled filled
dense dense
v-model="receive.data.payment_hash" v-model="receive.data.payment_hash"
+4 -2
View File
@@ -45,8 +45,10 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
const SETTINGS = {{ SETTINGS | tojson | safe }} const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
const CURRENCIES = {{ CURRENCIES | tojson | safe }} Object.keys(WINDOW_SETTINGS).forEach(key => {
window[key] = WINDOW_SETTINGS[key]
})
</script> </script>
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {% {% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
for url in INCLUDED_JS %} for url in INCLUDED_JS %}
-7
View File
@@ -14,13 +14,6 @@ from lnbits.settings import settings
def log_server_info(): def log_server_info():
logger.info("LNbits Info") logger.info("LNbits Info")
if settings.first_install:
logger.success("This is a fresh install of LNbits.")
if settings.first_install_token:
logger.success(
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
"Please provide this token on /first_install."
)
logger.info(f"Version: {settings.version}") logger.info(f"Version: {settings.version}")
logger.info(f"Baseurl: {settings.lnbits_baseurl}") logger.info(f"Baseurl: {settings.lnbits_baseurl}")
logger.info(f"Host: {settings.host}") logger.info(f"Host: {settings.host}")
-2
View File
@@ -98,8 +98,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",
Generated
+762 -813
View File
File diff suppressed because it is too large Load Diff
+59 -37
View File
@@ -8,30 +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",
"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",
@@ -41,14 +42,14 @@ 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)", "greenlet (>=3.3.0,<4.0.0)",
] ]
@@ -59,30 +60,30 @@ 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.11.11"]
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]
@@ -91,6 +92,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",
+2 -2
View File
@@ -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 ################################

Some files were not shown because too many files have changed in this diff Show More