Compare commits

...
Author SHA1 Message Date
Vlad Stan c5388e23a9 fix: settings reset 2026-02-04 14:05:19 +02:00
Vlad Stan d01893f577 test: one time code 2026-02-04 14:04:25 +02:00
Vlad Stan 1fc15cde81 test: invalid code, reusable code 2026-02-04 13:57:46 +02:00
Vlad Stan c711be4d60 test: no code provided 2026-02-04 13:48:46 +02:00
Vlad Stan ccb9dda164 fix: handle empty strings better 2026-02-04 13:11:40 +02:00
Vlad Stan 100bc45185 chore: code clean-up 2026-02-03 18:11:04 +02:00
Vlad Stan 222d1c8990 fix: remove nostr message 2026-02-03 18:11:04 +02:00
Vlad Stan 69c414fabf feat: better wording 2026-02-03 18:11:04 +02:00
Vlad Stan b33aa41b9a feat: check invitation code 2026-02-03 18:11:04 +02:00
Vlad Stan 68a36dfd18 feat: update settings 2026-02-03 18:11:04 +02:00
Vlad Stan 84a958af6e feat: send invitation code to backend 2026-02-03 18:11:04 +02:00
Vlad Stan 7db645d388 feat: add confirmation options UI 2026-02-03 18:11:04 +02:00
Vlad Stan 30e61eacf3 feat: configure ui 2026-02-03 18:11:04 +02:00
Vlad Stan de496ed9e8 feat: configure activation codes 2026-02-03 18:11:04 +02:00
Vlad Stan 8cb35d9aac feat: add some info 2026-02-03 18:11:04 +02:00
Vlad Stan d662b03370 refactor: reorder fields 2026-02-03 18:11:04 +02:00
Vlad Stan ef78ca7db3 feat: add ui config 2026-02-03 18:11:04 +02:00
Vlad Stan c97c5842ad refactor: better query 2026-02-03 18:10:41 +02:00
Vlad Stan abce6196de refactor: code cleanup 2026-02-03 18:10:41 +02:00
Vlad Stan e7d3be9854 chore: clean-up 2026-02-03 18:10:41 +02:00
Vlad Stan e9dc8c775d chore: bundle 2026-02-03 18:10:41 +02:00
Vlad Stan 31a2d68285 refactor: simplify queries 2026-02-03 18:10:41 +02:00
Vlad Stan bf1ad185b9 refactor: use normal update 2026-02-03 18:10:41 +02:00
Vlad Stan 62de35541c test: add more check 2026-02-03 18:10:41 +02:00
Vlad Stan 1a61435868 test: login after user disabled does not work 2026-02-03 18:10:41 +02:00
Vlad Stan 03ba28d0cb fix: column selection 2026-02-03 18:10:41 +02:00
Vlad Stan 2b260bfdaf fix: better message 2026-02-03 18:10:41 +02:00
Vlad Stan 30ab42c0b2 feat: add back toggle admin 2026-02-03 18:10:41 +02:00
Vlad Stan f603026d5e fix: only fetch keys for activated users 2026-02-03 18:10:41 +02:00
Vlad Stan 84e6b3a9a8 fix: clear cache 2026-02-03 18:10:41 +02:00
Vlad Stan 3aafc0724f feat: clear cache on user deactivation 2026-02-03 18:10:41 +02:00
Vlad Stan b978ce11fc feat: basic account activate/deactivate 2026-02-03 18:10:41 +02:00
dni ⚡andGitHub 656c6cac5b hotfix: websocket with old balance was sent. (#3759) 2026-02-03 12:23:49 +01:00
45e773b66f feat: FIRST_INSTALL_TOKEN to help services like Umbrel secure the first_install endpoint (#3751)
Co-authored-by: dni  <office@dnilabs.com>
2026-01-30 11:21:52 +01:00
f692ac6f12 refactor: payment.check_status() into check_payment_status(payment) (#3747)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-01-30 11:18:17 +01:00
dni ⚡andGitHub 78202a027b fix: uiCustomisation JS Error (#3752) 2026-01-30 09:27:54 +01:00
dni ⚡andGitHub 28fa0906a6 refactor: g.settings with a PublicSettings class for mapping, for reactive WINDOW_SETTINGS (#3644) 2026-01-30 09:19:01 +01:00
57 changed files with 1358 additions and 424 deletions
+4
View File
@@ -6,6 +6,10 @@
# 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.
# === 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 ===
DEBUG=False
+75 -21
View File
@@ -4,10 +4,12 @@ from typing import Any
from uuid import uuid4
from lnbits.core.crud.extensions import get_user_active_extensions_ids
from lnbits.core.crud.wallets import create_wallet, get_wallets
from lnbits.core.crud.wallets import clear_wallet_cache, create_wallet, get_wallets
from lnbits.core.db import db
from lnbits.core.models import UserAcls
from lnbits.db import Connection, Filters, Page
from lnbits.helpers import sha256s
from lnbits.utils.cache import cache
from ..models import (
Account,
@@ -41,6 +43,7 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
"DELETE from accounts WHERE id = :user",
{"user": user_id},
)
await clear_user_id_cache(user_id)
async def get_accounts(
@@ -69,6 +72,7 @@ async def get_accounts(
accounts.email,
accounts.pubkey,
accounts.external_id,
accounts.activated,
SUM(COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)) as balance_msat,
@@ -93,12 +97,18 @@ async def get_accounts(
)
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
async def get_account(
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
if len(user_id) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE id = :id",
{"id": user_id},
"""
SELECT * FROM accounts
WHERE id = :id AND (activated = true OR activated = :activated)
""",
{"id": user_id, "activated": active_only},
Account,
)
@@ -124,55 +134,79 @@ async def delete_accounts_no_wallets(
async def get_account_by_username(
username: str, conn: Connection | None = None
username: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
if len(username) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(username) = :username",
{"username": username.lower()},
"""
SELECT * FROM accounts
WHERE
LOWER(username) = :username
AND (activated = true OR activated = :activated)
""",
{"username": username.lower(), "activated": active_only},
Account,
)
async def get_account_by_pubkey(
pubkey: str, conn: Connection | None = None
pubkey: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
{"pubkey": pubkey.lower()},
"""
SELECT * FROM accounts
WHERE
LOWER(pubkey) = :pubkey
AND (activated = true OR activated = :activated)
""",
{"pubkey": pubkey.lower(), "activated": active_only},
Account,
)
async def get_account_by_email(
email: str, conn: Connection | None = None
email: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
if len(email) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(email) = :email",
{"email": email.lower()},
"""
SELECT * FROM accounts
WHERE
LOWER(email) = :email
AND (activated = true OR activated = :activated)
""",
{"email": email.lower(), "activated": active_only},
Account,
)
async def get_account_by_username_or_email(
username_or_email: str, conn: Connection | None = None
username_or_email: str,
active_only: bool = True,
conn: Connection | None = None,
) -> Account | None:
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE LOWER(email) = :value or LOWER(username) = :value
WHERE
(LOWER(email) = :value or LOWER(username) = :value)
AND (activated = true OR activated = :activated)
""",
{"value": username_or_email.lower()},
{"value": username_or_email.lower(), "activated": active_only},
Account,
)
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
async def get_user(
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:
account = await get_account(user_id, conn=conn)
account = await get_account(user_id, active_only, conn=conn)
if not account:
return None
return await get_user_from_account(account, conn=conn)
@@ -191,6 +225,7 @@ async def get_user_from_account(
return User(
id=account.id,
activated=account.activated,
email=account.email,
username=account.username,
pubkey=account.pubkey,
@@ -216,12 +251,31 @@ async def update_user_access_control_list(
async def get_user_access_control_lists(
user_id: str, conn: Connection | None = None
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> UserAcls:
user_acls = await (conn or db).fetchone(
"SELECT id, access_control_list FROM accounts WHERE id = :id",
{"id": user_id},
"""
SELECT id, access_control_list FROM accounts
WHERE id = :user_id AND (activated = true OR activated = :activated)
""",
{"user_id": user_id, "activated": active_only},
UserAcls,
)
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)
+20 -8
View File
@@ -50,7 +50,7 @@ async def delete_wallet(
deleted: bool = True,
conn: Connection | None = None,
) -> None:
_clear_wallet_cache(wallet_id)
clear_wallet_id_cache(wallet_id)
now = int(time())
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:
_clear_wallet_cache(wallet_id)
clear_wallet_id_cache(wallet_id)
await (conn or db).execute(
"DELETE FROM wallets WHERE id = :wallet",
{"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(
wallet_id: str, conn: Connection | None = None
) -> int | None:
_clear_wallet_cache(wallet_id)
clear_wallet_id_cache(wallet_id)
now = int(time())
result = await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
@@ -226,11 +226,14 @@ async def get_wallet_for_key(
) -> Wallet | None:
wallet = await (conn or db).fetchone(
"""
SELECT *, COALESCE((
SELECT wallets.*, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)
AS balance_msat FROM wallets
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
INNER JOIN accounts ON wallets.user = accounts.id
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
""",
{"key": key},
Wallet,
@@ -250,8 +253,11 @@ async def get_base_wallet_for_key(
) -> BaseWallet | None:
wallet = await (conn or db).fetchone(
"""
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
SELECT wallets.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
AND accounts.activated = true
""",
{"key": key},
BaseWallet,
@@ -294,8 +300,14 @@ async def get_total_balance(conn: Connection | None = None):
return row.get("balance", 0) or 0
def _clear_wallet_cache(wallet_id):
def clear_wallet_id_cache(wallet_id: str):
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
if cached_wallet:
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
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}")
+8
View File
@@ -856,3 +856,11 @@ async def m043_add_ui_customization_to_accounts(db: Connection):
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")
+10 -53
View File
@@ -6,23 +6,16 @@ from typing import Literal
from fastapi import Query
from lnurl import LnurlWithdrawResponse
from loguru import logger
from pydantic import BaseModel, Field, validator
from lnbits.db import FilterModel
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
)
from lnbits.utils.exchange_rates import allowed_currencies
from lnbits.wallets import get_funding_source
from lnbits.wallets.base import (
PaymentFailedStatus,
PaymentPendingStatus,
PaymentStatus,
PaymentSuccessStatus,
)
@@ -130,59 +123,23 @@ class Payment(BaseModel):
"fiat_"
)
# DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_status(
self, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
if self.is_internal:
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
logger.warning("payment.check_status() is deprecated.")
from lnbits.core.services.payments import check_payment_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(
self, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
if not self.is_internal:
return FiatPaymentPendingStatus()
if self.success:
return FiatPaymentSuccessStatus()
if self.failed:
return FiatPaymentFailedStatus()
logger.warning("payment.check_fiat_status() is deprecated.")
from lnbits.core.services.fiat_providers import check_fiat_status
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
return await check_fiat_status(self, skip_internal_payment_notifications)
class PaymentFilters(FilterModel):
+5
View File
@@ -181,6 +181,7 @@ class AccountId(BaseModel):
class Account(AccountId):
activated: bool = True
external_id: str | None = None # for external account linking
username: str | None = None
password_hash: str | None = None
@@ -241,6 +242,7 @@ class Account(AccountId):
class AccountOverview(Account):
activated: bool = True
transaction_count: int | None = 0
wallet_count: int | None = 0
balance_msat: int | None = 0
@@ -276,6 +278,7 @@ class AccountFilters(FilterModel):
class User(BaseModel):
id: str
activated: bool = True
created_at: datetime
updated_at: datetime
email: str | None = None
@@ -315,6 +318,7 @@ class RegisterUser(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20)
password: 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, min_length=1, max_length=256)
class CreateUser(BaseModel):
@@ -358,6 +362,7 @@ class UpdateSuperuserPassword(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20)
password: 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):
+4
View File
@@ -1,3 +1,4 @@
from .fiat_providers import check_fiat_status
from .funding_source import (
get_balance_delta,
switch_to_voidwallet,
@@ -7,6 +8,7 @@ from .notifications import enqueue_admin_notification, send_payment_notification
from .payments import (
calculate_fiat_amounts,
cancel_hold_invoice,
check_payment_status,
check_transaction_status,
check_wallet_limits,
create_fiat_invoice,
@@ -40,6 +42,8 @@ __all__ = [
"calculate_fiat_amounts",
"cancel_hold_invoice",
"check_admin_settings",
"check_fiat_status",
"check_payment_status",
"check_transaction_status",
"check_wallet_limits",
"check_webpush_settings",
+40
View File
@@ -12,6 +12,12 @@ from lnbits.core.models import CreatePayment, Payment, PaymentState
from lnbits.core.models.misc import SimpleStatus
from lnbits.db import Connection
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
)
from lnbits.settings import settings
@@ -29,6 +35,40 @@ async def handle_fiat_payment_confirmation(
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(
payload: bytes,
sig_header: str | None,
+40 -5
View File
@@ -25,6 +25,7 @@ from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount
from lnbits.wallets import fake_wallet, get_funding_source
from lnbits.wallets.base import (
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
@@ -47,6 +48,7 @@ from ..models import (
PaymentState,
Wallet,
)
from .fiat_providers import check_fiat_status
from .notifications import send_payment_notification_in_background
payment_lock = asyncio.Lock()
@@ -364,7 +366,7 @@ async def update_pending_payments(wallet_id: str):
async def update_pending_payment(
payment: Payment, conn: Connection | None = None
) -> Payment:
status = await payment.check_status()
status = await check_payment_status(payment)
if status.failed:
payment.status = PaymentState.FAILED
await update_payment(payment, conn=conn)
@@ -618,7 +620,29 @@ async def check_transaction_status(
if payment.status == PaymentState.SUCCESS.value:
return PaymentSuccessStatus(fee_msat=payment.fee)
return await payment.check_status()
return await check_payment_status(payment)
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(
@@ -752,7 +776,7 @@ async def _pay_internal_invoice(
await update_payment(internal_payment, conn=conn)
logger.success(f"internal payment successful {internal_payment.checking_id}")
send_payment_notification_in_background(wallet, payment)
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue
@@ -825,7 +849,8 @@ async def _pay_external_invoice(
payment = await update_payment_success_status(
payment, payment_response, conn=conn
)
send_payment_notification_in_background(wallet, payment)
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
logger.success(f"payment successful {payment_response.checking_id}")
payment.checking_id = payment_response.checking_id
@@ -868,7 +893,7 @@ async def _verify_external_payment(
raise PaymentError("Payment already paid.", status="success")
# payment failed
status = await payment.check_status()
status = await check_payment_status(payment)
if status.failed:
raise PaymentError(
"Payment is failed node, retrying is not possible.", status="failed"
@@ -1033,3 +1058,13 @@ async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
await update_payment(payment)
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,8 +3,10 @@ from uuid import uuid4
from loguru import logger
from lnbits.core.crud.settings import set_settings_field
from lnbits.core.db import db
from lnbits.core.models.extensions import UserExtension
from lnbits.core.models.users import RegisterUser
from lnbits.db import Connection
from lnbits.settings import (
EditableSettings,
@@ -192,3 +194,25 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
editable_settings = EditableSettings.from_dict(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.")
+18 -4
View File
@@ -26,7 +26,10 @@ from lnbits.core.models.users import (
UpdateAccessControlList,
)
from lnbits.core.services import create_user_account
from lnbits.core.services.users import update_user_account
from lnbits.core.services.users import (
check_register_activation_settings,
update_user_account,
)
from lnbits.decorators import (
access_token_payload,
check_account_exists,
@@ -86,6 +89,7 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
account = await get_account_by_username_or_email(data.username)
if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
return _auth_success_response(account.username, account.id, account.email)
@@ -94,7 +98,7 @@ async def nostr_login(request: Request) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
event = _nostr_nip98_event(request)
account = await get_account_by_pubkey(event["pubkey"])
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
if not account:
account = Account(
id=uuid4().hex,
@@ -102,6 +106,8 @@ async def nostr_login(request: Request) -> JSONResponse:
extra=UserExtra(provider="nostr"),
)
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)
@@ -357,12 +363,14 @@ async def register(data: RegisterUser) -> JSONResponse:
if not is_valid_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
if await get_account_by_username(data.username):
if await get_account_by_username(data.username, active_only=False):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
if data.email and not is_valid_email_address(data.email):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
await check_register_activation_settings(data)
account = Account(
id=uuid4().hex,
email=data.email,
@@ -503,6 +511,12 @@ async def update_ui_customization(
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
if not settings.first_install:
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
if settings.first_install_token:
if not data.first_install_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.")
if settings.first_install_token != data.first_install_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid first_install_token.")
account = await get_account(settings.super_user)
if not account:
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
@@ -521,7 +535,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
redirect_path = "/wallet"
account = await get_account_by_email(email)
account = await get_account_by_email(email, active_only=False)
if verified_user_id:
if account:
+6 -5
View File
@@ -9,6 +9,7 @@ from lnbits.core.crud.payments import (
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.payments import CreateInvoice
from lnbits.core.services.fiat_providers import (
check_fiat_status,
check_stripe_signature,
verify_paypal_webhook,
)
@@ -87,7 +88,7 @@ async def _handle_stripe_intent_session_completed(event: dict):
if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.")
return
await payment.check_fiat_status()
await check_fiat_status(payment)
async def _handle_stripe_checkout_session_completed(event: dict):
@@ -110,7 +111,7 @@ async def _handle_stripe_checkout_session_completed(event: dict):
payment = await get_standalone_payment(payment_hash)
if not payment:
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
await payment.check_fiat_status()
await check_fiat_status(payment)
async def _handle_stripe_subscription_invoice_paid(event: dict):
@@ -155,7 +156,7 @@ async def _handle_stripe_subscription_invoice_paid(event: dict):
),
)
await payment.check_fiat_status()
await check_fiat_status(payment)
async def _get_stripe_subscription_payment_options(
@@ -192,7 +193,7 @@ async def handle_paypal_event(event: dict):
if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.")
return
await payment.check_fiat_status()
await check_fiat_status(payment)
return
if event_type in (
@@ -247,7 +248,7 @@ async def _handle_paypal_subscription_payment(resource: dict):
),
)
await payment.check_fiat_status()
await check_fiat_status(payment)
def _paypal_extract_payment_hash(resource: dict) -> str | None:
+40 -2
View File
@@ -20,6 +20,7 @@ from lnbits.core.crud import (
update_admin_settings,
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.models import (
AccountFilters,
@@ -73,7 +74,7 @@ async def api_get_users(
summary="Get user by Id",
)
async def api_get_user(user_id: str) -> User:
user = await get_user(user_id)
user = await get_user(user_id, active_only=False)
if not user:
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
return user
@@ -197,7 +198,7 @@ async def api_users_reset_password(user_id: str) -> str:
return f"reset_key_{reset_key_b64}"
@users_router.get(
@users_router.put(
"/user/{user_id}/admin",
dependencies=[Depends(check_super_user)],
name="Give or revoke admin permsisions to a user",
@@ -220,6 +221,43 @@ 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")
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
return await get_wallets(user_id, deleted=None)
+2 -4
View File
@@ -9,6 +9,7 @@ from fastapi import (
)
from lnbits.core.crud.wallets import (
clear_wallet_cache,
create_wallet,
get_wallets_paginated,
)
@@ -37,7 +38,6 @@ from lnbits.decorators import (
require_invoice_key,
)
from lnbits.helpers import generate_filter_params_openapi
from lnbits.utils.cache import cache
from ..crud import (
delete_wallet,
@@ -134,9 +134,7 @@ async def api_reset_wallet_keys(
if not wallet or wallet.user != account_id.id:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
cache.pop(f"auth:wallet:{wallet.id}")
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
cache.pop(f"auth:x-api-key:{wallet.inkey}")
clear_wallet_cache(wallet)
wallet.adminkey = uuid4().hex
wallet.inkey = uuid4().hex
+1
View File
@@ -261,6 +261,7 @@ async def check_account_id_exists(
account_id,
expiry=settings.auth_authentication_cache_minutes * 60,
)
cache.set(f"auth:user:cache_key:{sha256s(account.id)}", cache_key)
return account_id
+8 -54
View File
@@ -70,60 +70,11 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
t.env.globals["static_url_for"] = static_url_for
t.env.globals["normalize_path"] = normalize_path
window_settings = {
"AD_SPACE": settings.lnbits_ad_space,
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
"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,
"WALLET_FEATURED_BUTTON_LABEL": settings.lnbits_wallet_featured_button_label,
"WALLET_FEATURED_BUTTON_URL": settings.lnbits_wallet_featured_button_url,
"WALLET_FEATURED_BUTTON_ICON": settings.lnbits_wallet_featured_button_icon,
"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
# used in base.html
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
t.env.globals["LNBITS_APPLE_TOUCH_ICON"] = settings.lnbits_apple_touch_icon
t.env.globals["SETTINGS"] = settings.to_public().dict(by_alias=True)
t.env.globals["CURRENCIES"] = list(currencies.keys())
if settings.bundle_assets:
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
@@ -137,6 +88,9 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
# backwards compatibility for extensions (tpos)
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
return t
+126
View File
@@ -41,6 +41,14 @@ class UsersSettings(LNbitsSettings):
lnbits_admin_users: list[str] = Field(default=[])
lnbits_allowed_users: list[str] = Field(default=[])
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
def new_accounts_allowed(self) -> bool:
@@ -983,6 +991,7 @@ class EnvSettings(LNbitsSettings):
enable_log_to_file: bool = Field(default=True)
log_rotation: str = Field(default="100 MB")
log_retention: str = Field(default="3 months")
first_install_token: str | None = Field(default=None)
cleanup_wallets_days: int = Field(default=90, ge=0)
funding_source_max_retries: int = Field(default=4, ge=0)
@@ -1131,6 +1140,123 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
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):
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
+26 -1
View File
@@ -171,6 +171,7 @@ window.localisation.en = {
drop_db: 'Remove Data',
enable: 'Enable',
enabled: 'Enabled',
disabled: 'Disabled',
pay_to_enable: 'Pay To Enable',
enable_extension_details: 'Enable extension for current user',
disable: 'Disable',
@@ -178,6 +179,8 @@ window.localisation.en = {
installed: 'Installed',
activated: 'Activated',
deactivated: 'Deactivated',
activate: 'Activate',
deactivate: 'Deactivate',
release_notes: 'Release Notes',
activate_extension_details: 'Make extension available/unavailable for users',
featured: 'Featured',
@@ -186,6 +189,8 @@ window.localisation.en = {
only_admins_can_create_extensions:
'Only admin accounts can create extensions',
admin_only: 'Admin Only',
make_user_admin: 'Make User Admin',
revoke_admin: 'Revoke Admin',
new_version: 'New Version',
reviews_url: 'Reviews URL',
reviews_url_label: 'Reviews server URL',
@@ -280,7 +285,7 @@ window.localisation.en = {
'Nip5 identifier to send notifications to',
notifications_nostr_identifiers: 'Nostr Identifiers',
notifications_nostr_identifiers_desc:
'List of identifiers to send notifications to',
'List of identifiers to send notifications to.',
notifications_telegram_config: 'Telegram Configuration',
notifications_enable_telegram: 'Enable Telegram',
@@ -518,6 +523,7 @@ window.localisation.en = {
hash: 'Hash: ',
welcome_lnbits: 'Welcome to LNbits',
setup_su_account: 'Set up the Superuser account below.',
first_install_token: 'First Install Token (optional)',
create_ticker_converter: 'Create Currency Ticker Converter',
enable_audit: 'Enable Audit',
recommended: 'Recommended',
@@ -703,6 +709,25 @@ window.localisation.en = {
allowed_users_label: 'User ID',
allow_creation_user: 'Allow creation of new users',
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.',
start_user_impersonation: 'Impersonate this user',
stop_user_impersonation: 'Stop User Impersonation',
+4 -3
View File
@@ -66,7 +66,7 @@ window._lnbitsApi = {
name: name
})
},
register(username, email, password, password_repeat) {
register(username, email, password, password_repeat, invitation_code) {
return axios({
method: 'POST',
url: '/api/v1/auth/register',
@@ -74,7 +74,8 @@ window._lnbitsApi = {
username,
email,
password,
password_repeat
password_repeat,
invitation_code
}
})
},
@@ -147,7 +148,7 @@ window._lnbitsApi = {
name: name,
wallet_type: walletType,
...opts
}).catch(LNbits.utils.notifyApiError)
})
},
updateWallet(name, wallet) {
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
+26 -19
View File
@@ -35,23 +35,6 @@ window.app.component('lnbits-extension-rating', {
window.app.component('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: {
isActive(path) {
return window.location.pathname === path
@@ -449,8 +432,10 @@ window.app.component('username-password', {
username: String,
password_1: String,
password_2: String,
invitationCode: String,
resetKey: String
},
data() {
return {
oauth: [
@@ -463,8 +448,10 @@ window.app.component('username-password', {
password: this.password_1,
passwordRepeat: this.password_2,
reset_key: this.resetKey,
keycloakOrg: LNBITS_AUTH_KEYCLOAK_ORG || 'Keycloak',
keycloakIcon: LNBITS_AUTH_KEYCLOAK_ICON
confirmationMethod: 'code',
confirmationEmail: '',
confirmationCode: this.invitationCode || '',
showConfirmationCode: false
}
},
methods: {
@@ -477,6 +464,7 @@ window.app.component('username-password', {
this.$emit('update:userName', this.username)
this.$emit('update:password_1', this.password)
this.$emit('update:password_2', this.passwordRepeat)
this.$emit('update:invitationCode', this.confirmationCode)
this.$emit('register')
},
reset() {
@@ -568,6 +556,25 @@ window.app.component('username-password', {
computed: {
showOauth() {
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() {}
@@ -4,7 +4,9 @@ window.app.component('lnbits-admin-users', {
data() {
return {
formAddUser: '',
formAddAdmin: ''
formAddAdmin: '',
formAddActivationCode: '',
showReusableActivationCode: false
}
},
methods: {
@@ -31,6 +33,24 @@ window.app.component('lnbits-admin-users', {
removeAdminUser(user) {
let admin_users = this.formData.lnbits_admin_users
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
)
}
}
})
+3 -6
View File
@@ -1,14 +1,11 @@
window.app.component('lnbits-footer', {
template: '#lnbits-footer',
computed: {
version() {
return this.LNBITS_VERSION || 'unknown version'
},
title() {
return `${this.SITE_TITLE}, ${this.SITE_TAGLINE}`
return `${this.g.settings.siteTitle}, ${this.g.settings.siteTagline}`
},
showFooter() {
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
version() {
return this.$t('lnbits_version') + ': ' + this.g.settings.version
}
}
})
@@ -1,48 +1,9 @@
window.app.component('lnbits-header', {
template: '#lnbits-header',
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() {
return this.g.user && (this.g.user.super_user || this.g.user.admin)
},
showVoidwallet() {
return this.g.user && this.VOIDWALLET == true
},
displayName() {
return (
this.g.user?.extra?.display_name ||
@@ -95,8 +95,8 @@ window.app.component('lnbits-home-logos', {
showLogos() {
return (
this.g.isSatsDenomination &&
this.SITE_TITLE == 'LNbits' &&
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
this.g.settings.siteTitle == 'LNbits' &&
this.g.settings.showHomePageElements == true
)
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ window.app.component('lnbits-qrcode', {
},
logo: {
type: String,
default: LNBITS_QR_LOGO
default: window.g.settings.qrLogo || null
}
},
data() {
+1 -1
View File
@@ -127,7 +127,7 @@ window.app.component('lnbits-theme', {
if (this.g.mobileSimple === true) {
document.body.classList.add('mobile-simple')
}
Object.entries(this.g.user.uiCustomization || {}).forEach(
Object.entries(this.g.user?.uiCustomization || {}).forEach(
([key, value]) => {
if (key in this.g) {
this.g[key] = value
@@ -97,6 +97,7 @@ window.app.component('lnbits-wallet-new', {
this.g.lastWalletId = res.data.id
this.$router.push(`/wallet/${res.data.id}`)
})
.catch(LNbits.utils.notifyApiError)
}
},
created() {
+35 -40
View File
@@ -9,26 +9,22 @@ const localStore = (key, defaultValue) => {
}
window.g = Vue.reactive({
errorCode: null,
errorMessage: null,
user: null,
wallet: null,
isPublicPage: true,
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
offline: !navigator.onLine,
hasCamera: false,
visibleDrawer: false,
extensions: WINDOW_SETTINGS.EXTENSIONS,
fiatBalance: 0,
exchangeRate: 0,
fiatTracking: false,
payments: [],
walletEventListeners: [],
updatePayments: false, // used for updating the lnbits-payment-list
updatePaymentsHash: false, // used for closing the receive dialog
currencies: WINDOW_SETTINGS.LNBITS_CURRENCIES ?? [],
allowedCurrencies: WINDOW_SETTINGS.LNBITS_ALLOWED_CURRENCIES ?? [],
// 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),
@@ -36,26 +32,25 @@ window.g = Vue.reactive({
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',
// cookie vars
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
// frontend vars
errorCode: null,
errorMessage: null,
user: null,
wallet: null,
isPublicPage: true,
offline: !navigator.onLine,
hasCamera: false,
visibleDrawer: false,
fiatBalance: 0,
exchangeRate: 0,
fiatTracking: false,
payments: [],
walletEventListeners: [],
updatePayments: false, // used for updating the lnbits-payment-list
updatePaymentsHash: false, // used for closing the receive dialog
scanner: null,
newWalletType: null
})
+1 -2
View File
@@ -157,8 +157,7 @@ window.app.mixin({
return {
api: window._lnbitsApi,
utils: window._lnbitsUtils,
g: window.g,
...WINDOW_SETTINGS
g: window.g
}
},
// backwards compatibility for extensions, should not be used in the future
+1 -1
View File
@@ -711,7 +711,7 @@ window.PageAccount = {
await this.getUserAssets()
// filter out themes that are not allowed
this.themeOptions = this.themeOptions.filter(theme =>
this.LNBITS_THEME_OPTIONS.includes(theme.name)
this.g.settings.themeOptions.includes(theme.name)
)
}
}
+2 -2
View File
@@ -842,8 +842,8 @@ window.PageExtensions = {
},
async created() {
this.extensions = await this.fetchAllExtensions()
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
this.reviewsUrl = this.LNBITS_EXTENSIONS_REVIEWS_URL
this.extbuilderEnabled = this.g.user.admin || this.g.settings.extBuilder
this.reviewsUrl = this.g.settings.extensionsReviewsUrl
if (this.g.user.extensions.length === 0) {
this.tab = 'all'
+16 -10
View File
@@ -7,7 +7,8 @@ window.PageFirstInstall = {
isPwdRepeat: true,
username: '',
password: '',
passwordRepeat: ''
passwordRepeat: '',
firstInstallToken: ''
}
}
},
@@ -17,20 +18,25 @@ window.PageFirstInstall = {
}
},
methods: {
async setPassword() {
try {
await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
setPassword() {
LNbits.api
.request('PUT', `/api/v1/auth/first_install`, null, {
username: this.loginData.username,
password: this.loginData.password,
password_repeat: this.loginData.passwordRepeat
password_repeat: this.loginData.passwordRepeat,
first_install_token: this.loginData.firstInstallToken
})
window.location.href = '/admin'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
.then(async () => {
const res = await LNbits.api.getAuthUser()
this.g.user = LNbits.map.user(res.data)
this.g.isPublicPage = false
this.$router.push('/admin')
})
.catch(this.utils.notifyApiError)
}
},
created() {
document.title = 'First Install - LNbits'
const params = new URLSearchParams(window.location.search)
this.loginData.firstInstallToken = params.get('token') || ''
}
}
+7 -29
View File
@@ -11,6 +11,7 @@ window.PageHome = {
email: '',
password: '',
passwordRepeat: '',
invitationCode: '',
walletName: '',
signup: false
}
@@ -19,40 +20,15 @@ window.PageHome = {
showClaimLnurl() {
return (
this.lnurl !== '' &&
this.allowRegister &&
'user-id-only' in this.LNBITS_AUTH_METHODS
this.g.settings.allowRegister &&
'user-id-only' in this.g.settings.authMethods
)
},
formatDescription() {
return LNbits.utils.convertMarkdown(this.SITE_DESCRIPTION)
return LNbits.utils.convertMarkdown(this.g.settings.siteDescription)
},
isAccessTokenExpired() {
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: {
@@ -65,6 +41,7 @@ window.PageHome = {
this.username = null
this.password = null
this.passwordRepeat = null
this.invitationCode = null
this.authAction = 'register'
this.authMethod = authMethod
@@ -76,7 +53,8 @@ window.PageHome = {
this.username,
this.email,
this.password,
this.passwordRepeat
this.passwordRepeat,
this.invitationCode
)
this.refreshAuthUser()
} catch (e) {
+17 -4
View File
@@ -70,10 +70,10 @@ window.PageUsers = {
usersTable: {
columns: [
{
name: 'admin',
name: 'activated',
align: 'left',
label: 'Admin',
field: 'admin',
label: this.$t('activated'),
field: 'activated',
sortable: false
},
{
@@ -401,7 +401,7 @@ window.PageUsers = {
toggleAdmin(userId) {
LNbits.api
.request('GET', `/users/api/v1/user/${userId}/admin`)
.request('PUT', `/users/api/v1/user/${userId}/admin`)
.then(() => {
this.fetchUsers()
Quasar.Notify.create({
@@ -412,6 +412,19 @@ window.PageUsers = {
})
.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) {
this.activeUser.showPassword = false
this.activeUser.showUserId = false
+1 -1
View File
@@ -1,7 +1,7 @@
window._lnbitsUtils = {
url_for(url) {
const _url = new URL(url, window.location.origin)
_url.searchParams.set('v', WINDOW_SETTINGS.CACHE_KEY)
_url.searchParams.set('v', window.g.settings.cacheKey)
return _url.toString()
},
loadScript(src) {
+5 -1
View File
@@ -178,7 +178,11 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
return
status = await payment.check_status(skip_internal_payment_notifications=True)
from lnbits.core.services.payments import check_payment_status
status = await check_payment_status(
payment, skip_internal_payment_notifications=True
)
payment.fee = status.fee_msat or payment.fee
# only overwrite preimage if status.preimage provides it
payment.preimage = status.preimage or payment.preimage
+4 -4
View File
@@ -70,10 +70,10 @@
</div>
<script type="text/javascript">
const RENDERED_ROUTE = '{{ normalize_path(request.path) }}'
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
Object.keys(WINDOW_SETTINGS).forEach(key => {
window[key] = WINDOW_SETTINGS[key]
})
const SETTINGS = {{ SETTINGS | tojson | safe }}
const CURRENCIES = {{ CURRENCIES | tojson | safe }}
// backwards compatibility for extensions (tpos)
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson | safe }}
</script>
<!-- vue templates -->
{% include('components.vue') %} {% include('pages.vue') %}
+90 -16
View File
@@ -36,7 +36,7 @@ include('components/lnbits-error.vue') %}
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
<q-item-label header v-text="$t('manage')"></q-item-label>
<div v-if="g.user.admin">
<q-item v-if="showAdmin" to="/admin">
<q-item v-if="g.settings.showAdmin" to="/admin">
<q-item-section side>
<q-icon
name="settings"
@@ -51,7 +51,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>
<q-item v-if="showNode" to="/node">
<q-item v-if="g.settings.showNodemanager" to="/node">
<q-item-section side>
<q-icon
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-item-section>
</q-item>
<q-item v-if="showUsers" to="/users">
<q-item v-if="g.settings.showAdmin" to="/users">
<q-item-section side>
<q-icon
name="groups"
@@ -81,7 +81,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>
<q-item v-if="showAudit" to="/audit">
<q-item v-if="g.settings.showAudit" to="/audit">
<q-item-section side>
<q-icon
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-item-section>
</q-item>
<q-item v-if="showExtensions" to="/extensions">
<q-item v-if="g.settings.showExtensions" to="/extensions">
<q-item-section side>
<q-icon
name="extension"
@@ -686,7 +686,7 @@ include('components/lnbits-error.vue') %}
dense
filled
v-model="walletName"
:label="$t('name_your_wallet', {name: SITE_TITLE + ' *'})"
:label="$t('name_your_wallet', {name: g.settings.siteTitle + ' *'})"
></q-input>
<q-card-actions vertical align="center" class="q-pa-none">
<q-btn
@@ -814,16 +814,90 @@ include('components/lnbits-error.vue') %}
type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
></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">
<q-btn
unelevated
color="primary"
:disable="
!password ||
!passwordRepeat ||
!username ||
password !== passwordRepeat
"
:disable="disableRegister"
type="submit"
class="full-width"
:label="$t('create_account')"
@@ -960,9 +1034,9 @@ include('components/lnbits-error.vue') %}
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="
keycloakIcon
? keycloakIcon
: 'lnbits/static/images/keycloak-logo.png'
g.settings.keycloakIcon
? g.settings.keycloakIcon
: utils.url_for('lnbits/static/images/keycloak-logo.png')
"
></q-img>
</q-avatar>
@@ -970,7 +1044,7 @@ include('components/lnbits-error.vue') %}
<span
v-text="
$t('signin_with_custom_org', {
custom_org: keycloakOrg
custom_org: g.settings.keycloakOrg || 'Keycloak'
})
"
></span>
@@ -50,7 +50,7 @@
<br />
</div>
<div class="col">
<div v-if="LNBITS_NODE_UI">
<div v-if="g.settings.hasNodemanager">
<p>
<span v-text="$t('node_management')"></span>
</p>
@@ -70,7 +70,7 @@
v-model="formData.lnbits_node_ui_transactions"
></q-toggle>
</div>
<p v-if="!LNBITS_NODE_UI">
<p v-if="!g.settings.hasNodemanager">
<span v-text="$t('node_management_not_supported')"></span>
</p>
</div>
+234 -3
View File
@@ -30,7 +30,6 @@
>
</q-chip>
</div>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('allowed_users')"></span></p>
@@ -57,7 +56,10 @@
>
</q-chip>
</div>
<br />
</div>
</div>
<div class="row">
<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('allow_creation_user')"></q-item-label>
@@ -76,8 +78,237 @@
/>
</q-item-section>
</q-item>
<br />
</div>
<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>
</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>
@@ -1,6 +1,6 @@
<template id="lnbits-footer">
<q-footer
v-if="showFooter"
v-if="g.settings.showHomePageElements"
class="bg-transparent q-px-lg q-py-md"
:class="{'text-dark': !$q.dark.isActive}"
>
@@ -9,7 +9,7 @@
<q-toolbar-title class="text-caption">
<span v-text="title"></span>
<br />
<small v-text="$t('lnbits_version') + ': ' + version"></small>
<small v-text="version"></small>
</q-toolbar-title>
<q-space></q-space>
<q-btn
+19 -13
View File
@@ -1,6 +1,10 @@
<template id="lnbits-header">
<q-header bordered class="bg-marginal-bg">
<q-banner v-if="showVoidwallet" class="bg-warning text-white" dense>
<q-banner
v-if="g.settings.showVoidwallet"
class="bg-warning text-white"
dense
>
<template v-slot:avatar>
<q-icon name="warning" color="white" />
</template>
@@ -24,13 +28,15 @@
<q-toolbar-title>
<q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
<q-img
v-if="customLogoUrl"
v-if="customLogo"
height="30px"
alt="Logo"
:src="customLogoUrl"
:src="customLogo"
></q-img>
<span v-else-if="!titleIsLnbits"><strong>LN</strong>bits</span>
<span v-else v-text="title"></span>
<span v-else-if="g.settings.siteTitle == 'LNbits'"
><strong>LN</strong>bits</span
>
<span v-else v-text="g.settings.siteTitle"></span>
</q-btn>
<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>
@@ -38,31 +44,31 @@
<q-badge
class="q-mr-md"
v-show="$q.screen.gt.sm"
v-if="hasCustomBadge"
:label="customBadge"
:color="customBadgeColor"
v-if="g.settings.customBadge"
:label="g.settings.customBadge"
:color="g.settings.customBadgeColor"
>
</q-badge>
<q-badge
v-show="$q.screen.gt.sm"
v-if="hasServiceFee"
v-if="g.user && g.settings.serviceFee > 0"
color="green"
class="q-mr-md"
>
<span
v-if="hasServiceFeeMax"
v-if="g.user && g.settings.serviceFeeMax > 0"
v-text="
$t('service_fee_max_badge', {
amount: serviceFee,
max: serviceFeeMax,
amount: g.settings.serviceFee,
max: g.settings.serviceFeeMax,
denom: g.denomination
})
"
></span>
<span
v-else
v-text="$t('service_fee_badge', {amount: serviceFee})"
v-text="$t('service_fee_badge', {amount: g.settings.serviceFee})"
></span>
<q-tooltip><span v-text="$t('service_fee_tooltip')"></span></q-tooltip>
</q-badge>
@@ -118,7 +118,7 @@
</q-list>
</q-card-section>
<q-expansion-item
v-if="!HIDE_API"
v-if="!g.settings.hideApi"
group="api"
dense
expand-separator
@@ -152,7 +152,7 @@
</q-expansion-item>
<q-expansion-item
v-if="!HIDE_API"
v-if="!g.settings.hideApi"
group="api"
dense
expand-separator
@@ -197,7 +197,7 @@
</q-card>
</q-expansion-item>
<q-expansion-item
v-if="!HIDE_API"
v-if="!g.settings.hideApi"
group="api"
dense
expand-separator
@@ -244,7 +244,7 @@
</q-expansion-item>
<q-expansion-item
v-if="!HIDE_API"
v-if="!g.settings.hideApi"
group="api"
dense
expand-separator
@@ -273,7 +273,7 @@
</q-card>
</q-expansion-item>
<q-expansion-item
v-if="!HIDE_API"
v-if="!g.settings.hideApi"
group="api"
dense
expand-separator
+18 -12
View File
@@ -258,15 +258,20 @@
class="q-mb-md"
>
</q-input>
<div v-if="!g.user.email" class="row"></div>
<div v-if="!g.user.email" class="row">
{% if "google-auth" in LNBITS_AUTH_METHODS or
"github-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm text-h6">
<div
v-if="
'google-auth' in g.settings.authMethods ||
'github-auth' in g.settings.authMethods
"
class="col q-pa-sm text-h6"
>
<span v-text="$t('verify_email')"></span>:
</div>
{%endif%} {% if "google-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<div
v-if="'google-auth' in g.settings.authMethods"
class="col q-pa-sm"
>
<q-btn
:href="`/api/v1/auth/google?user_id=${g.user.id}`"
type="a"
@@ -284,8 +289,10 @@
<div>Google</div>
</q-btn>
</div>
{%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<div
v-if="'github-auth' in g.settings.authMethods"
class="col q-pa-sm"
>
<q-btn
:href="`/api/v1/auth/github?user_id=${g.user.id}`"
type="a"
@@ -303,7 +310,6 @@
<div>GitHub</div>
</q-btn>
</div>
{%endif%}
</div>
</q-card-section>
@@ -508,7 +514,7 @@
<div class="col-8">
<lnbits-notifications-btn
v-if="g.user"
pubkey="{{ WEBPUSH_PUBKEY }}"
pubkey="g.settings.webpushPubkey"
></lnbits-notifications-btn>
</div>
</div>
@@ -552,7 +558,7 @@
></span>
<br />
<q-badge
v-if="!LNBITS_NOSTR_CONFIGURED"
v-if="!g.settings.nostrConfigured"
v-text="$t('not_connected')"
></q-badge>
</div>
@@ -572,7 +578,7 @@
<span v-text="$t('notifications_chat_id')"></span>
<br />
<q-badge
v-if="!LNBITS_TELEGRAM_CONFIGURED"
v-if="!g.settings.telegramConfigured"
v-text="$t('not_connected')"
></q-badge>
</div>
+12
View File
@@ -53,6 +53,18 @@
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
/> </template
></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
@click="setPassword()"
unelevated
+29 -15
View File
@@ -2,14 +2,14 @@
<div class="home row justify-center items-center">
<div
class="full-width content-center"
:style="`max-width: ${hasCustomImage ? '850' : '600'}px; min-height: 55vh;`"
:style="`max-width: ${g.settings.customImage ? '850' : '600'}px; min-height: 55vh;`"
>
<div v-if="showHomepageElements" class="row q-mb-md">
<div v-if="g.settings.showHomepageElements" class="row q-mb-md">
<div class="col-12">
<div>
<h5 v-text="siteTitle" class="q-my-none"></h5>
<h5 v-text="g.settings.siteTitle" class="q-my-none"></h5>
<template v-if="$q.screen.gt.sm">
<h6 class="q-my-sm" v-text="siteTagline"></h6>
<h6 class="q-my-sm" v-text="g.settings.siteTagline"></h6>
<p class="q-my-sm" v-html="formatDescription"></p>
</template>
</div>
@@ -31,7 +31,10 @@
<div class="row">
<div
class="col-12"
:class="{'col-sm-7': hasCustomImage, 'col-lg-6': hasCustomImage}"
:class="{
'col-sm-7': g.settings.customImage,
'col-lg-6': g.settings.customImage
}"
>
<div v-if="showClaimLnurl">
<q-card-section>
@@ -54,12 +57,13 @@
<div v-else>
<username-password
v-if="authMethod != 'user-id-only'"
:allowed_new_users="allowRegister"
:auth-methods="LNBITS_AUTH_METHODS"
:allowed_new_users="g.settings.allowRegister"
:auth-methods="g.settings.authMethods"
:auth-action="authAction"
v-model:user-name="username"
v-model:password_1="password"
v-model:password_2="passwordRepeat"
v-model:invitation-code="invitationCode"
v-model:reset-key="reset_key"
@login="login"
@register="register"
@@ -70,7 +74,7 @@
v-if="authAction !== 'reset'"
>
<p
v-if="authAction === 'login' && allowRegister"
v-if="authAction === 'login' && g.settings.allowRegister"
class="q-mb-none"
>
Not registered?
@@ -82,7 +86,9 @@
>
</p>
<p
v-else-if="authAction === 'login' && !allowRegister"
v-else-if="
authAction === 'login' && !g.settings.allowRegister
"
class="q-mb-none"
>
<span v-text="$t('new_user_not_allowed')"></span>
@@ -101,7 +107,7 @@
</username-password>
<user-id-only
v-if="authMethod == 'user-id-only'"
:allowed_new_users="allowRegister"
:allowed_new_users="g.settings.allowRegister"
v-model:usr="usr"
v-model:wallet="walletName"
:auth-action="authAction"
@@ -114,16 +120,20 @@
</user-id-only>
</div>
</div>
<div v-if="hasCustomImage" class="col-sm-5 col-lg-6 gt-xs">
<div v-if="g.settings.customImage" class="col-sm-5 col-lg-6 gt-xs">
<div class="full-height flex flex-center q-pa-lg">
<q-img :src="hasCustomImage" :ratio="1" width="250px"></q-img>
<q-img
:src="g.settings.customImage"
:ratio="1"
width="250px"
></q-img>
</div>
</div>
</div>
</q-card>
</div>
<div v-if="showHomepageElements">
<div v-if="g.settings.showHomepageElements">
<div class="flex flex-center q-gutter-md q-py-md">
<q-btn
outline
@@ -148,10 +158,14 @@
</div>
<div
v-if="adsEnabled"
v-if="g.settings.showAdSpace"
class="justify-center col-10 q-my-lg row q-col-gutter-sm"
>
<a :href="ad[0]" v-for="ad in g.ads" class="lnbits-ad col-12 col-md-4">
<a
: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-else :src="ad[2]"></q-img>
</a>
+33 -5
View File
@@ -218,6 +218,26 @@
:label="$t('update_account')"
class="q-ml-md"
></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
v-else
@click="createUser()"
@@ -590,9 +610,9 @@
<q-btn
@click="showAccountPage(props.row.id)"
round
icon="edit"
:icon="props.row.is_admin ? 'admin_panel_settings' : 'edit'"
size="sm"
color="secondary"
:color="props.row.is_admin ? 'primary' : 'secondary'"
class="q-ml-xs"
>
<q-tooltip>
@@ -605,10 +625,18 @@
size="xs"
v-if="!props.row.is_super_user"
color="secondary"
v-model="props.row.is_admin"
@update:model-value="toggleAdmin(props.row.id)"
v-model="props.row.activated"
@update:model-value="toggleUserActivated(props.row.id)"
>
<q-tooltip>Toggle Admin</q-tooltip>
<q-tooltip
><span
v-text="
props.row.activated
? $t('deactivate')
: $t('activate')
"
></span
></q-tooltip>
</q-toggle>
<q-btn
round
+8 -8
View File
@@ -172,15 +172,15 @@
></q-tooltip>
</q-btn>
<div
v-if="WALLET_FEATURED_BUTTON_URL"
v-if="g.settings.walletFeaturedButtonUrl"
class="float-right q-mt-sm q-ml-sm"
>
<q-btn
color="primary"
:label="WALLET_FEATURED_BUTTON_LABEL"
:icon="WALLET_FEATURED_BUTTON_ICON || undefined"
:label="g.settings.walletFeaturedButtonLabel"
:icon="g.settings.walletFeaturedButtonIcon || undefined"
size="sm"
:to="WALLET_FEATURED_BUTTON_URL"
:to="g.settings.walletFeaturedButtonUrl"
>
</q-btn>
</div>
@@ -212,10 +212,10 @@
@send-lnurl="handleSendLnurl"
:chart-config="chartConfig"
></lnbits-wallet-extra>
<q-card class="lnbits-wallet-ads" v-if="AD_SPACE_ENABLED">
<q-card class="lnbits-wallet-ads" v-if="g.settings.showAdSpace">
<q-card-section class="text-subtitle1">
<span v-text="AD_SPACE_TITLE"></span>
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.ads">
<span v-text="g.settings.adSpaceTitle"></span>
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.settings.adSpace">
<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>
</a>
@@ -289,7 +289,7 @@
></q-input>
</div>
<q-input
v-if="has_holdinvoice"
v-if="g.settings.hasHoldinvoice"
filled
dense
v-model="receive.data.payment_hash"
+2 -4
View File
@@ -45,10 +45,8 @@
</div>
<script type="text/javascript">
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
Object.keys(WINDOW_SETTINGS).forEach(key => {
window[key] = WINDOW_SETTINGS[key]
})
const SETTINGS = {{ SETTINGS | tojson | safe }}
const CURRENCIES = {{ CURRENCIES | tojson | safe }}
</script>
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
for url in INCLUDED_JS %}
+7
View File
@@ -14,6 +14,13 @@ from lnbits.settings import settings
def log_server_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"Baseurl: {settings.lnbits_baseurl}")
logger.info(f"Host: {settings.host}")
+142
View File
@@ -297,6 +297,148 @@ async def test_register_ok(http_client: AsyncClient):
), 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
async def test_register_email_twice(http_client: AsyncClient):
tiny_id = shortuuid.uuid()[:8]
+126
View File
@@ -1,11 +1,13 @@
from typing import Any
from uuid import uuid4
import jwt
import pytest
import shortuuid
from httpx import AsyncClient
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.services.users import create_user_account
from lnbits.settings import Settings
@@ -610,3 +612,127 @@ 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)
assert undeleted_wallet is not None
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,3 +341,7 @@ def _settings_cleanup(settings: Settings):
settings.lnbits_max_outgoing_payment_amount_sats = 10_000_000_100
settings.lnbits_max_incoming_payment_amount_sats = 10_000_000_200
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 = []
+6 -2
View File
@@ -7,7 +7,11 @@ from lnbits import bolt11
from lnbits.core.crud import get_standalone_payment, update_payment
from lnbits.core.crud.wallets import create_wallet, get_wallet
from lnbits.core.models import CreateInvoice, Payment, PaymentState
from lnbits.core.services import fee_reserve_total, get_balance_delta
from lnbits.core.services import (
check_payment_status,
fee_reserve_total,
get_balance_delta,
)
from lnbits.core.services.payments import pay_invoice, update_wallet_balance
from lnbits.core.services.users import create_user_account
from lnbits.exceptions import PaymentError
@@ -355,7 +359,7 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
assert payment_db_after_settlement is not None
# payment is failed
status = await payment_db_after_settlement.check_status()
status = await check_payment_status(payment_db_after_settlement)
assert not status.paid
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.users import User
from lnbits.core.models.wallets import Wallet
from lnbits.core.services import payments
from lnbits.core.services import check_payment_status, payments
from lnbits.core.services.fiat_providers import (
check_stripe_signature,
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.fee <= 0
status = await payment.check_status()
status = await check_payment_status(payment)
assert status.success is False
assert status.pending is True
@@ -230,7 +230,7 @@ async def test_create_wallet_fiat_invoice_success(
"lnbits.fiat.StripeWallet.get_invoice_status",
AsyncMock(return_value=fiat_mock_status),
)
status = await payment.check_status()
status = await check_payment_status(payment)
assert status.paid is True
assert status.success is True