Compare commits

..
Author SHA1 Message Date
Vlad Stan 648a657922 feat: download specific release 2026-02-09 12:40:36 +02:00
Vlad Stan cf251b1f33 chode: lint 2026-02-09 08:54:59 +02:00
Vlad Stan f94c4e432e fix: auth 2026-02-09 08:53:33 +02:00
Vlad Stan 6360c49ed0 feat: configure settings 2026-02-09 08:53:33 +02:00
Vlad Stan bfa35d9c69 refactor: extract methods and move them to the bottom 2026-02-09 08:53:33 +02:00
Vlad Stan 561d0976b3 fix: logging 2026-02-09 08:53:33 +02:00
Vlad Stan d40515e1ba fix: no logs 2026-02-09 08:53:33 +02:00
Vlad Stan 766fe3b5c6 feat: try to capture logs 2026-02-09 08:53:33 +02:00
Vlad Stan 82940a91b6 fix: better paths 2026-02-09 08:53:33 +02:00
Vlad Stan 5bd524660f feat: experiment node from python 2026-02-09 08:53:33 +02:00
Vlad Stan be2bb929c0 doc: add todos for missing doc 2026-02-09 08:53:33 +02:00
blackcoffeexbt 7d34f459fa Include error messages from Spark API in error message to users when payments from Spark fail 2026-02-05 12:26:17 +00:00
Arc 11d16620dd fundle 2026-01-29 05:02:31 +00:00
Arc c99bb28963 Merge branch 'origindev' into sparkwallet 2026-01-29 04:57:29 +00:00
arcbtc 2bd398b59c fixes 2026-01-23 00:41:18 +00:00
arcbtc 0e69aa536d bundle 2026-01-21 23:58:25 +00:00
arcbtc 984fc05caa Merge remote-tracking branch 'origin/dev' into sparkwallet 2026-01-21 23:57:42 +00:00
Arc 213cde5ec6 make 2026-01-14 12:12:10 +00:00
Arc 2cd694c36e Merge remote-tracking branch 'origin/dev' into sparkwallet 2026-01-14 12:10:37 +00:00
Arc fb9cb6f2ed adds invoice stream 2026-01-14 12:05:59 +00:00
Arc 2852caeb42 moved to own repo 2026-01-13 13:36:28 +00:00
Arc 34fa193dc5 make 2026-01-10 15:51:39 +00:00
Arc 0f38fcfaa9 Needed longer wait time + poll 2026-01-10 15:48:13 +00:00
Arc 2cac4e6b4d sending kinda working 2026-01-10 15:39:35 +00:00
Arc 9983af4eec graceful shutdown 2026-01-10 15:26:43 +00:00
Arc a061dda5c1 make 2026-01-10 14:50:19 +00:00
Arc 6b233cc067 working 2026-01-10 14:22:17 +00:00
72 changed files with 623 additions and 1741 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 ]
+21 -76
View File
@@ -4,12 +4,10 @@ from typing import Any
from uuid import uuid4 from uuid import uuid4
from lnbits.core.crud.extensions import get_user_active_extensions_ids from lnbits.core.crud.extensions import get_user_active_extensions_ids
from lnbits.core.crud.wallets import clear_wallet_cache, create_wallet, get_wallets from lnbits.core.crud.wallets import create_wallet, get_wallets
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.models import UserAcls from lnbits.core.models import UserAcls
from lnbits.db import Connection, Filters, Page from lnbits.db import Connection, Filters, Page
from lnbits.helpers import sha256s
from lnbits.utils.cache import cache
from ..models import ( from ..models import (
Account, Account,
@@ -43,7 +41,6 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
"DELETE from accounts WHERE id = :user", "DELETE from accounts WHERE id = :user",
{"user": user_id}, {"user": user_id},
) )
await clear_user_id_cache(user_id)
async def get_accounts( async def get_accounts(
@@ -72,7 +69,6 @@ async def get_accounts(
accounts.email, accounts.email,
accounts.pubkey, accounts.pubkey,
accounts.external_id, accounts.external_id,
accounts.activated,
SUM(COALESCE(( SUM(COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)) as balance_msat, ), 0)) as balance_msat,
@@ -97,18 +93,12 @@ async def get_accounts(
) )
async def get_account( async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
if len(user_id) == 0: if len(user_id) == 0:
return None return None
return await (conn or db).fetchone( return await (conn or db).fetchone(
""" "SELECT * FROM accounts WHERE id = :id",
SELECT * FROM accounts {"id": user_id},
WHERE id = :id AND (activated = true OR activated = :activated)
""",
{"id": user_id, "activated": active_only},
Account, Account,
) )
@@ -134,79 +124,55 @@ async def delete_accounts_no_wallets(
async def get_account_by_username( async def get_account_by_username(
username: str, active_only: bool = True, conn: Connection | None = None username: str, conn: Connection | None = None
) -> Account | None: ) -> Account | None:
if len(username) == 0: if len(username) == 0:
return None return None
return await (conn or db).fetchone( return await (conn or db).fetchone(
""" "SELECT * FROM accounts WHERE LOWER(username) = :username",
SELECT * FROM accounts {"username": username.lower()},
WHERE
LOWER(username) = :username
AND (activated = true OR activated = :activated)
""",
{"username": username.lower(), "activated": active_only},
Account, Account,
) )
async def get_account_by_pubkey( async def get_account_by_pubkey(
pubkey: str, active_only: bool = True, conn: Connection | None = None pubkey: str, conn: Connection | None = None
) -> Account | None: ) -> Account | None:
return await (conn or db).fetchone( return await (conn or db).fetchone(
""" "SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
SELECT * FROM accounts {"pubkey": pubkey.lower()},
WHERE
LOWER(pubkey) = :pubkey
AND (activated = true OR activated = :activated)
""",
{"pubkey": pubkey.lower(), "activated": active_only},
Account, Account,
) )
async def get_account_by_email( async def get_account_by_email(
email: str, active_only: bool = True, conn: Connection | None = None email: str, conn: Connection | None = None
) -> Account | None: ) -> Account | None:
if len(email) == 0: if len(email) == 0:
return None return None
return await (conn or db).fetchone( return await (conn or db).fetchone(
""" "SELECT * FROM accounts WHERE LOWER(email) = :email",
SELECT * FROM accounts {"email": email.lower()},
WHERE
LOWER(email) = :email
AND (activated = true OR activated = :activated)
""",
{"email": email.lower(), "activated": active_only},
Account, Account,
) )
async def get_account_by_username_or_email( async def get_account_by_username_or_email(
username_or_email: str, username_or_email: str, conn: Connection | None = None
active_only: bool = True,
conn: Connection | None = None,
) -> Account | None: ) -> Account | None:
return await (conn or db).fetchone( return await (conn or db).fetchone(
""" """
SELECT * FROM accounts SELECT * FROM accounts
WHERE WHERE LOWER(email) = :value or LOWER(username) = :value
(LOWER(email) = :value or LOWER(username) = :value)
AND (activated = true OR activated = :activated)
""", """,
{"value": username_or_email.lower(), "activated": active_only}, {"value": username_or_email.lower()},
Account, Account,
) )
async def get_user( async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> User | None:
async with db.reuse_conn(conn) if conn else db.connect() as conn: async with db.reuse_conn(conn) if conn else db.connect() as conn:
account = await get_account(user_id, active_only, conn=conn) account = await get_account(user_id, conn=conn)
if not account: if not account:
return None return None
return await get_user_from_account(account, conn=conn) return await get_user_from_account(account, conn=conn)
@@ -225,7 +191,6 @@ async def get_user_from_account(
return User( return User(
id=account.id, id=account.id,
activated=account.activated,
email=account.email, email=account.email,
username=account.username, username=account.username,
pubkey=account.pubkey, pubkey=account.pubkey,
@@ -239,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 {},
) )
@@ -251,31 +215,12 @@ async def update_user_access_control_list(
async def get_user_access_control_lists( async def get_user_access_control_lists(
user_id: str, active_only: bool = True, conn: Connection | None = None user_id: str, conn: Connection | None = None
) -> UserAcls: ) -> UserAcls:
user_acls = await (conn or db).fetchone( user_acls = await (conn or db).fetchone(
""" "SELECT id, access_control_list FROM accounts WHERE id = :id",
SELECT id, access_control_list FROM accounts {"id": user_id},
WHERE id = :user_id AND (activated = true OR activated = :activated)
""",
{"user_id": user_id, "activated": active_only},
UserAcls, UserAcls,
) )
return user_acls or UserAcls(id=user_id) return user_acls or UserAcls(id=user_id)
async def clear_user_id_cache(user_id: str):
user = await get_user(user_id, active_only=True)
if user:
clear_user_cache(user)
def clear_user_cache(user: User):
user_cache_key: str | None = cache.pop(
f"auth:user:cache_key:{sha256s(user.id)}", None
)
if user_cache_key:
cache.pop(user_cache_key)
for wallet in user.wallets:
clear_wallet_cache(wallet)
+8 -20
View File
@@ -50,7 +50,7 @@ async def delete_wallet(
deleted: bool = True, deleted: bool = True,
conn: Connection | None = None, conn: Connection | None = None,
) -> None: ) -> None:
clear_wallet_id_cache(wallet_id) _clear_wallet_cache(wallet_id)
now = int(time()) now = int(time())
await (conn or db).execute( await (conn or db).execute(
@@ -65,7 +65,7 @@ async def delete_wallet(
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None: async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
clear_wallet_id_cache(wallet_id) _clear_wallet_cache(wallet_id)
await (conn or db).execute( await (conn or db).execute(
"DELETE FROM wallets WHERE id = :wallet", "DELETE FROM wallets WHERE id = :wallet",
{"wallet": wallet_id}, {"wallet": wallet_id},
@@ -75,7 +75,7 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
async def delete_wallet_by_id( async def delete_wallet_by_id(
wallet_id: str, conn: Connection | None = None wallet_id: str, conn: Connection | None = None
) -> int | None: ) -> int | None:
clear_wallet_id_cache(wallet_id) _clear_wallet_cache(wallet_id)
now = int(time()) now = int(time())
result = await (conn or db).execute( result = await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input) # Timestamp placeholder is safe from SQL injection (not user input)
@@ -226,14 +226,11 @@ async def get_wallet_for_key(
) -> Wallet | None: ) -> Wallet | None:
wallet = await (conn or db).fetchone( wallet = await (conn or db).fetchone(
""" """
SELECT wallets.*, COALESCE(( SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0) ), 0)
AS balance_msat FROM wallets AS balance_msat FROM wallets
INNER JOIN accounts ON wallets.user = accounts.id WHERE (adminkey = :key OR inkey = :key) AND deleted = false
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
""", """,
{"key": key}, {"key": key},
Wallet, Wallet,
@@ -253,11 +250,8 @@ async def get_base_wallet_for_key(
) -> BaseWallet | None: ) -> BaseWallet | None:
wallet = await (conn or db).fetchone( wallet = await (conn or db).fetchone(
""" """
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
INNER JOIN accounts ON wallets.user = accounts.id WHERE (adminkey = :key OR inkey = :key) AND deleted = false
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
""", """,
{"key": key}, {"key": key},
BaseWallet, BaseWallet,
@@ -300,14 +294,8 @@ async def get_total_balance(conn: Connection | None = None):
return row.get("balance", 0) or 0 return row.get("balance", 0) or 0
def clear_wallet_id_cache(wallet_id: str): def _clear_wallet_cache(wallet_id):
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}") cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
if cached_wallet: if cached_wallet:
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}") cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}") cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
def clear_wallet_cache(wallet: Wallet):
cache.pop(f"auth:wallet:{wallet.id}")
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
cache.pop(f"auth:x-api-key:{wallet.inkey}")
-16
View File
@@ -848,19 +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")
async def m044_add_activated_to_accounts(db: Connection):
"""
Adds activated column to accounts.
Used for account activation status.
"""
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
+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):
-7
View File
@@ -181,14 +181,12 @@ class AccountId(BaseModel):
class Account(AccountId): class Account(AccountId):
activated: bool = True
external_id: str | None = None # for external account linking external_id: str | None = None # for external account linking
username: str | None = None username: str | None = None
password_hash: str | None = None password_hash: str | None = None
pubkey: str | None = None pubkey: str | None = None
email: str | None = None email: str | None = None
extra: UserExtra = UserExtra() extra: UserExtra = UserExtra()
ui_customization: dict = Field(default_factory=dict)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@@ -242,7 +240,6 @@ class Account(AccountId):
class AccountOverview(Account): class AccountOverview(Account):
activated: bool = True
transaction_count: int | None = 0 transaction_count: int | None = 0
wallet_count: int | None = 0 wallet_count: int | None = 0
balance_msat: int | None = 0 balance_msat: int | None = 0
@@ -278,7 +275,6 @@ class AccountFilters(FilterModel):
class User(BaseModel): class User(BaseModel):
id: str id: str
activated: bool = True
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
email: str | None = None email: str | None = None
@@ -292,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]:
@@ -318,7 +313,6 @@ class RegisterUser(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20) username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50) password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50) password_repeat: str = Query(default=..., min_length=8, max_length=50)
invitation_code: str | None = Query(default=None, max_length=256)
class CreateUser(BaseModel): class CreateUser(BaseModel):
@@ -362,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",
+4 -44
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,
@@ -118,8 +78,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
""" """
webhook_id = settings.paypal_webhook_id webhook_id = settings.paypal_webhook_id
if not webhook_id: if not webhook_id:
logger.warning("PayPal webhook ID not set.") logger.warning("PayPal webhook ID not set; skipping verification.")
raise ValueError("PayPal webhook cannot be verified. Missing webhook ID.") return
required_headers = { required_headers = {
"PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"), "PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"),
@@ -129,8 +89,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
"PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"), "PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"),
} }
if not all(required_headers.values()): if not all(required_headers.values()):
logger.warning("Missing PayPal webhook headers.") logger.warning("Missing PayPal webhook headers; skipping verification.")
raise ValueError("PayPal webhook cannot be verified. Missing headers.") return
try: try:
async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client: async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client:
+5 -40
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(
@@ -776,7 +752,7 @@ async def _pay_internal_invoice(
await update_payment(internal_payment, conn=conn) await update_payment(internal_payment, conn=conn)
logger.success(f"internal payment successful {internal_payment.checking_id}") logger.success(f"internal payment successful {internal_payment.checking_id}")
await _send_payment_notification_in_background(wallet.id, payment, conn=conn) send_payment_notification_in_background(wallet, payment)
# notify receiver asynchronously # notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue from lnbits.tasks import internal_invoice_queue
@@ -849,8 +825,7 @@ async def _pay_external_invoice(
payment = await update_payment_success_status( payment = await update_payment_success_status(
payment, payment_response, conn=conn payment, payment_response, conn=conn
) )
send_payment_notification_in_background(wallet, payment)
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
logger.success(f"payment successful {payment_response.checking_id}") logger.success(f"payment successful {payment_response.checking_id}")
payment.checking_id = payment_response.checking_id payment.checking_id = payment_response.checking_id
@@ -893,7 +868,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"
@@ -1058,13 +1033,3 @@ async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
await update_payment(payment) await update_payment(payment)
return response return response
async def _send_payment_notification_in_background(
wallet_id: str, payment: Payment, conn: Connection | None = None
):
# fetch balance again
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet:
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
send_payment_notification_in_background(wallet, payment)
-24
View File
@@ -3,10 +3,8 @@ from uuid import uuid4
from loguru import logger from loguru import logger
from lnbits.core.crud.settings import set_settings_field
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.models.extensions import UserExtension from lnbits.core.models.extensions import UserExtension
from lnbits.core.models.users import RegisterUser
from lnbits.db import Connection from lnbits.db import Connection
from lnbits.settings import ( from lnbits.settings import (
EditableSettings, EditableSettings,
@@ -194,25 +192,3 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
editable_settings = EditableSettings.from_dict(settings.dict()) editable_settings = EditableSettings.from_dict(settings.dict())
return await create_admin_settings(account.id, editable_settings.dict()) return await create_admin_settings(account.id, editable_settings.dict())
async def check_register_activation_settings(data: RegisterUser):
if not settings.lnbits_require_user_activation:
return None
if settings.lnbits_user_activation_by_invitation_code:
code = data.invitation_code.strip() if data.invitation_code else ""
if len(code) == 0:
raise ValueError("Invitation code cannot be empty.")
if code == settings.lnbits_register_reusable_activation_code:
return None
if code in settings.lnbits_register_one_time_activation_codes:
settings.lnbits_register_one_time_activation_codes.remove(code)
await set_settings_field(
"lnbits_register_one_time_activation_codes",
settings.lnbits_register_one_time_activation_codes,
)
return None
raise ValueError("Invalid invitation code.")
raise ValueError("No activation method provided.")
+5 -41
View File
@@ -26,10 +26,7 @@ from lnbits.core.models.users import (
UpdateAccessControlList, UpdateAccessControlList,
) )
from lnbits.core.services import create_user_account from lnbits.core.services import create_user_account
from lnbits.core.services.users import ( from lnbits.core.services.users import update_user_account
check_register_activation_settings,
update_user_account,
)
from lnbits.decorators import ( from lnbits.decorators import (
access_token_payload, access_token_payload,
check_account_exists, check_account_exists,
@@ -89,7 +86,6 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
account = await get_account_by_username_or_email(data.username) account = await get_account_by_username_or_email(data.username)
if not account or not account.verify_password(data.password): if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.") raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
return _auth_success_response(account.username, account.id, account.email) return _auth_success_response(account.username, account.id, account.email)
@@ -98,7 +94,7 @@ async def nostr_login(request: Request) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98): if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.") raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
event = _nostr_nip98_event(request) event = _nostr_nip98_event(request)
account = await get_account_by_pubkey(event["pubkey"], active_only=False) account = await get_account_by_pubkey(event["pubkey"])
if not account: if not account:
account = Account( account = Account(
id=uuid4().hex, id=uuid4().hex,
@@ -106,8 +102,6 @@ async def nostr_login(request: Request) -> JSONResponse:
extra=UserExtra(provider="nostr"), extra=UserExtra(provider="nostr"),
) )
await create_user_account(account) await create_user_account(account)
if not account.activated:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
return _auth_success_response(account.username or "", account.id, account.email) return _auth_success_response(account.username or "", account.id, account.email)
@@ -363,14 +357,12 @@ async def register(data: RegisterUser) -> JSONResponse:
if not is_valid_username(data.username): if not is_valid_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.") raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
if await get_account_by_username(data.username, active_only=False): if await get_account_by_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.") raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
if data.email and not is_valid_email_address(data.email): if data.email and not is_valid_email_address(data.email):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.") raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
await check_register_activation_settings(data)
account = Account( account = Account(
id=uuid4().hex, id=uuid4().hex,
email=data.email, email=data.email,
@@ -475,7 +467,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:
@@ -491,41 +483,13 @@ 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_by_username(data.username, False)
if account:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
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.")
account.username = data.username account.username = data.username
account.extra = account.extra or UserExtra() account.extra = account.extra or UserExtra()
account.extra.provider = "lnbits" account.extra.provider = "lnbits"
@@ -541,7 +505,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.") raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
redirect_path = "/wallet" redirect_path = "/wallet"
account = await get_account_by_email(email, active_only=False) account = await get_account_by_email(email)
if verified_user_id: if verified_user_id:
if account: if account:
+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:
+2 -40
View File
@@ -20,7 +20,6 @@ from lnbits.core.crud import (
update_admin_settings, update_admin_settings,
update_wallet, update_wallet,
) )
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
from lnbits.core.crud.wallets import delete_wallet_by_id from lnbits.core.crud.wallets import delete_wallet_by_id
from lnbits.core.models import ( from lnbits.core.models import (
AccountFilters, AccountFilters,
@@ -74,7 +73,7 @@ async def api_get_users(
summary="Get user by Id", summary="Get user by Id",
) )
async def api_get_user(user_id: str) -> User: async def api_get_user(user_id: str) -> User:
user = await get_user(user_id, active_only=False) user = await get_user(user_id)
if not user: if not user:
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.") raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
return user return user
@@ -198,7 +197,7 @@ async def api_users_reset_password(user_id: str) -> str:
return f"reset_key_{reset_key_b64}" return f"reset_key_{reset_key_b64}"
@users_router.put( @users_router.get(
"/user/{user_id}/admin", "/user/{user_id}/admin",
dependencies=[Depends(check_super_user)], dependencies=[Depends(check_super_user)],
name="Give or revoke admin permsisions to a user", name="Give or revoke admin permsisions to a user",
@@ -221,43 +220,6 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
) )
@users_router.put(
"/user/{user_id}/activate",
name="Activate or deactivate a user",
)
async def api_users_toggle_activated(
user_id: str, admin_account: Account = Depends(check_admin)
) -> SimpleStatus:
if user_id == settings.super_user:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Cannot deactivate super user.",
)
if user_id == admin_account.id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="You cannot deactivate yourself.",
)
if settings.is_admin_user(user_id):
settings.lnbits_admin_users.remove(user_id)
user_account = await get_account(user_id, active_only=False)
if not user_account:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="User not found.",
)
user_account.activated = not user_account.activated
await update_account(user_account)
await clear_user_id_cache(user_id)
return SimpleStatus(
success=True,
message=f"User {'activated' if user_account.activated else 'deactivated'}.",
)
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user") @users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]: async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
return await get_wallets(user_id, deleted=None) return await get_wallets(user_id, deleted=None)
+4 -2
View File
@@ -9,7 +9,6 @@ from fastapi import (
) )
from lnbits.core.crud.wallets import ( from lnbits.core.crud.wallets import (
clear_wallet_cache,
create_wallet, create_wallet,
get_wallets_paginated, get_wallets_paginated,
) )
@@ -38,6 +37,7 @@ from lnbits.decorators import (
require_invoice_key, require_invoice_key,
) )
from lnbits.helpers import generate_filter_params_openapi from lnbits.helpers import generate_filter_params_openapi
from lnbits.utils.cache import cache
from ..crud import ( from ..crud import (
delete_wallet, delete_wallet,
@@ -134,7 +134,9 @@ async def api_reset_wallet_keys(
if not wallet or wallet.user != account_id.id: if not wallet or wallet.user != account_id.id:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found") raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
clear_wallet_cache(wallet) cache.pop(f"auth:wallet:{wallet.id}")
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
cache.pop(f"auth:x-api-key:{wallet.inkey}")
wallet.adminkey = uuid4().hex wallet.adminkey = uuid4().hex
wallet.inkey = uuid4().hex wallet.inkey = uuid4().hex
+42 -9
View File
@@ -12,6 +12,7 @@ from typing import Any, Generic, Literal, TypeVar, get_origin
from loguru import logger from loguru import logger
from pydantic import BaseModel, ValidationError, root_validator from pydantic import BaseModel, ValidationError, root_validator
from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
from sqlalchemy.sql import text from sqlalchemy.sql import text
@@ -55,6 +56,14 @@ def compat_timestamp_placeholder(key: str):
return f":{key}" return f":{key}"
def get_placeholder(model: Any, field: str) -> str:
type_ = model.__fields__[field].type_
if type_ == datetime:
return compat_timestamp_placeholder(field)
else:
return f":{field}"
class Compat: class Compat:
type: str | None = "<inherited>" type: str | None = "<inherited>"
schema: str | None = "<inherited>" schema: str | None = "<inherited>"
@@ -317,7 +326,31 @@ class Database(Compat):
self.engine: AsyncEngine = create_async_engine( self.engine: AsyncEngine = create_async_engine(
database_uri, echo=settings.debug_database database_uri, echo=settings.debug_database
) )
if self.type in {POSTGRES, COCKROACH}:
@event.listens_for(self.engine.sync_engine, "connect")
def register_custom_types(dbapi_connection, *_):
def _parse_date(value) -> datetime:
if value is None:
value = "1970-01-01 00:00:00"
f = "%Y-%m-%d %H:%M:%S.%f"
if "." not in value:
f = "%Y-%m-%d %H:%M:%S"
# Parse and add UTC timezone info
return datetime.strptime(value, f).replace(tzinfo=timezone.utc)
dbapi_connection.run_async(
lambda connection: connection.set_type_codec(
"TIMESTAMP",
encoder=datetime,
decoder=_parse_date,
schema="pg_catalog",
)
)
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
logger.trace(f"database {self.type} added for {self.name}") logger.trace(f"database {self.type} added for {self.name}")
@asynccontextmanager @asynccontextmanager
@@ -630,7 +663,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
placeholders = [] placeholders = []
keys = model_to_dict(model).keys() keys = model_to_dict(model).keys()
for field in keys: for field in keys:
placeholders.append(f":{field}") placeholders.append(get_placeholder(model, field))
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword) # add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
fields = ", ".join([f'"{key}"' for key in keys]) fields = ", ".join([f'"{key}"' for key in keys])
values = ", ".join(placeholders) values = ", ".join(placeholders)
@@ -648,8 +681,9 @@ def update_query(
""" """
fields = [] fields = []
for field in model_to_dict(model).keys(): for field in model_to_dict(model).keys():
placeholder = get_placeholder(model, field)
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword) # add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
fields.append(f'"{field}" = :{field}') fields.append(f'"{field}" = {placeholder}')
query = ", ".join(fields) query = ", ".join(fields)
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608 return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
@@ -667,12 +701,7 @@ def model_to_dict(model: BaseModel) -> dict:
if model.__fields__[key].field_info.extra.get("no_database", False): if model.__fields__[key].field_info.extra.get("no_database", False):
continue continue
if isinstance(value, datetime): if isinstance(value, datetime):
if DB_TYPE == SQLITE: _dict[key] = value.timestamp()
_dict[key] = value.timestamp()
else:
# remove tz. postgres and cockroach TIMESTAMP is not tz aware
# so it will throw if we dont remove the UTC.
_dict[key] = value.replace(tzinfo=None)
continue continue
if ( if (
type(type_) is type(BaseModel) type(type_) is type(BaseModel)
@@ -728,7 +757,11 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
if DB_TYPE == SQLITE: if DB_TYPE == SQLITE:
_dict[key] = datetime.fromtimestamp(value, timezone.utc) _dict[key] = datetime.fromtimestamp(value, timezone.utc)
else: else:
_dict[key] = value.replace(tzinfo=timezone.utc) # Ensure PostgreSQL datetime values have timezone info
if isinstance(value, datetime) and value.tzinfo is None:
_dict[key] = value.replace(tzinfo=timezone.utc)
else:
_dict[key] = value
continue continue
if issubclass(type_, BaseModel): if issubclass(type_, BaseModel):
_dict[key] = dict_to_submodel(type_, value) _dict[key] = dict_to_submodel(type_, value)
-1
View File
@@ -261,7 +261,6 @@ async def check_account_id_exists(
account_id, account_id,
expiry=settings.auth_authentication_cache_minutes * 60, expiry=settings.auth_authentication_cache_minutes * 60,
) )
cache.set(f"auth:user:cache_key:{sha256s(account.id)}", cache_key)
return account_id return account_id
+51 -8
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
-131
View File
@@ -41,14 +41,6 @@ class UsersSettings(LNbitsSettings):
lnbits_admin_users: list[str] = Field(default=[]) lnbits_admin_users: list[str] = Field(default=[])
lnbits_allowed_users: list[str] = Field(default=[]) lnbits_allowed_users: list[str] = Field(default=[])
lnbits_allow_new_accounts: bool = Field(default=True) lnbits_allow_new_accounts: bool = Field(default=True)
lnbits_require_user_activation: bool = Field(default=False)
lnbits_user_activation_by_email: bool = Field(default=False)
lnbits_user_activation_by_payment: bool = Field(default=False)
lnbits_user_activation_by_invitation_code: bool = Field(default=False)
lnbits_register_reusable_activation_code: str = Field(default="")
lnbits_register_one_time_activation_codes: list[str] = Field(default=[])
@property @property
def new_accounts_allowed(self) -> bool: def new_accounts_allowed(self) -> bool:
@@ -260,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(
@@ -1003,7 +990,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)
@@ -1153,123 +1139,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")
lnbits_user_activation_by_email: bool = Field(alias="userActivationByEmail")
lnbits_user_activation_by_payment: bool = Field(alias="userActivationByPayment")
lnbits_user_activation_by_invitation_code: bool = Field(
alias="userActivationByInvitationCode"
)
@classmethod
def from_settings(cls, settings: Settings):
ads = [x.split(";") for x in settings.lnbits_ad_space.split(",")]
return cls(
adSpace=ads,
adSpaceTitle=settings.lnbits_ad_space_title,
showAdSpace=settings.lnbits_ad_space_enabled and len(ads) > 0,
allowRegister=settings.new_accounts_allowed,
qrLogo=settings.lnbits_qr_logo,
siteDescription=settings.lnbits_site_description,
siteTitle=settings.lnbits_site_title,
siteTagline=settings.lnbits_site_tagline,
authMethods=settings.auth_allowed_methods,
keycloakOrg=settings.keycloak_client_custom_org,
keycloakIcon=settings.keycloak_client_custom_icon,
hasHoldinvoice=settings.has_holdinvoice,
hasNodemanager=settings.has_nodemanager,
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
customLogo=settings.lnbits_custom_logo,
showVoidwallet=settings.lnbits_backend_wallet_class == "VoidWallet",
version=settings.version,
showHomePageElements=settings.lnbits_show_home_page_elements,
hideApi=settings.lnbits_hide_api,
cacheKey=str(settings.server_startup_time),
webpushPubkey=settings.lnbits_webpush_pubkey,
showExtensions=not settings.lnbits_extensions_deactivate_all,
showAudit=settings.lnbits_audit_enabled,
showAdmin=settings.lnbits_admin_ui,
customImage=settings.lnbits_custom_image,
customBadge=settings.lnbits_custom_badge,
customBadgeColor=settings.lnbits_custom_badge_color,
serviceFee=settings.lnbits_service_fee,
serviceFeeMax=settings.lnbits_service_fee_max,
serviceFeeWallet=settings.lnbits_service_fee_wallet,
themeOptions=settings.lnbits_theme_options,
defaultReaction=settings.lnbits_default_reaction,
defaultTheme=settings.lnbits_default_theme,
defaultBorder=settings.lnbits_default_border,
defaultGradient=settings.lnbits_default_gradient,
defaultBgimage=settings.lnbits_default_bgimage,
denomination=settings.lnbits_denomination,
extensions=list(settings.lnbits_installed_extensions_ids),
allowedCurrencies=settings.lnbits_allowed_currencies,
extensionsReviewsUrl=settings.lnbits_extensions_reviews_url,
extBuilder=settings.lnbits_extensions_builder_activate_non_admins,
nostrConfigured=settings.is_nostr_notifications_configured(),
telegramConfigured=settings.is_telegram_notifications_configured(),
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
userActivationByEmail=settings.lnbits_user_activation_by_email,
userActivationByPayment=settings.lnbits_user_activation_by_payment,
userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code,
)
class SuperSettings(EditableSettings): class SuperSettings(EditableSettings):
super_user: str super_user: str
File diff suppressed because one or more lines are too long
+10 -10
View File
File diff suppressed because one or more lines are too long
+1 -35
View File
@@ -171,7 +171,6 @@ window.localisation.en = {
drop_db: 'Remove Data', drop_db: 'Remove Data',
enable: 'Enable', enable: 'Enable',
enabled: 'Enabled', enabled: 'Enabled',
disabled: 'Disabled',
pay_to_enable: 'Pay To Enable', pay_to_enable: 'Pay To Enable',
enable_extension_details: 'Enable extension for current user', enable_extension_details: 'Enable extension for current user',
disable: 'Disable', disable: 'Disable',
@@ -179,8 +178,6 @@ window.localisation.en = {
installed: 'Installed', installed: 'Installed',
activated: 'Activated', activated: 'Activated',
deactivated: 'Deactivated', deactivated: 'Deactivated',
activate: 'Activate',
deactivate: 'Deactivate',
release_notes: 'Release Notes', release_notes: 'Release Notes',
activate_extension_details: 'Make extension available/unavailable for users', activate_extension_details: 'Make extension available/unavailable for users',
featured: 'Featured', featured: 'Featured',
@@ -189,8 +186,6 @@ window.localisation.en = {
only_admins_can_create_extensions: only_admins_can_create_extensions:
'Only admin accounts can create extensions', 'Only admin accounts can create extensions',
admin_only: 'Admin Only', admin_only: 'Admin Only',
make_user_admin: 'Make User Admin',
revoke_admin: 'Revoke Admin',
new_version: 'New Version', new_version: 'New Version',
reviews_url: 'Reviews URL', reviews_url: 'Reviews URL',
reviews_url_label: 'Reviews server URL', reviews_url_label: 'Reviews server URL',
@@ -285,7 +280,7 @@ window.localisation.en = {
'Nip5 identifier to send notifications to', 'Nip5 identifier to send notifications to',
notifications_nostr_identifiers: 'Nostr Identifiers', notifications_nostr_identifiers: 'Nostr Identifiers',
notifications_nostr_identifiers_desc: notifications_nostr_identifiers_desc:
'List of identifiers to send notifications to.', 'List of identifiers to send notifications to',
notifications_telegram_config: 'Telegram Configuration', notifications_telegram_config: 'Telegram Configuration',
notifications_enable_telegram: 'Enable Telegram', notifications_enable_telegram: 'Enable Telegram',
@@ -523,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',
@@ -664,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',
@@ -709,25 +694,6 @@ window.localisation.en = {
allowed_users_label: 'User ID', allowed_users_label: 'User ID',
allow_creation_user: 'Allow creation of new users', allow_creation_user: 'Allow creation of new users',
allow_creation_user_desc: 'Allow creation of new users on the index page', allow_creation_user_desc: 'Allow creation of new users on the index page',
require_user_activation: 'Require user activation',
require_user_activation_desc:
'New users will be activated only after they pass one of the confirmation methods. Admins can activate users manually from the admin panel.',
reusable_activation_code: 'Reusable activation code',
reusable_activation_code_label: 'Reusable activation code',
reusable_activation_code_hint:
'This activation code can be used multiple times by different users.',
one_time_activation_code: 'One-time activation codes',
one_time_activation_code_label: 'Add activation code',
one_time_activation_code_hint:
'List of one-time activation codes. Each code can be used only once, then will be reomved from the list.',
invitation_code: 'Invitation Code',
invitation_code_hint: 'The invitation code that you have received.',
email: 'Email',
email_confirmation_hint: 'Email address to send the confirmation code to.',
nostr_identifier: 'Nostr Identifier',
nostr_identifier_hint:
'Nostr nip5 identifier or <npub> to send the confirmation code to.',
new_user_not_allowed: 'Registration is disabled.', new_user_not_allowed: 'Registration is disabled.',
start_user_impersonation: 'Impersonate this user', start_user_impersonation: 'Impersonate this user',
stop_user_impersonation: 'Stop User Impersonation', stop_user_impersonation: 'Stop User Impersonation',
+3 -7
View File
@@ -66,7 +66,7 @@ window._lnbitsApi = {
name: name name: name
}) })
}, },
register(username, email, password, password_repeat, invitation_code) { register(username, email, password, password_repeat) {
return axios({ return axios({
method: 'POST', method: 'POST',
url: '/api/v1/auth/register', url: '/api/v1/auth/register',
@@ -74,8 +74,7 @@ window._lnbitsApi = {
username, username,
email, email,
password, password,
password_repeat, password_repeat
invitation_code
} }
}) })
}, },
@@ -148,16 +147,13 @@ window._lnbitsApi = {
name: name, name: name,
wallet_type: walletType, wallet_type: walletType,
...opts ...opts
}) }).catch(LNbits.utils.notifyApiError)
}, },
updateWallet(name, wallet) { updateWallet(name, wallet) {
return this.request('patch', '/api/v1/wallet', wallet.adminkey, { return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
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) => {
+19 -26
View File
@@ -35,6 +35,23 @@ window.app.component('lnbits-extension-rating', {
window.app.component('lnbits-manage', { window.app.component('lnbits-manage', {
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
@@ -432,10 +449,8 @@ window.app.component('username-password', {
username: String, username: String,
password_1: String, password_1: String,
password_2: String, password_2: String,
invitationCode: String,
resetKey: String resetKey: String
}, },
data() { data() {
return { return {
oauth: [ oauth: [
@@ -448,10 +463,8 @@ window.app.component('username-password', {
password: this.password_1, password: this.password_1,
passwordRepeat: this.password_2, passwordRepeat: this.password_2,
reset_key: this.resetKey, reset_key: this.resetKey,
confirmationMethod: 'code', keycloakOrg: LNBITS_AUTH_KEYCLOAK_ORG || 'Keycloak',
confirmationEmail: '', keycloakIcon: LNBITS_AUTH_KEYCLOAK_ICON
confirmationCode: this.invitationCode || '',
showConfirmationCode: false
} }
}, },
methods: { methods: {
@@ -464,7 +477,6 @@ window.app.component('username-password', {
this.$emit('update:userName', this.username) this.$emit('update:userName', this.username)
this.$emit('update:password_1', this.password) this.$emit('update:password_1', this.password)
this.$emit('update:password_2', this.passwordRepeat) this.$emit('update:password_2', this.passwordRepeat)
this.$emit('update:invitationCode', this.confirmationCode)
this.$emit('register') this.$emit('register')
}, },
reset() { reset() {
@@ -556,25 +568,6 @@ window.app.component('username-password', {
computed: { computed: {
showOauth() { showOauth() {
return this.oauth.some(m => this.authMethods.includes(m)) return this.oauth.some(m => this.authMethods.includes(m))
},
disableRegister() {
const usernameOK = !!this.username
const passwordOK = !!this.password && this.password.length >= 8
const passwordsMatch = this.password === this.passwordRepeat
const codeOk =
this.confirmationMethodsCount === 0 ||
this.confirmationMethod !== 'code' ||
this.confirmationCode.length > 0
return !usernameOK || !passwordOK || !passwordsMatch || !codeOk
},
confirmationMethodsCount() {
const methods = [
this.g.settings.userActivationByEmail,
this.g.settings.userActivationByPayment,
this.g.settings.userActivationByInvitationCode
]
return methods.filter(Boolean).length
} }
}, },
created() {} created() {}
@@ -4,9 +4,7 @@ window.app.component('lnbits-admin-users', {
data() { data() {
return { return {
formAddUser: '', formAddUser: '',
formAddAdmin: '', formAddAdmin: ''
formAddActivationCode: '',
showReusableActivationCode: false
} }
}, },
methods: { methods: {
@@ -33,24 +31,6 @@ window.app.component('lnbits-admin-users', {
removeAdminUser(user) { removeAdminUser(user) {
let admin_users = this.formData.lnbits_admin_users let admin_users = this.formData.lnbits_admin_users
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user) this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
},
addOneTimeActivationCode() {
const code = this.formAddActivationCode
const activationCodes =
this.formData.lnbits_register_one_time_activation_codes
if (code?.length && !activationCodes.includes(code)) {
this.formData.lnbits_register_one_time_activation_codes = [
...activationCodes,
code
]
this.formAddActivationCode = ''
}
},
removeOneTimeActivationCode(code) {
const codes = this.formData.lnbits_register_one_time_activation_codes
this.formData.lnbits_register_one_time_activation_codes = codes.filter(
u => u !== code
)
} }
} }
}) })
+7 -4
View File
@@ -1,11 +1,14 @@
window.app.component('lnbits-footer', { window.app.component('lnbits-footer', {
template: '#lnbits-footer', template: '#lnbits-footer',
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
} }
} }
}) })
+39 -11
View File
@@ -1,9 +1,48 @@
window.app.component('lnbits-header', { window.app.component('lnbits-header', {
template: '#lnbits-header', template: '#lnbits-header',
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 ||
@@ -30,17 +69,6 @@ window.app.component('lnbits-header', {
window.location = '/users' window.location = '/users'
} catch (e) { } catch (e) {
console.warn(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) LNbits.utils.notifyApiError(e)
} }
} }
@@ -95,8 +95,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,16 +1,5 @@
window.app.component('lnbits-language-dropdown', { window.app.component('lnbits-language-dropdown', {
template: '#lnbits-language-dropdown', template: '#lnbits-language-dropdown',
computed: {
currentLanguage() {
return (
this.langs.find(lang => lang.value === window.i18n.global.locale) || {
value: 'en',
label: 'English',
display: '🇬🇧 EN'
}
)
}
},
methods: { methods: {
activeLanguage(lang) { activeLanguage(lang) {
return window.i18n.global.locale === lang return window.i18n.global.locale === lang
@@ -19,7 +8,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 -1
View File
@@ -34,7 +34,7 @@ window.app.component('lnbits-qrcode', {
}, },
logo: { logo: {
type: String, type: String,
default: window.g.settings.qrLogo || null default: LNBITS_QR_LOGO
} }
}, },
data() { data() {
@@ -9,10 +9,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 +123,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()
} }
}) })
@@ -71,10 +71,6 @@ window.app.component('lnbits-wallet-extra', {
'lnbits.exchangeRate.' + this.g.wallet.currency, 'lnbits.exchangeRate.' + this.g.wallet.currency,
this.g.exchangeRate this.g.exchangeRate
) )
if (this.g.exchangeRate <= 0) {
this.g.fiatTracking = false
this.g.isFiatPriority = false
}
}) })
.catch(e => console.error(e)) .catch(e => console.error(e))
} }
@@ -97,7 +97,6 @@ window.app.component('lnbits-wallet-new', {
this.g.lastWalletId = res.data.id this.g.lastWalletId = res.data.id
this.$router.push(`/wallet/${res.data.id}`) this.$router.push(`/wallet/${res.data.id}`)
}) })
.catch(LNbits.utils.notifyApiError)
} }
}, },
created() { created() {
+32 -27
View File
@@ -9,41 +9,17 @@ 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'),
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
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 +27,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 -1
View File
@@ -157,7 +157,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
+15 -33
View File
@@ -39,15 +39,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 +213,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({
@@ -681,23 +680,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 +693,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)
) )
} }
} }
+2 -2
View File
@@ -842,8 +842,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'
+10 -16
View File
@@ -7,8 +7,7 @@ window.PageFirstInstall = {
isPwdRepeat: true, isPwdRepeat: true,
username: '', username: '',
password: '', password: '',
passwordRepeat: '', passwordRepeat: ''
firstInstallToken: ''
} }
} }
}, },
@@ -18,25 +17,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 -7
View File
@@ -11,7 +11,6 @@ window.PageHome = {
email: '', email: '',
password: '', password: '',
passwordRepeat: '', passwordRepeat: '',
invitationCode: '',
walletName: '', walletName: '',
signup: false signup: false
} }
@@ -20,15 +19,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: {
@@ -41,7 +65,6 @@ window.PageHome = {
this.username = null this.username = null
this.password = null this.password = null
this.passwordRepeat = null this.passwordRepeat = null
this.invitationCode = null
this.authAction = 'register' this.authAction = 'register'
this.authMethod = authMethod this.authMethod = authMethod
@@ -53,8 +76,7 @@ window.PageHome = {
this.username, this.username,
this.email, this.email,
this.password, this.password,
this.passwordRepeat, this.passwordRepeat
this.invitationCode
) )
this.refreshAuthUser() this.refreshAuthUser()
} catch (e) { } catch (e) {
+4 -17
View File
@@ -70,10 +70,10 @@ window.PageUsers = {
usersTable: { usersTable: {
columns: [ columns: [
{ {
name: 'activated', name: 'admin',
align: 'left', align: 'left',
label: this.$t('activated'), label: 'Admin',
field: 'activated', field: 'admin',
sortable: false sortable: false
}, },
{ {
@@ -401,7 +401,7 @@ window.PageUsers = {
toggleAdmin(userId) { toggleAdmin(userId) {
LNbits.api LNbits.api
.request('PUT', `/users/api/v1/user/${userId}/admin`) .request('GET', `/users/api/v1/user/${userId}/admin`)
.then(() => { .then(() => {
this.fetchUsers() this.fetchUsers()
Quasar.Notify.create({ Quasar.Notify.create({
@@ -412,19 +412,6 @@ window.PageUsers = {
}) })
.catch(LNbits.utils.notifyApiError) .catch(LNbits.utils.notifyApiError)
}, },
toggleUserActivated(userId) {
LNbits.api
.request('PUT', `/users/api/v1/user/${userId}/activate`)
.then(res => {
this.fetchUsers()
Quasar.Notify.create({
type: 'positive',
message: res.data.message,
icon: null
})
})
.catch(LNbits.utils.notifyApiError)
},
async showAccountPage(user_id) { async showAccountPage(user_id) {
this.activeUser.showPassword = false this.activeUser.showPassword = false
this.activeUser.showUserId = false this.activeUser.showUserId = false
+2 -6
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) {
@@ -144,7 +144,7 @@ window._lnbitsUtils = {
return null return null
} }
}, },
async notifyApiError(error) { notifyApiError(error) {
if (!error.response) { if (!error.response) {
return console.error(error) return console.error(error)
} }
@@ -154,10 +154,6 @@ window._lnbitsUtils = {
500: 'negative' 500: 'negative'
} }
let messages = error.response.data.detail let messages = error.response.data.detail
if (!messages) {
const text = await error.response.data?.text()
messages = this.parseJSONSafe(text)?.detail
}
if (messages) { if (messages) {
messages = Array.isArray(messages) messages = Array.isArray(messages)
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`) ? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
+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') %}
+16 -90
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
@@ -814,90 +814,16 @@ include('components/lnbits-error.vue') %}
type="password" type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]" :rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
></q-input> ></q-input>
<div
v-if="confirmationMethodsCount > 1"
class="row justify-center q-mb-md"
>
<q-tabs
v-model="confirmationMethod"
dense
active-color="primary"
indicator-color="primary"
>
<q-tab
v-if="g.settings.userActivationByInvitationCode"
name="code"
icon="confirmation_number"
label="Code"
></q-tab>
<q-tab
v-if="g.settings.userActivationByPayment"
name="payment"
icon="bolt"
label="Payment"
></q-tab>
<q-tab
v-if="g.settings.userActivationByEmail"
name="email"
icon="email"
label="Email"
></q-tab>
</q-tabs>
</div>
<div v-if="confirmationMethodsCount > 0" class="q-mb-md">
<q-tab-panels v-model="confirmationMethod">
<q-tab-panel name="code" class="q-pa-none">
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon name="info" color="orange-4" class="q-mr-xs"></q-icon>
You need an invitation code to register.
</div>
<div>
<q-input
dense
filled
v-model="confirmationCode"
:label="$t('invitation_code')"
:type="showConfirmationCode ? 'text' : 'password'"
:hint="$t('invitation_code_hint')"
>
<q-btn
@click="showConfirmationCode = !showConfirmationCode"
dense
flat
:icon="
showConfirmationCode ? 'visibility_off' : 'visibility'
"
color="grey"
></q-btn>
</q-input>
</div>
</q-tab-panel>
<q-tab-panel name="payment">
<div>payment</div>
</q-tab-panel>
<q-tab-panel name="email" class="q-pa-none">
<div>
<q-input
dense
filled
v-model="confirmationEmail"
:label="$t('email')"
:hint="$t('email_confirmation_hint')"
>
</q-input>
</div>
</q-tab-panel>
</q-tab-panels>
</div>
<div class="row justify-end"> <div class="row justify-end">
<q-btn <q-btn
unelevated unelevated
color="primary" color="primary"
:disable="disableRegister" :disable="
!password ||
!passwordRepeat ||
!username ||
password !== passwordRepeat
"
type="submit" type="submit"
class="full-width" class="full-width"
:label="$t('create_account')" :label="$t('create_account')"
@@ -1034,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>
@@ -1044,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>
@@ -310,13 +310,7 @@
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<q-select <q-select
filled filled
:options="[ :options="[$t('second'), $t('minute'), $t('hour')]"
{label: $t('second'), value: 'second'},
{label: $t('minute'), value: 'minute'},
{label: $t('hour'), value: 'hour'}
]"
emit-value
map-options
v-model="formData.lnbits_rate_limit_unit" v-model="formData.lnbits_rate_limit_unit"
:label="$t('time_unit')" :label="$t('time_unit')"
></q-select> ></q-select>
@@ -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>
+3 -234
View File
@@ -30,6 +30,7 @@
> >
</q-chip> </q-chip>
</div> </div>
<br />
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<p><span v-text="$t('allowed_users')"></span></p> <p><span v-text="$t('allowed_users')"></span></p>
@@ -56,10 +57,7 @@
> >
</q-chip> </q-chip>
</div> </div>
</div> <br />
</div>
<div class="row">
<div class="col-12 col-md-6 q-mt-sm">
<q-item tag="label" v-ripple> <q-item tag="label" v-ripple>
<q-item-section> <q-item-section>
<q-item-label v-text="$t('allow_creation_user')"></q-item-label> <q-item-label v-text="$t('allow_creation_user')"></q-item-label>
@@ -78,237 +76,8 @@
/> />
</q-item-section> </q-item-section>
</q-item> </q-item>
</div> <br />
<div class="col-12 col-md-6 q-mt-sm">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('require_user_activation')"></q-item-label>
<q-item-label
caption
v-text="$t('require_user_activation_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_require_user_activation"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
<div v-if="formData.lnbits_require_user_activation" class="row">
<div class="col">
<q-list bordered class="rounded-borders">
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-icon name="confirmation_number" size="md"></q-icon>
</q-item-section>
<q-item-section> Invitation Code </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
size="md"
:label="
formData.lnbits_user_activation_by_invitation_code
? $t('enabled')
: $t('disabled')
"
v-model="formData.lnbits_user_activation_by_invitation_code"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-card-section>
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon
name="info"
color="orange-4"
size="18px"
class="q-mr-xs"
></q-icon>
Users will need to provide a valid invitation code during
registration to activate their account.
</div>
</q-card-section>
<q-card-section>
<div class="row">
<div class="col-12 col-md-6 q-pr-sm">
<p><span v-text="$t('reusable_activation_code')"></span></p>
<q-input
filled
v-model="formData.lnbits_register_reusable_activation_code"
:type="showReusableActivationCode ? 'text' : 'password'"
:label="$t('reusable_activation_code_label')"
:hint="$t('reusable_activation_code_hint')"
>
<q-btn
@click="
showReusableActivationCode = !showReusableActivationCode
"
dense
flat
:icon="
showReusableActivationCode
? 'visibility_off'
: 'visibility'
"
color="grey"
></q-btn>
<q-btn
@click="
utils.copyText(
formData.lnbits_register_reusable_activation_code
)
"
dense
flat
icon="content_copy"
color="grey"
></q-btn>
</q-input>
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('one_time_activation_code')"></span></p>
<q-input
filled
v-model="formAddActivationCode"
@keydown.enter="addOneTimeActivationCode"
type="text"
:label="$t('one_time_activation_code_label')"
:hint="$t('one_time_activation_code_hint')"
>
<q-btn
@click="addOneTimeActivationCode"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="code in formData.lnbits_register_one_time_activation_codes"
:key="code"
removable
@remove="removeOneTimeActivationCode(code)"
color="primary"
text-color="white"
:label="code"
class="ellipsis"
>
</q-chip>
</div>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-avatar>
<q-icon name="bolt" size="md"></q-icon>
</q-avatar>
</q-item-section>
<q-item-section> Payment </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
size="md"
disable
:label="
formData.lnbits_user_activation_by_payment
? $t('enabled')
: $t('disabled')
"
v-model="formData.lnbits_user_activation_by_payment"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-card-section>
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon
name="info"
color="orange-4"
size="18px"
class="q-mr-xs"
></q-icon>
Users will need to make a payment during registration to
activate their account.
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-avatar>
<q-icon name="email"></q-icon>
</q-avatar>
</q-item-section>
<q-item-section> Email </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
disable
size="md"
:label="
formData.lnbits_user_activation_by_email
? $t('enabled')
: $t('disabled')
"
v-model="formData.lnbits_user_activation_by_email"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-card-section>
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon
name="info"
color="orange-4"
size="18px"
class="q-mr-xs"
></q-icon>
Users will receive a confirmation email.
</div>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</div>
</div>
</template> </template>
@@ -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 -24
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,8 @@
<span>OFFLINE</span> <span>OFFLINE</span>
</q-badge> </q-badge>
<lnbits-language-dropdown <lnbits-language-dropdown></lnbits-language-dropdown>
@language-changed="handleLanguageChanged({locale: $event})" <q-btn-dropdown v-if="g.user" flat rounded size="sm" class="q-pl-sm">
></lnbits-language-dropdown>
<q-btn-dropdown v-if="g.user" flat rounded size="md" 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 !== ''"
@@ -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
+29 -62
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>
+1 -1
View File
@@ -178,7 +178,7 @@
" "
:clickable="!!reviewsUrl" :clickable="!!reviewsUrl"
@click="openReviews(extension)" @click="openReviews(extension)"
></lnbits-extension-rating> />
<q-btn-group size="xs" style="margin: 5px 0"> <q-btn-group size="xs" style="margin: 5px 0">
<q-btn <q-btn
v-if="extension.hasFreeRelease" v-if="extension.hasFreeRelease"
-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 -29
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,13 +54,12 @@
<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"
v-model:password_2="passwordRepeat" v-model:password_2="passwordRepeat"
v-model:invitation-code="invitationCode"
v-model:reset-key="reset_key" v-model:reset-key="reset_key"
@login="login" @login="login"
@register="register" @register="register"
@@ -74,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?
@@ -86,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>
@@ -107,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"
@@ -120,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
@@ -158,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>
+5 -33
View File
@@ -218,26 +218,6 @@
:label="$t('update_account')" :label="$t('update_account')"
class="q-ml-md" class="q-ml-md"
></q-btn> ></q-btn>
<q-btn
v-if="activeUser.data.id"
outline
color="primary"
class="q-ml-md"
>
<q-toggle
size="xs"
color="secondary"
v-model="activeUser.data.admin"
@update:model-value="toggleAdmin(activeUser.data.id)"
:label="
activeUser.data.admin
? $t('revoke_admin')
: $t('make_user_admin')
"
>
</q-toggle>
</q-btn>
<q-btn <q-btn
v-else v-else
@click="createUser()" @click="createUser()"
@@ -610,9 +590,9 @@
<q-btn <q-btn
@click="showAccountPage(props.row.id)" @click="showAccountPage(props.row.id)"
round round
:icon="props.row.is_admin ? 'admin_panel_settings' : 'edit'" icon="edit"
size="sm" size="sm"
:color="props.row.is_admin ? 'primary' : 'secondary'" color="secondary"
class="q-ml-xs" class="q-ml-xs"
> >
<q-tooltip> <q-tooltip>
@@ -625,18 +605,10 @@
size="xs" size="xs"
v-if="!props.row.is_super_user" v-if="!props.row.is_super_user"
color="secondary" color="secondary"
v-model="props.row.activated" v-model="props.row.is_admin"
@update:model-value="toggleUserActivated(props.row.id)" @update:model-value="toggleAdmin(props.row.id)"
> >
<q-tooltip <q-tooltip>Toggle Admin</q-tooltip>
><span
v-text="
props.row.activated
? $t('deactivate')
: $t('activate')
"
></span
></q-tooltip>
</q-toggle> </q-toggle>
<q-btn <q-btn
round round
+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 %}
+4 -10
View File
@@ -292,8 +292,7 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
async def btc_price(currency: str) -> float: async def btc_price(currency: str) -> float:
rates = await btc_rates(currency) rates = await btc_rates(currency)
if not rates: if not rates:
logger.warning("Could not fetch any Bitcoin price.") raise ValueError("Could not fetch any Bitcoin price.")
return 0.0
elif len(rates) == 1: elif len(rates) == 1:
logger.warning("Could only fetch one Bitcoin price.") logger.warning("Could only fetch one Bitcoin price.")
@@ -307,8 +306,7 @@ async def get_fiat_rate_and_price_satoshis(currency: str) -> tuple[float, float]
f"btc-price-{currency}", f"btc-price-{currency}",
settings.lnbits_exchange_rate_cache_seconds, settings.lnbits_exchange_rate_cache_seconds,
) )
rate = float(100_000_000 / price) if price > 0 else 0.0 return float(100_000_000 / price), price
return rate, price
async def get_fiat_rate_satoshis(currency: str) -> float: async def get_fiat_rate_satoshis(currency: str) -> float:
@@ -318,13 +316,9 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int: async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
rate = await get_fiat_rate_satoshis(currency) rate = await get_fiat_rate_satoshis(currency)
if rate > 0: return int(amount * (rate))
return int(amount * rate)
raise ValueError(f"Could not get exchange rate for {currency}.")
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float: async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
rate = await get_fiat_rate_satoshis(currency) rate = await get_fiat_rate_satoshis(currency)
if rate > 0: return float(amount / rate)
return float(amount / rate)
raise ValueError(f"Could not get exchange rate for {currency}.")
-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}")
+98 -105
View File
@@ -1,7 +1,6 @@
import asyncio import asyncio
import hashlib import hashlib
import json import json
import os
import shutil import shutil
import subprocess import subprocess
import uuid import uuid
@@ -81,6 +80,34 @@ class LightsparkSparkWallet(Wallet):
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Error closing wallet connection: {e}") logger.warning(f"Error closing wallet connection: {e}")
async def _request(
self, method: str, path: str, json_data: dict[str, Any] | None = None
) -> dict[str, Any]:
error_message = None
try:
r = await self.client.request(method, path, json=json_data)
r.raise_for_status()
j = r.json()
except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as exc:
if isinstance(exc, httpx.HTTPStatusError) and exc.response is not None:
try:
error_json = exc.response.json()
if "error" in error_json:
error_message = error_json["error"]
except Exception as json_exc:
logger.error(
f"Failed to parse Spark error response as JSON: {json_exc}"
)
raise SparkSidecarError(
error_message or f"Spark sidecar request error: '{exc}'"
) from exc
if error_message or j.get("error"):
raise SparkSidecarError(
error_message or f"Spark sidecar error: {j['error']}"
)
return j
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
try: try:
res = await self._request("POST", "/v1/balance") res = await self._request("POST", "/v1/balance")
@@ -92,7 +119,6 @@ class LightsparkSparkWallet(Wallet):
return StatusResponse("Spark sidecar: missing balance.", 0) return StatusResponse("Spark sidecar: missing balance.", 0)
return StatusResponse(None, int(balance_sats) * 1000) return StatusResponse(None, int(balance_sats) * 1000)
except Exception as e: except Exception as e:
logger.warning(e)
return StatusResponse(f"Spark sidecar status error: {e}", 0) return StatusResponse(f"Spark sidecar status error: {e}", 0)
async def create_invoice( async def create_invoice(
@@ -123,9 +149,8 @@ class LightsparkSparkWallet(Wallet):
bolt11 = res.get("payment_request") bolt11 = res.get("payment_request")
checking_id = res.get("checking_id") checking_id = res.get("checking_id")
if not bolt11 or not checking_id: if not bolt11 or not checking_id:
return InvoiceResponse( raise SparkSidecarError(
ok=False, "Spark sidecar invoice response missing fields."
error_message="Spark sidecar invoice response missing fields.",
) )
self.pending_invoices.append(checking_id) self.pending_invoices.append(checking_id)
@@ -133,7 +158,7 @@ class LightsparkSparkWallet(Wallet):
ok=True, ok=True,
payment_request=bolt11, payment_request=bolt11,
checking_id=checking_id, checking_id=checking_id,
preimage=res.get("preimage", None), preimage=res.get("preimage"),
) )
except Exception as e: except Exception as e:
return InvoiceResponse(ok=False, error_message=str(e)) return InvoiceResponse(ok=False, error_message=str(e))
@@ -145,8 +170,7 @@ class LightsparkSparkWallet(Wallet):
payment_hash = None payment_hash = None
try: try:
payment_hash = bolt11_decode(bolt11).payment_hash payment_hash = bolt11_decode(bolt11).payment_hash
except Exception as exc: except Exception:
logger.warning(exc)
payment_hash = None payment_hash = None
payload = { payload = {
"bolt11": bolt11, "bolt11": bolt11,
@@ -156,9 +180,8 @@ class LightsparkSparkWallet(Wallet):
res = await self._request("POST", "/v1/payments", payload) res = await self._request("POST", "/v1/payments", payload)
checking_id = payment_hash or res.get("checking_id") checking_id = payment_hash or res.get("checking_id")
if not checking_id: if not checking_id:
return PaymentResponse( raise SparkSidecarError(
ok=False, "Spark sidecar payment response missing checking_id."
error_message="Spark sidecar payment response missing checking_id.",
) )
status = res.get("status") status = res.get("status")
fee_msat = res.get("fee_msat") fee_msat = res.get("fee_msat")
@@ -182,8 +205,7 @@ class LightsparkSparkWallet(Wallet):
if not status: if not status:
return PaymentPendingStatus() return PaymentPendingStatus()
return self._map_invoice_status(status) return self._map_invoice_status(status)
except Exception as exc: except Exception:
logger.warning(exc)
return PaymentPendingStatus() return PaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> PaymentStatus: async def get_payment_status(self, checking_id: str) -> PaymentStatus:
@@ -203,10 +225,69 @@ class LightsparkSparkWallet(Wallet):
if mapped.failed: if mapped.failed:
return PaymentFailedStatus() return PaymentFailedStatus()
return PaymentPendingStatus() return PaymentPendingStatus()
except Exception as exc: except Exception:
logger.warning(exc)
return PaymentPendingStatus() return PaymentPendingStatus()
def _map_invoice_status(self, status: str) -> PaymentStatus:
success = {
"LIGHTNING_PAYMENT_RECEIVED",
"TRANSFER_COMPLETED",
"PAYMENT_PREIMAGE_RECOVERED",
}
failed = {
"TRANSFER_FAILED",
"PAYMENT_PREIMAGE_RECOVERING_FAILED",
"REFUND_SIGNING_FAILED",
"REFUND_SIGNING_COMMITMENTS_QUERYING_FAILED",
"TRANSFER_CREATION_FAILED",
}
if status in success:
return PaymentSuccessStatus()
if status in failed:
return PaymentFailedStatus()
return PaymentPendingStatus()
def _map_payment_status(self, status: str) -> PaymentStatus:
success = {
"LIGHTNING_PAYMENT_SUCCEEDED",
"TRANSFER_COMPLETED",
"PREIMAGE_PROVIDED",
}
failed = {
"LIGHTNING_PAYMENT_FAILED",
"TRANSFER_FAILED",
"PREIMAGE_PROVIDING_FAILED",
"USER_TRANSFER_VALIDATION_FAILED",
"USER_SWAP_RETURN_FAILED",
}
if status in success:
return PaymentSuccessStatus()
if status in failed:
return PaymentFailedStatus()
return PaymentPendingStatus()
def _map_payment_ok(self, status: str) -> bool | None:
mapped = self._map_payment_status(status)
if mapped.success:
return True
if mapped.failed:
return False
return None
async def _poll_pending_invoices(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
for invoice in list(self.pending_invoices):
try:
status = await self.get_invoice_status(invoice)
if status.paid:
yield invoice
self.pending_invoices.remove(invoice)
elif status.failed:
self.pending_invoices.remove(invoice)
except Exception as exc:
logger.error(f"could not get status of invoice {invoice}: '{exc}' ")
await asyncio.sleep(5)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
stream_path = "/v1/invoices/stream" stream_path = "/v1/invoices/stream"
while settings.lnbits_running: while settings.lnbits_running:
@@ -237,48 +318,6 @@ class LightsparkSparkWallet(Wallet):
) )
await asyncio.sleep(5) await asyncio.sleep(5)
async def _request(
self, method: str, path: str, json_data: dict[str, Any] | None = None
) -> dict[str, Any]:
error_message = None
try:
r = await self.client.request(method, path, json=json_data)
r.raise_for_status()
j = r.json()
except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as exc:
if isinstance(exc, httpx.HTTPStatusError) and exc.response is not None:
try:
error_json = exc.response.json()
if "error" in error_json:
error_message = error_json["error"]
except Exception as json_exc:
logger.error(
f"Failed to parse Spark error response as JSON: {json_exc}"
)
raise SparkSidecarError(
error_message or f"Spark sidecar request error: '{exc}'"
) from exc
if error_message or j.get("error"):
raise SparkSidecarError(
error_message or f"Spark sidecar error: {j['error']}"
)
return j
async def _poll_pending_invoices(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
for invoice in list(self.pending_invoices):
try:
status = await self.get_invoice_status(invoice)
if status.paid:
yield invoice
self.pending_invoices.remove(invoice)
elif status.failed:
self.pending_invoices.remove(invoice)
except Exception as exc:
logger.error(f"could not get status of invoice {invoice}: '{exc}' ")
await asyncio.sleep(5)
async def _start_sidecar(self): async def _start_sidecar(self):
logger.info("Starting Spark sidecar") logger.info("Starting Spark sidecar")
node_path = shutil.which("node") node_path = shutil.which("node")
@@ -352,8 +391,6 @@ class LightsparkSparkWallet(Wallet):
zip_path = Path(self._sidecar_path, f"{repo}.zip") zip_path = Path(self._sidecar_path, f"{repo}.zip")
logger.info(f"⏳ Downloading Spark sidecar to {zip_path}") logger.info(f"⏳ Downloading Spark sidecar to {zip_path}")
Path(zip_path).parent.mkdir(parents=True, exist_ok=True) Path(zip_path).parent.mkdir(parents=True, exist_ok=True)
if zip_path.is_file():
os.remove(zip_path)
await asyncio.to_thread( await asyncio.to_thread(
download_url, download_url,
@@ -369,6 +406,8 @@ class LightsparkSparkWallet(Wallet):
self._sidecar_path, self._sidecar_path,
) )
logger.info("✅ Extracted Spark sidecar.") logger.info("✅ Extracted Spark sidecar.")
shutil.rmtree(zip_path, ignore_errors=True)
# todo: remove zip
def _log_process_output(self, process: subprocess.Popen): def _log_process_output(self, process: subprocess.Popen):
if process.stdout: if process.stdout:
@@ -376,49 +415,3 @@ class LightsparkSparkWallet(Wallet):
logger.warning(f"[Lightspark]: {line}", end="") logger.warning(f"[Lightspark]: {line}", end="")
else: else:
logger.error(" No output captured for Spark sidecar.") logger.error(" No output captured for Spark sidecar.")
def _map_invoice_status(self, status: str) -> PaymentStatus:
success = {
"LIGHTNING_PAYMENT_RECEIVED",
"TRANSFER_COMPLETED",
"PAYMENT_PREIMAGE_RECOVERED",
}
failed = {
"TRANSFER_FAILED",
"PAYMENT_PREIMAGE_RECOVERING_FAILED",
"REFUND_SIGNING_FAILED",
"REFUND_SIGNING_COMMITMENTS_QUERYING_FAILED",
"TRANSFER_CREATION_FAILED",
}
if status in success:
return PaymentSuccessStatus()
if status in failed:
return PaymentFailedStatus()
return PaymentPendingStatus()
def _map_payment_status(self, status: str) -> PaymentStatus:
success = {
"LIGHTNING_PAYMENT_SUCCEEDED",
"TRANSFER_COMPLETED",
"PREIMAGE_PROVIDED",
}
failed = {
"LIGHTNING_PAYMENT_FAILED",
"TRANSFER_FAILED",
"PREIMAGE_PROVIDING_FAILED",
"USER_TRANSFER_VALIDATION_FAILED",
"USER_SWAP_RETURN_FAILED",
}
if status in success:
return PaymentSuccessStatus()
if status in failed:
return PaymentFailedStatus()
return PaymentPendingStatus()
def _map_payment_ok(self, status: str) -> bool | None:
mapped = self._map_payment_status(status)
if mapped.success:
return True
if mapped.failed:
return False
return None
Generated
+1 -1
View File
@@ -4638,4 +4638,4 @@ migration = ["psycopg2-binary"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10,<3.13" python-versions = ">=3.10,<3.13"
content-hash = "f63a5be62359f837513e01a8b0e5282a3b4509ea97439ba0c8226ccf3c158ceb" content-hash = "6c17d723baa390c36ecf6f861166878f2619acd47f878cf9253c5614ba25b9ea"
+1
View File
@@ -9,6 +9,7 @@ readme = "README.md"
dependencies = [ dependencies = [
"bech32==1.2.0", "bech32==1.2.0",
"click==8.3.1", "click==8.3.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.2",
+9 -145
View File
@@ -297,148 +297,6 @@ async def test_register_ok(http_client: AsyncClient):
), f"Expected 1 default wallet, not {len(user.wallets)}." ), f"Expected 1 default wallet, not {len(user.wallets)}."
@pytest.mark.anyio
async def test_register_no_activation_code(
http_client: AsyncClient, settings: Settings
):
settings.lnbits_require_user_activation = True
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
json={
"username": f"u21.{tiny_id}",
"password": "secret1234",
"password_repeat": "secret1234",
"email": f"u21.{tiny_id}@lnbits.com",
},
)
assert response.status_code == 400
assert response.json().get("detail") == "No activation method provided."
settings.lnbits_user_activation_by_invitation_code = True
response = await http_client.post(
"/api/v1/auth/register",
json={
"username": f"u21.{tiny_id}",
"password": "secret1234",
"password_repeat": "secret1234",
"email": f"u21.{tiny_id}@lnbits.com",
},
)
assert response.status_code == 400, "User creation blocked without activation code."
assert response.json().get("detail") == "Invitation code cannot be empty."
@pytest.mark.anyio
async def test_register_invalid_activation_code(
http_client: AsyncClient, settings: Settings
):
settings.lnbits_require_user_activation = True
settings.lnbits_user_activation_by_invitation_code = True
settings.lnbits_register_reusable_activation_code = "foo"
settings.lnbits_register_one_time_activation_codes = ["baz", "qux"]
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
json={
"username": f"u21.{tiny_id}",
"password": "secret1234",
"password_repeat": "secret1234",
"email": f"u21.{tiny_id}@lnbits.com",
"invitation_code": "bar",
},
)
assert response.status_code == 400
assert response.json().get("detail") == "Invalid invitation code."
@pytest.mark.anyio
async def test_register_reusable_activation_code(
http_client: AsyncClient, settings: Settings
):
settings.lnbits_require_user_activation = True
settings.lnbits_user_activation_by_invitation_code = True
settings.lnbits_register_reusable_activation_code = "foo"
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
json={
"username": f"u21.{tiny_id}",
"password": "secret1234",
"password_repeat": "secret1234",
"email": f"u21.{tiny_id}@lnbits.com",
"invitation_code": "foo",
},
)
assert response.status_code == 200, "User created with reusable code."
assert response.json().get("access_token") is not None
# Register again with the same code
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
json={
"username": f"u21.{tiny_id}",
"password": "secret1234",
"password_repeat": "secret1234",
"email": f"u21.{tiny_id}@lnbits.com",
"invitation_code": "foo",
},
)
assert response.status_code == 200, "User created with reusable code."
assert response.json().get("access_token") is not None
@pytest.mark.anyio
async def test_register_one_time_activation_code(
http_client: AsyncClient, settings: Settings
):
settings.lnbits_require_user_activation = True
settings.lnbits_user_activation_by_invitation_code = True
settings.lnbits_register_reusable_activation_code = "foo"
settings.lnbits_register_one_time_activation_codes = ["baz", "qux"]
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
json={
"username": f"u21.{tiny_id}",
"password": "secret1234",
"password_repeat": "secret1234",
"email": f"u21.{tiny_id}@lnbits.com",
"invitation_code": "baz",
},
)
assert response.status_code == 200, "User created with one-time code."
assert response.json().get("access_token") is not None
# Register again with the same code
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
json={
"username": f"u21.{tiny_id}",
"password": "secret1234",
"password_repeat": "secret1234",
"email": f"u21.{tiny_id}@lnbits.com",
"invitation_code": "baz",
},
)
assert response.status_code == 400, "Invalid invitation code."
assert response.json().get("detail") == "Invalid invitation code."
@pytest.mark.anyio @pytest.mark.anyio
async def test_register_email_twice(http_client: AsyncClient): async def test_register_email_twice(http_client: AsyncClient):
tiny_id = shortuuid.uuid()[:8] tiny_id = shortuuid.uuid()[:8]
@@ -2158,7 +2016,9 @@ async def test_api_update_user_labels(http_client: AsyncClient):
] ]
data = UpdateUser(user_id=user.id, username=f"u{tiny_id}", extra=user.extra) data = UpdateUser(user_id=user.id, username=f"u{tiny_id}", extra=user.extra)
assert data.extra assert data.extra
response = await http_client.patch("/api/v1/auth?usr=" + user.id, json=data.dict()) response = await http_client.put(
"/api/v1/auth/update?usr=" + user.id, json=data.dict()
)
assert response.status_code == 200 assert response.status_code == 200
user_data = response.json() user_data = response.json()
assert len(user_data["extra"]["labels"]) == 2 assert len(user_data["extra"]["labels"]) == 2
@@ -2168,14 +2028,18 @@ async def test_api_update_user_labels(http_client: AsyncClient):
assert user_data["extra"]["labels"][1]["color"] == "#00FF00" assert user_data["extra"]["labels"][1]["color"] == "#00FF00"
data.extra.labels = [] data.extra.labels = []
response = await http_client.patch("/api/v1/auth?usr=" + user.id, json=data.dict()) response = await http_client.put(
"/api/v1/auth/update?usr=" + user.id, json=data.dict()
)
assert response.status_code == 200 assert response.status_code == 200
user_data = response.json() user_data = response.json()
assert len(user_data["extra"]["labels"]) == 0 assert len(user_data["extra"]["labels"]) == 0
json_data = data.dict() json_data = data.dict()
json_data["extra"] = {"labels": [{"name": "label + 01", "color": "#FF0000"}]} json_data["extra"] = {"labels": [{"name": "label + 01", "color": "#FF0000"}]}
response = await http_client.patch("/api/v1/auth?usr=" + user.id, json=json_data) response = await http_client.put(
"/api/v1/auth/update?usr=" + user.id, json=json_data
)
assert response.status_code == 400 assert response.status_code == 400
data = response.json() data = response.json()
-126
View File
@@ -1,13 +1,11 @@
from typing import Any from typing import Any
from uuid import uuid4 from uuid import uuid4
import jwt
import pytest import pytest
import shortuuid import shortuuid
from httpx import AsyncClient from httpx import AsyncClient
from lnbits.core.crud.wallets import get_wallets from lnbits.core.crud.wallets import get_wallets
from lnbits.core.models import AccessTokenPayload, Payment
from lnbits.core.models.users import Account, User from lnbits.core.models.users import Account, User
from lnbits.core.services.users import create_user_account from lnbits.core.services.users import create_user_account
from lnbits.settings import Settings from lnbits.settings import Settings
@@ -612,127 +610,3 @@ async def test_delete_and_undelete_wallet(http_client: AsyncClient, superuser_to
undeleted_wallet = next((w for w in wallets if w.id == wallet_id), None) undeleted_wallet = next((w for w in wallets if w.id == wallet_id), None)
assert undeleted_wallet is not None assert undeleted_wallet is not None
assert undeleted_wallet.deleted is False assert undeleted_wallet.deleted is False
@pytest.mark.anyio
async def test_user_activation(
http_client: AsyncClient, invoice: Payment, settings: Settings, superuser_token: str
):
# Register a new user
username = f"u21.{shortuuid.uuid()[:8]}"
response = await http_client.post(
"/api/v1/auth/register",
json={
"username": username,
"password": "secret1234",
"password_repeat": "secret1234",
"email": f"{username}@lnbits.com",
},
)
access_token = response.json().get("access_token")
assert response.status_code == 200, "User created."
assert response.json().get("access_token") is not None
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
access_token_payload = AccessTokenPayload(**payload)
user_id = access_token_payload.usr
assert user_id is not None
# Login works
response = await http_client.post(
"/api/v1/auth", json={"username": username, "password": "secret1234"}
)
assert response.status_code == 200, "User logs in OK"
# Deactivate the user
respones = await http_client.put(
f"/users/api/v1/user/{user_id}/activate",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert respones.status_code == 200, "User deactivated."
assert respones.json().get("message") == "User deactivated."
# Login should now fail
response = await http_client.post(
"/api/v1/auth", json={"username": username, "password": "secret1234"}
)
assert response.status_code == 401
assert response.json().get("detail") == "Invalid credentials."
response = await http_client.get(
"/api/v1/auth",
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == 401
assert response.json().get("detail") == "User not found."
wallets = await get_wallets(user_id=user_id)
assert len(wallets) == 1, "User's wallet still exists."
wallet = wallets[0]
response = await http_client.get(
"/api/v1/payments/paginated",
params={"limit": 2},
headers={"x-Api-Key": wallet.inkey},
)
assert response.status_code == 404
assert response.json().get("detail") == "Wallet not found."
response = await http_client.post(
"/api/v1/payments",
json={
"out": False,
"amount": 1000,
"memo": "test payment",
},
headers={"x-Api-Key": wallet.inkey},
)
assert response.status_code == 404
assert response.json().get("detail") == "Wallet not found."
data = {"out": True, "bolt11": invoice.bolt11}
response = await http_client.post(
"/api/v1/payments",
json=data,
headers={"x-Api-Key": wallet.adminkey},
)
assert response.status_code == 404
assert response.json().get("detail") == "Wallet not found."
# Reactivate the user
response = await http_client.put(
f"/users/api/v1/user/{user_id}/activate",
headers={"Authorization": f"Bearer {superuser_token}"},
)
print("### response", response.text)
assert response.status_code == 200
assert response.json().get("message") == "User activated."
# Login should now pass
response = await http_client.post(
"/api/v1/auth", json={"username": username, "password": "secret1234"}
)
assert response.status_code == 200, "User logs in OK again."
response = await http_client.get(
"/api/v1/payments/paginated",
params={"limit": 2},
headers={"x-Api-Key": wallet.inkey},
)
assert response.status_code == 200
response = await http_client.post(
"/api/v1/payments",
json={
"out": False,
"amount": 1000,
"memo": "test payment",
},
headers={"x-Api-Key": wallet.inkey},
)
assert response.status_code == 201
-4
View File
@@ -341,7 +341,3 @@ def _settings_cleanup(settings: Settings):
settings.lnbits_max_outgoing_payment_amount_sats = 10_000_000_100 settings.lnbits_max_outgoing_payment_amount_sats = 10_000_000_100
settings.lnbits_max_incoming_payment_amount_sats = 10_000_000_200 settings.lnbits_max_incoming_payment_amount_sats = 10_000_000_200
settings.stripe_limits = FiatProviderLimits() settings.stripe_limits = FiatProviderLimits()
settings.lnbits_require_user_activation = False
settings.lnbits_user_activation_by_invitation_code = False
settings.lnbits_register_reusable_activation_code = ""
settings.lnbits_register_one_time_activation_codes = []
+2 -6
View File
@@ -7,11 +7,7 @@ from lnbits import bolt11
from lnbits.core.crud import get_standalone_payment, update_payment from lnbits.core.crud import get_standalone_payment, update_payment
from lnbits.core.crud.wallets import create_wallet, get_wallet from lnbits.core.crud.wallets import create_wallet, get_wallet
from lnbits.core.models import CreateInvoice, Payment, PaymentState from lnbits.core.models import CreateInvoice, Payment, PaymentState
from lnbits.core.services import ( from lnbits.core.services import fee_reserve_total, get_balance_delta
check_payment_status,
fee_reserve_total,
get_balance_delta,
)
from lnbits.core.services.payments import pay_invoice, update_wallet_balance from lnbits.core.services.payments import pay_invoice, update_wallet_balance
from lnbits.core.services.users import create_user_account from lnbits.core.services.users import create_user_account
from lnbits.exceptions import PaymentError from lnbits.exceptions import PaymentError
@@ -359,7 +355,7 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
assert payment_db_after_settlement is not None assert payment_db_after_settlement is not None
# payment is failed # payment is failed
status = await check_payment_status(payment_db_after_settlement) status = await payment_db_after_settlement.check_status()
assert not status.paid assert not status.paid
assert status.failed assert status.failed
+3 -3
View File
@@ -12,7 +12,7 @@ from lnbits.core.crud.wallets import create_wallet
from lnbits.core.models.payments import CreateInvoice, PaymentState from lnbits.core.models.payments import CreateInvoice, PaymentState
from lnbits.core.models.users import User from lnbits.core.models.users import User
from lnbits.core.models.wallets import Wallet from lnbits.core.models.wallets import Wallet
from lnbits.core.services import check_payment_status, payments from lnbits.core.services import payments
from lnbits.core.services.fiat_providers import ( from lnbits.core.services.fiat_providers import (
check_stripe_signature, check_stripe_signature,
handle_fiat_payment_confirmation, handle_fiat_payment_confirmation,
@@ -221,7 +221,7 @@ async def test_create_wallet_fiat_invoice_success(
assert payment.checking_id.startswith("fiat_stripe_") assert payment.checking_id.startswith("fiat_stripe_")
assert payment.fee <= 0 assert payment.fee <= 0
status = await check_payment_status(payment) status = await payment.check_status()
assert status.success is False assert status.success is False
assert status.pending is True assert status.pending is True
@@ -230,7 +230,7 @@ async def test_create_wallet_fiat_invoice_success(
"lnbits.fiat.StripeWallet.get_invoice_status", "lnbits.fiat.StripeWallet.get_invoice_status",
AsyncMock(return_value=fiat_mock_status), AsyncMock(return_value=fiat_mock_status),
) )
status = await check_payment_status(payment) status = await payment.check_status()
assert status.paid is True assert status.paid is True
assert status.success is True assert status.success is True
Generated
+2
View File
@@ -1256,6 +1256,7 @@ dependencies = [
{ name = "bech32" }, { name = "bech32" },
{ name = "bolt11" }, { name = "bolt11" },
{ name = "click" }, { name = "click" },
{ name = "ecdsa" },
{ name = "embit" }, { name = "embit" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "fastapi-sso" }, { name = "fastapi-sso" },
@@ -1339,6 +1340,7 @@ requires-dist = [
{ name = "breez-sdk", marker = "extra == 'breez'", specifier = "==0.8.0" }, { name = "breez-sdk", marker = "extra == 'breez'", specifier = "==0.8.0" },
{ name = "breez-sdk-liquid", marker = "extra == 'breez'", specifier = "==0.11.11" }, { name = "breez-sdk-liquid", marker = "extra == 'breez'", specifier = "==0.11.11" },
{ name = "click", specifier = "==8.3.1" }, { name = "click", specifier = "==8.3.1" },
{ name = "ecdsa", specifier = "==0.19.1" },
{ name = "embit", specifier = "==0.8.0" }, { name = "embit", specifier = "==0.8.0" },
{ name = "fastapi", specifier = "==0.116.1" }, { name = "fastapi", specifier = "==0.116.1" },
{ name = "fastapi-sso", specifier = "==0.19.0" }, { name = "fastapi-sso", specifier = "==0.19.0" },