Compare commits

..
Author SHA1 Message Date
Vlad Stan 18ce66e507 chore: make bundle 2026-02-05 12:21:08 +02:00
Vlad Stan acce8d726b feat: configure activation codes 2026-02-05 12:21:08 +02:00
Vlad Stan 164e498476 [feat] Allow no exchange providers (#3763) 2026-02-05 12:21:08 +02:00
Vlad Stan 48e0ca80bd fix: handle validation errors when the response is binary (#3765) 2026-02-05 12:21:08 +02:00
Vlad Stan 0f4930f6c8 fix: settings reset 2026-02-05 12:21:08 +02:00
Vlad Stan e0cade01c9 test: one time code 2026-02-05 12:21:08 +02:00
Vlad Stan dd79dbc90b test: invalid code, reusable code 2026-02-05 12:21:08 +02:00
Vlad Stan 9e1ac86761 test: no code provided 2026-02-05 12:21:08 +02:00
Vlad Stan abfc8ebfb4 fix: handle empty strings better 2026-02-05 12:21:08 +02:00
Vlad Stan 872c24e6ea chore: code clean-up 2026-02-05 12:21:08 +02:00
Vlad Stan 6dcff91bf7 fix: remove nostr message 2026-02-05 12:21:08 +02:00
Vlad Stan ef253de426 feat: better wording 2026-02-05 12:21:08 +02:00
Vlad Stan 0819f51c3a feat: check invitation code 2026-02-05 12:21:08 +02:00
Vlad Stan d1cae5341f feat: update settings 2026-02-05 12:21:08 +02:00
Vlad Stan 3b249fbad2 feat: send invitation code to backend 2026-02-05 12:21:08 +02:00
Vlad Stan 9648587850 feat: add confirmation options UI 2026-02-05 12:21:08 +02:00
Vlad Stan a5523c1bf6 feat: configure ui 2026-02-05 12:21:08 +02:00
Vlad Stan eb84621f23 feat: configure activation codes 2026-02-05 12:21:08 +02:00
Vlad Stan b702229cb1 feat: add some info 2026-02-05 12:21:02 +02:00
Vlad Stan c722776bc0 refactor: reorder fields 2026-02-05 12:21:01 +02:00
Vlad Stan bd7917ab20 feat: add ui config 2026-02-05 12:21:01 +02:00
Vlad Stan db9006ea8b refactor: better query 2026-02-05 12:21:01 +02:00
Vlad Stan 15f2048bcb refactor: code cleanup 2026-02-05 12:21:01 +02:00
Vlad Stan b0079871b0 chore: clean-up 2026-02-05 12:21:01 +02:00
Vlad Stan 2bedcae9f1 refactor: simplify queries 2026-02-05 12:21:01 +02:00
Vlad Stan 341974fb35 refactor: use normal update 2026-02-05 12:21:01 +02:00
Vlad Stan d6bfbb0045 fix: better message 2026-02-05 12:21:01 +02:00
Vlad Stan dc9338b991 feat: add back toggle admin 2026-02-05 12:21:01 +02:00
Vlad Stan 6f6f7d4542 fix: clear cache 2026-02-05 12:21:01 +02:00
Vlad Stan 617f9c14ed feat: clear cache on user deactivation 2026-02-05 12:21:01 +02:00
Vlad Stan 54fdb0948c feat: basic account activate/deactivate 2026-02-05 12:21:01 +02:00
Vlad StanandGitHub 2fe7ab4f83 [feat] Allow no exchange providers (#3763) 2026-02-05 11:11:48 +01:00
dni ⚡andGitHub 10c8ca4ca1 fix: datetime in db.py added utc offset (#3762) 2026-02-05 11:05:58 +01:00
Vlad StanandGitHub f079762b71 fix: handle validation errors when the response is binary (#3765) 2026-02-05 11:55:40 +02:00
Vlad StanandGitHub 2e042d2597 fix: extension paid/free label (#3764) 2026-02-05 11:32:46 +02:00
Vlad StanandGitHub 49b57c9f0b [feat] User activation (#3749) 2026-02-05 11:31:47 +02:00
dni ⚡andGitHub 656c6cac5b hotfix: websocket with old balance was sent. (#3759) 2026-02-03 12:23:49 +01:00
15 changed files with 233 additions and 91 deletions
+31 -26
View File
@@ -98,19 +98,17 @@ async def get_accounts(
async def get_account( async def get_account(
user_id: str, activated: bool | None = True, conn: Connection | None = None user_id: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None: ) -> Account | None:
if len(user_id) == 0: if len(user_id) == 0:
return None return None
activate_clause = "" if activated is None else "AND activated = :activated"
return await (conn or db).fetchone( return await (conn or db).fetchone(
f""" """
SELECT * FROM accounts SELECT * FROM accounts
WHERE id = :id {activate_clause} WHERE id = :id AND (activated = true OR activated = :activated)
""", # noqa: S608 """,
{"id": user_id, "activated": activated}, {"id": user_id, "activated": active_only},
Account, Account,
) )
@@ -136,7 +134,7 @@ async def delete_accounts_no_wallets(
async def get_account_by_username( async def get_account_by_username(
username: str, activated: bool = True, 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
@@ -144,28 +142,32 @@ async def get_account_by_username(
return await (conn or db).fetchone( return await (conn or db).fetchone(
""" """
SELECT * FROM accounts SELECT * FROM accounts
WHERE LOWER(username) = :username AND activated = :activated WHERE
LOWER(username) = :username
AND (activated = true OR activated = :activated)
""", """,
{"username": username.lower(), "activated": activated}, {"username": username.lower(), "activated": active_only},
Account, Account,
) )
async def get_account_by_pubkey( async def get_account_by_pubkey(
pubkey: str, activated: bool | None = True, 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 SELECT * FROM accounts
WHERE LOWER(pubkey) = :pubkey AND activated = :activated WHERE
LOWER(pubkey) = :pubkey
AND (activated = true OR activated = :activated)
""", """,
{"pubkey": pubkey.lower(), "activated": activated}, {"pubkey": pubkey.lower(), "activated": active_only},
Account, Account,
) )
async def get_account_by_email( async def get_account_by_email(
email: str, activated: bool = True, 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
@@ -173,35 +175,38 @@ async def get_account_by_email(
return await (conn or db).fetchone( return await (conn or db).fetchone(
""" """
SELECT * FROM accounts SELECT * FROM accounts
WHERE LOWER(email) = :email AND activated = :activated WHERE
LOWER(email) = :email
AND (activated = true OR activated = :activated)
""", """,
{"email": email.lower(), "activated": activated}, {"email": email.lower(), "activated": active_only},
Account, Account,
) )
async def get_account_by_username_or_email( async def get_account_by_username_or_email(
username_or_email: str, username_or_email: str,
activated: bool = True, active_only: bool = True,
conn: Connection | None = None, 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
AND activated = :activated (LOWER(email) = :value or LOWER(username) = :value)
AND (activated = true OR activated = :activated)
""", """,
{"value": username_or_email.lower(), "activated": activated}, {"value": username_or_email.lower(), "activated": active_only},
Account, Account,
) )
async def get_user( async def get_user(
user_id: str, activated: bool | None = True, conn: Connection | None = None user_id: str, active_only: bool = True, conn: Connection | None = None
) -> User | 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, activated=activated, 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)
@@ -246,14 +251,14 @@ async def update_user_access_control_list(
async def get_user_access_control_lists( async def get_user_access_control_lists(
user_id: str, activated: bool = True, 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 SELECT id, access_control_list FROM accounts
WHERE id = :user_id AND activated = :activated WHERE id = :user_id AND (activated = true OR activated = :activated)
""", """,
{"user_id": user_id, "activated": activated}, {"user_id": user_id, "activated": active_only},
UserAcls, UserAcls,
) )
@@ -261,7 +266,7 @@ async def get_user_access_control_lists(
async def clear_user_id_cache(user_id: str): async def clear_user_id_cache(user_id: str):
user = await get_user(user_id, activated=None) user = await get_user(user_id, active_only=True)
if user: if user:
clear_user_cache(user) clear_user_cache(user)
+13 -2
View File
@@ -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)
+5 -2
View File
@@ -199,8 +199,11 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
async def check_register_activation_settings(data: RegisterUser): async def check_register_activation_settings(data: RegisterUser):
if not settings.lnbits_require_user_activation: if not settings.lnbits_require_user_activation:
return None return None
if settings.lnbits_user_activation_by_invitation_code and data.invitation_code: if settings.lnbits_user_activation_by_invitation_code:
code = data.invitation_code.strip() 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: if code == settings.lnbits_register_reusable_activation_code:
return None return None
if code in settings.lnbits_register_one_time_activation_codes: if code in settings.lnbits_register_one_time_activation_codes:
+5 -3
View File
@@ -98,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,
@@ -106,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)
@@ -361,7 +363,7 @@ 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):
@@ -533,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:
+2 -2
View File
@@ -74,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, activated=None) 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
@@ -242,7 +242,7 @@ async def api_users_toggle_activated(
if settings.is_admin_user(user_id): if settings.is_admin_user(user_id):
settings.lnbits_admin_users.remove(user_id) settings.lnbits_admin_users.remove(user_id)
user_account = await get_account(user_id, activated=None) user_account = await get_account(user_id, active_only=False)
if not user_account: if not user_account:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, status_code=HTTPStatus.NOT_FOUND,
+9 -42
View File
@@ -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)
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
-6
View File
@@ -566,12 +566,6 @@ window.app.component('username-password', {
this.confirmationMethod !== 'code' || this.confirmationMethod !== 'code' ||
this.confirmationCode.length > 0 this.confirmationCode.length > 0
console.log('### disableRegister', {
usernameOK,
passwordOK,
passwordsMatch,
codeOk
})
return !usernameOK || !passwordOK || !passwordsMatch || !codeOk return !usernameOK || !passwordOK || !passwordsMatch || !codeOk
}, },
confirmationMethodsCount() { confirmationMethodsCount() {
@@ -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))
} }
+5 -1
View File
@@ -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('/')})`)
+1 -1
View File
@@ -178,7 +178,7 @@
" "
:clickable="!!reviewsUrl" :clickable="!!reviewsUrl"
@click="openReviews(extension)" @click="openReviews(extension)"
/> ></lnbits-extension-rating>
<q-btn-group size="xs" style="margin: 5px 0"> <q-btn-group size="xs" style="margin: 5px 0">
<q-btn <q-btn
v-if="extension.hasFreeRelease" v-if="extension.hasFreeRelease"
+10 -4
View File
@@ -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}.")
+142
View File
@@ -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]
+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_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 = []