Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18ce66e507 | ||
|
|
acce8d726b | ||
|
|
164e498476 | ||
|
|
48e0ca80bd | ||
|
|
0f4930f6c8 | ||
|
|
e0cade01c9 | ||
|
|
dd79dbc90b | ||
|
|
9e1ac86761 | ||
|
|
abfc8ebfb4 | ||
|
|
872c24e6ea | ||
|
|
6dcff91bf7 | ||
|
|
ef253de426 | ||
|
|
0819f51c3a | ||
|
|
d1cae5341f | ||
|
|
3b249fbad2 | ||
|
|
9648587850 | ||
|
|
a5523c1bf6 | ||
|
|
eb84621f23 | ||
|
|
b702229cb1 | ||
|
|
c722776bc0 | ||
|
|
bd7917ab20 | ||
|
|
db9006ea8b | ||
|
|
15f2048bcb | ||
|
|
b0079871b0 | ||
|
|
2bedcae9f1 | ||
|
|
341974fb35 | ||
|
|
d6bfbb0045 | ||
|
|
dc9338b991 | ||
|
|
6f6f7d4542 | ||
|
|
617f9c14ed | ||
|
|
54fdb0948c | ||
|
|
2fe7ab4f83 | ||
|
|
10c8ca4ca1 | ||
|
|
f079762b71 | ||
|
|
2e042d2597 | ||
|
|
49b57c9f0b | ||
|
|
656c6cac5b |
+75
-21
@@ -4,10 +4,12 @@ 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 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.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,
|
||||||
@@ -41,6 +43,7 @@ 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(
|
||||||
@@ -69,6 +72,7 @@ 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,
|
||||||
@@ -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:
|
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",
|
"""
|
||||||
{"id": user_id},
|
SELECT * FROM accounts
|
||||||
|
WHERE id = :id AND (activated = true OR activated = :activated)
|
||||||
|
""",
|
||||||
|
{"id": user_id, "activated": active_only},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,55 +134,79 @@ async def delete_accounts_no_wallets(
|
|||||||
|
|
||||||
|
|
||||||
async def get_account_by_username(
|
async def get_account_by_username(
|
||||||
username: str, conn: Connection | None = None
|
username: str, active_only: bool = True, 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",
|
"""
|
||||||
{"username": username.lower()},
|
SELECT * FROM accounts
|
||||||
|
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, conn: Connection | None = None
|
pubkey: str, 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 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,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_email(
|
async def get_account_by_email(
|
||||||
email: str, conn: Connection | None = None
|
email: str, active_only: bool = True, 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",
|
"""
|
||||||
{"email": email.lower()},
|
SELECT * FROM accounts
|
||||||
|
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, conn: Connection | None = None
|
username_or_email: str,
|
||||||
|
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 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,
|
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:
|
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:
|
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)
|
||||||
@@ -191,6 +225,7 @@ 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,
|
||||||
@@ -216,12 +251,31 @@ async def update_user_access_control_list(
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_access_control_lists(
|
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:
|
) -> 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",
|
"""
|
||||||
{"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,
|
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)
|
||||||
|
|||||||
@@ -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_cache(wallet_id)
|
clear_wallet_id_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_cache(wallet_id)
|
clear_wallet_id_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_cache(wallet_id)
|
clear_wallet_id_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,11 +226,14 @@ async def get_wallet_for_key(
|
|||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
wallet = await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((
|
SELECT wallets.*, 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
|
||||||
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},
|
{"key": key},
|
||||||
Wallet,
|
Wallet,
|
||||||
@@ -250,8 +253,11 @@ async def get_base_wallet_for_key(
|
|||||||
) -> BaseWallet | None:
|
) -> BaseWallet | None:
|
||||||
wallet = await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
|
SELECT wallets.id, "user", wallet_type, adminkey, inkey 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},
|
{"key": key},
|
||||||
BaseWallet,
|
BaseWallet,
|
||||||
@@ -294,8 +300,14 @@ 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_cache(wallet_id):
|
def clear_wallet_id_cache(wallet_id: str):
|
||||||
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}")
|
||||||
|
|||||||
@@ -856,3 +856,11 @@ async def m043_add_ui_customization_to_accounts(db: Connection):
|
|||||||
Used for server side persistence of UI customization settings.
|
Used for server side persistence of UI customization settings.
|
||||||
"""
|
"""
|
||||||
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
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")
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ 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
|
||||||
@@ -241,6 +242,7 @@ 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
|
||||||
@@ -276,6 +278,7 @@ 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
|
||||||
@@ -315,6 +318,7 @@ 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, min_length=1, max_length=256)
|
||||||
|
|
||||||
|
|
||||||
class CreateUser(BaseModel):
|
class CreateUser(BaseModel):
|
||||||
|
|||||||
@@ -776,7 +776,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}")
|
||||||
|
|
||||||
send_payment_notification_in_background(wallet, payment)
|
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
||||||
|
|
||||||
# notify receiver asynchronously
|
# notify receiver asynchronously
|
||||||
from lnbits.tasks import internal_invoice_queue
|
from lnbits.tasks import internal_invoice_queue
|
||||||
@@ -849,7 +849,8 @@ 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
|
||||||
@@ -1057,3 +1058,13 @@ 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)
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ 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,
|
||||||
@@ -192,3 +194,25 @@ 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.")
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ 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 update_user_account
|
from lnbits.core.services.users import (
|
||||||
|
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,
|
||||||
@@ -86,6 +89,7 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
@@ -94,7 +98,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"])
|
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
|
||||||
if not account:
|
if not account:
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
@@ -102,6 +106,8 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
@@ -357,12 +363,14 @@ 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):
|
if await get_account_by_username(data.username, active_only=False):
|
||||||
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,
|
||||||
@@ -527,7 +535,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)
|
account = await get_account_by_email(email, active_only=False)
|
||||||
|
|
||||||
if verified_user_id:
|
if verified_user_id:
|
||||||
if account:
|
if account:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ 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,
|
||||||
@@ -73,7 +74,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)
|
user = await get_user(user_id, active_only=False)
|
||||||
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
|
||||||
@@ -197,7 +198,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.get(
|
@users_router.put(
|
||||||
"/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",
|
||||||
@@ -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")
|
@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)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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,
|
||||||
)
|
)
|
||||||
@@ -37,7 +38,6 @@ 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,9 +134,7 @@ 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")
|
||||||
|
|
||||||
cache.pop(f"auth:wallet:{wallet.id}")
|
clear_wallet_cache(wallet)
|
||||||
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
|
||||||
|
|||||||
+9
-42
@@ -12,7 +12,6 @@ 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
|
||||||
|
|
||||||
@@ -56,14 +55,6 @@ 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>"
|
||||||
@@ -326,31 +317,7 @@ 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
|
||||||
@@ -663,7 +630,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(get_placeholder(model, field))
|
placeholders.append(f":{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)
|
||||||
@@ -681,9 +648,8 @@ 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}" = {placeholder}')
|
fields.append(f'"{field}" = :{field}')
|
||||||
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
|
||||||
|
|
||||||
@@ -701,7 +667,12 @@ 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):
|
||||||
_dict[key] = value.timestamp()
|
if DB_TYPE == SQLITE:
|
||||||
|
_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)
|
||||||
@@ -757,11 +728,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
|
|||||||
if DB_TYPE == SQLITE:
|
if DB_TYPE == SQLITE:
|
||||||
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
||||||
else:
|
else:
|
||||||
# Ensure PostgreSQL datetime values have timezone info
|
_dict[key] = value.replace(tzinfo=timezone.utc)
|
||||||
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)
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ 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:
|
||||||
@@ -1188,6 +1196,11 @@ class PublicSettings(BaseModel):
|
|||||||
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
|
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
|
||||||
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
|
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
|
||||||
wallet_featured_button_icon: str | None = Field(alias="walletFeaturedButtonIcon")
|
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
|
@classmethod
|
||||||
def from_settings(cls, settings: Settings):
|
def from_settings(cls, settings: Settings):
|
||||||
@@ -1239,6 +1252,9 @@ class PublicSettings(BaseModel):
|
|||||||
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
|
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
|
||||||
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
|
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
|
||||||
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -171,6 +171,7 @@ 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',
|
||||||
@@ -178,6 +179,8 @@ 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',
|
||||||
@@ -186,6 +189,8 @@ 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',
|
||||||
@@ -280,7 +285,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',
|
||||||
@@ -704,6 +709,25 @@ 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',
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ window._lnbitsApi = {
|
|||||||
name: name
|
name: name
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
register(username, email, password, password_repeat) {
|
register(username, email, password, password_repeat, invitation_code) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/v1/auth/register',
|
url: '/api/v1/auth/register',
|
||||||
@@ -74,7 +74,8 @@ window._lnbitsApi = {
|
|||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
password_repeat
|
password_repeat,
|
||||||
|
invitation_code
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -147,7 +148,7 @@ 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, {
|
||||||
|
|||||||
@@ -432,8 +432,10 @@ 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: [
|
||||||
@@ -445,7 +447,11 @@ window.app.component('username-password', {
|
|||||||
username: this.userName,
|
username: this.userName,
|
||||||
password: this.password_1,
|
password: this.password_1,
|
||||||
passwordRepeat: this.password_2,
|
passwordRepeat: this.password_2,
|
||||||
reset_key: this.resetKey
|
reset_key: this.resetKey,
|
||||||
|
confirmationMethod: 'code',
|
||||||
|
confirmationEmail: '',
|
||||||
|
confirmationCode: this.invitationCode || '',
|
||||||
|
showConfirmationCode: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -458,6 +464,7 @@ 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() {
|
||||||
@@ -549,6 +556,25 @@ 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,7 +4,9 @@ window.app.component('lnbits-admin-users', {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddUser: '',
|
formAddUser: '',
|
||||||
formAddAdmin: ''
|
formAddAdmin: '',
|
||||||
|
formAddActivationCode: '',
|
||||||
|
showReusableActivationCode: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -31,6 +33,24 @@ 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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ 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,6 +97,7 @@ 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() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ window.PageHome = {
|
|||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordRepeat: '',
|
passwordRepeat: '',
|
||||||
|
invitationCode: '',
|
||||||
walletName: '',
|
walletName: '',
|
||||||
signup: false
|
signup: false
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ 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
|
||||||
@@ -51,7 +53,8 @@ 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) {
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ window.PageUsers = {
|
|||||||
usersTable: {
|
usersTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'admin',
|
name: 'activated',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Admin',
|
label: this.$t('activated'),
|
||||||
field: 'admin',
|
field: 'activated',
|
||||||
sortable: false
|
sortable: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -401,7 +401,7 @@ window.PageUsers = {
|
|||||||
|
|
||||||
toggleAdmin(userId) {
|
toggleAdmin(userId) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', `/users/api/v1/user/${userId}/admin`)
|
.request('PUT', `/users/api/v1/user/${userId}/admin`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchUsers()
|
this.fetchUsers()
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -412,6 +412,19 @@ 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
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ window._lnbitsUtils = {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
notifyApiError(error) {
|
async notifyApiError(error) {
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
return console.error(error)
|
return console.error(error)
|
||||||
}
|
}
|
||||||
@@ -154,6 +154,10 @@ 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('/')})`)
|
||||||
|
|||||||
@@ -814,16 +814,90 @@ 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="
|
:disable="disableRegister"
|
||||||
!password ||
|
|
||||||
!passwordRepeat ||
|
|
||||||
!username ||
|
|
||||||
password !== passwordRepeat
|
|
||||||
"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
:label="$t('create_account')"
|
:label="$t('create_account')"
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
>
|
>
|
||||||
</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>
|
||||||
@@ -57,7 +56,10 @@
|
|||||||
>
|
>
|
||||||
</q-chip>
|
</q-chip>
|
||||||
</div>
|
</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 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>
|
||||||
@@ -76,8 +78,237 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</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>
|
||||||
</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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
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"
|
||||||
|
|||||||
@@ -218,6 +218,26 @@
|
|||||||
: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()"
|
||||||
@@ -590,9 +610,9 @@
|
|||||||
<q-btn
|
<q-btn
|
||||||
@click="showAccountPage(props.row.id)"
|
@click="showAccountPage(props.row.id)"
|
||||||
round
|
round
|
||||||
icon="edit"
|
:icon="props.row.is_admin ? 'admin_panel_settings' : 'edit'"
|
||||||
size="sm"
|
size="sm"
|
||||||
color="secondary"
|
:color="props.row.is_admin ? 'primary' : 'secondary'"
|
||||||
class="q-ml-xs"
|
class="q-ml-xs"
|
||||||
>
|
>
|
||||||
<q-tooltip>
|
<q-tooltip>
|
||||||
@@ -605,10 +625,18 @@
|
|||||||
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.is_admin"
|
v-model="props.row.activated"
|
||||||
@update:model-value="toggleAdmin(props.row.id)"
|
@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-toggle>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
|
|||||||
@@ -292,7 +292,8 @@ 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:
|
||||||
raise ValueError("Could not fetch any Bitcoin price.")
|
logger.warning("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.")
|
||||||
|
|
||||||
@@ -306,7 +307,8 @@ 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,
|
||||||
)
|
)
|
||||||
return float(100_000_000 / price), price
|
rate = float(100_000_000 / price) if price > 0 else 0.0
|
||||||
|
return rate, price
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_rate_satoshis(currency: str) -> float:
|
async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||||
@@ -316,9 +318,13 @@ 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)
|
||||||
return int(amount * (rate))
|
if rate > 0:
|
||||||
|
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)
|
||||||
return float(amount / rate)
|
if rate > 0:
|
||||||
|
return float(amount / rate)
|
||||||
|
raise ValueError(f"Could not get exchange rate for {currency}.")
|
||||||
|
|||||||
@@ -297,6 +297,148 @@ 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]
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
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
|
||||||
@@ -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)
|
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
|
||||||
|
|||||||
@@ -341,3 +341,7 @@ 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 = []
|
||||||
|
|||||||
Reference in New Issue
Block a user