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 |
+31
-26
@@ -98,19 +98,17 @@ async def get_accounts(
|
||||
|
||||
|
||||
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:
|
||||
if len(user_id) == 0:
|
||||
return None
|
||||
|
||||
activate_clause = "" if activated is None else "AND activated = :activated"
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
f"""
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE id = :id {activate_clause}
|
||||
""", # noqa: S608
|
||||
{"id": user_id, "activated": activated},
|
||||
WHERE id = :id AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"id": user_id, "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
@@ -136,7 +134,7 @@ async def delete_accounts_no_wallets(
|
||||
|
||||
|
||||
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:
|
||||
if len(username) == 0:
|
||||
return None
|
||||
@@ -144,28 +142,32 @@ async def get_account_by_username(
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
if len(email) == 0:
|
||||
return None
|
||||
@@ -173,35 +175,38 @@ async def get_account_by_email(
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_username_or_email(
|
||||
username_or_email: str,
|
||||
activated: bool = True,
|
||||
active_only: bool = True,
|
||||
conn: Connection | None = None,
|
||||
) -> Account | None:
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE (LOWER(email) = :value or LOWER(username) = :value)
|
||||
AND activated = :activated
|
||||
WHERE
|
||||
(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,
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
return None
|
||||
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(
|
||||
user_id: str, activated: bool = True, conn: Connection | None = None
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> UserAcls:
|
||||
user_acls = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT id, access_control_list FROM accounts
|
||||
WHERE id = :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,
|
||||
)
|
||||
|
||||
@@ -261,7 +266,7 @@ async def get_user_access_control_lists(
|
||||
|
||||
|
||||
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:
|
||||
clear_user_cache(user)
|
||||
|
||||
|
||||
@@ -776,7 +776,7 @@ async def _pay_internal_invoice(
|
||||
await update_payment(internal_payment, conn=conn)
|
||||
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
||||
|
||||
send_payment_notification_in_background(wallet, payment)
|
||||
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
||||
|
||||
# notify receiver asynchronously
|
||||
from lnbits.tasks import internal_invoice_queue
|
||||
@@ -849,7 +849,8 @@ async def _pay_external_invoice(
|
||||
payment = await update_payment_success_status(
|
||||
payment, payment_response, conn=conn
|
||||
)
|
||||
send_payment_notification_in_background(wallet, payment)
|
||||
|
||||
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
||||
logger.success(f"payment successful {payment_response.checking_id}")
|
||||
|
||||
payment.checking_id = payment_response.checking_id
|
||||
@@ -1057,3 +1058,13 @@ async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
|
||||
await update_payment(payment)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def _send_payment_notification_in_background(
|
||||
wallet_id: str, payment: Payment, conn: Connection | None = None
|
||||
):
|
||||
# fetch balance again
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if not wallet:
|
||||
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||
send_payment_notification_in_background(wallet, payment)
|
||||
|
||||
@@ -199,8 +199,11 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
||||
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 and data.invitation_code:
|
||||
code = data.invitation_code.strip()
|
||||
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:
|
||||
|
||||
@@ -98,7 +98,7 @@ async def nostr_login(request: Request) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
||||
event = _nostr_nip98_event(request)
|
||||
account = await get_account_by_pubkey(event["pubkey"])
|
||||
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
|
||||
if not account:
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
@@ -106,6 +106,8 @@ async def nostr_login(request: Request) -> JSONResponse:
|
||||
extra=UserExtra(provider="nostr"),
|
||||
)
|
||||
await create_user_account(account)
|
||||
if not account.activated:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
|
||||
return _auth_success_response(account.username or "", account.id, account.email)
|
||||
|
||||
|
||||
@@ -361,7 +363,7 @@ async def register(data: RegisterUser) -> JSONResponse:
|
||||
if not is_valid_username(data.username):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
||||
|
||||
if await get_account_by_username(data.username):
|
||||
if await get_account_by_username(data.username, active_only=False):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||
|
||||
if data.email and not is_valid_email_address(data.email):
|
||||
@@ -533,7 +535,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||
|
||||
redirect_path = "/wallet"
|
||||
account = await get_account_by_email(email)
|
||||
account = await get_account_by_email(email, active_only=False)
|
||||
|
||||
if verified_user_id:
|
||||
if account:
|
||||
|
||||
@@ -74,7 +74,7 @@ async def api_get_users(
|
||||
summary="Get user by Id",
|
||||
)
|
||||
async def api_get_user(user_id: str) -> User:
|
||||
user = await get_user(user_id, activated=None)
|
||||
user = await get_user(user_id, active_only=False)
|
||||
if not user:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||
return user
|
||||
@@ -242,7 +242,7 @@ async def api_users_toggle_activated(
|
||||
if settings.is_admin_user(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:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
|
||||
+9
-42
@@ -12,7 +12,6 @@ from typing import Any, Generic, Literal, TypeVar, get_origin
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, ValidationError, root_validator
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
@@ -56,14 +55,6 @@ def compat_timestamp_placeholder(key: str):
|
||||
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:
|
||||
type: str | None = "<inherited>"
|
||||
schema: str | None = "<inherited>"
|
||||
@@ -326,31 +317,7 @@ class Database(Compat):
|
||||
self.engine: AsyncEngine = create_async_engine(
|
||||
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()
|
||||
|
||||
logger.trace(f"database {self.type} added for {self.name}")
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -663,7 +630,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
|
||||
placeholders = []
|
||||
keys = model_to_dict(model).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)
|
||||
fields = ", ".join([f'"{key}"' for key in keys])
|
||||
values = ", ".join(placeholders)
|
||||
@@ -681,9 +648,8 @@ def update_query(
|
||||
"""
|
||||
fields = []
|
||||
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)
|
||||
fields.append(f'"{field}" = {placeholder}')
|
||||
fields.append(f'"{field}" = :{field}')
|
||||
query = ", ".join(fields)
|
||||
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):
|
||||
continue
|
||||
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
|
||||
if (
|
||||
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:
|
||||
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
||||
else:
|
||||
# Ensure PostgreSQL datetime values have timezone info
|
||||
if isinstance(value, datetime) and value.tzinfo is None:
|
||||
_dict[key] = value.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
_dict[key] = value
|
||||
_dict[key] = value.replace(tzinfo=timezone.utc)
|
||||
continue
|
||||
if issubclass(type_, BaseModel):
|
||||
_dict[key] = dict_to_submodel(type_, value)
|
||||
|
||||
+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
@@ -566,12 +566,6 @@ window.app.component('username-password', {
|
||||
this.confirmationMethod !== 'code' ||
|
||||
this.confirmationCode.length > 0
|
||||
|
||||
console.log('### disableRegister', {
|
||||
usernameOK,
|
||||
passwordOK,
|
||||
passwordsMatch,
|
||||
codeOk
|
||||
})
|
||||
return !usernameOK || !passwordOK || !passwordsMatch || !codeOk
|
||||
},
|
||||
confirmationMethodsCount() {
|
||||
|
||||
@@ -71,6 +71,10 @@ window.app.component('lnbits-wallet-extra', {
|
||||
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
||||
this.g.exchangeRate
|
||||
)
|
||||
if (this.g.exchangeRate <= 0) {
|
||||
this.g.fiatTracking = false
|
||||
this.g.isFiatPriority = false
|
||||
}
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ window._lnbitsUtils = {
|
||||
return null
|
||||
}
|
||||
},
|
||||
notifyApiError(error) {
|
||||
async notifyApiError(error) {
|
||||
if (!error.response) {
|
||||
return console.error(error)
|
||||
}
|
||||
@@ -154,6 +154,10 @@ window._lnbitsUtils = {
|
||||
500: 'negative'
|
||||
}
|
||||
let messages = error.response.data.detail
|
||||
if (!messages) {
|
||||
const text = await error.response.data?.text()
|
||||
messages = this.parseJSONSafe(text)?.detail
|
||||
}
|
||||
if (messages) {
|
||||
messages = Array.isArray(messages)
|
||||
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
"
|
||||
:clickable="!!reviewsUrl"
|
||||
@click="openReviews(extension)"
|
||||
/>
|
||||
></lnbits-extension-rating>
|
||||
<q-btn-group size="xs" style="margin: 5px 0">
|
||||
<q-btn
|
||||
v-if="extension.hasFreeRelease"
|
||||
|
||||
@@ -292,7 +292,8 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
||||
async def btc_price(currency: str) -> float:
|
||||
rates = await btc_rates(currency)
|
||||
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:
|
||||
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}",
|
||||
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:
|
||||
@@ -316,9 +318,13 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||
|
||||
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||
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:
|
||||
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)}."
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_register_no_activation_code(
|
||||
http_client: AsyncClient, settings: Settings
|
||||
):
|
||||
settings.lnbits_require_user_activation = True
|
||||
|
||||
tiny_id = shortuuid.uuid()[:8]
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"username": f"u21.{tiny_id}",
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
"email": f"u21.{tiny_id}@lnbits.com",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json().get("detail") == "No activation method provided."
|
||||
|
||||
settings.lnbits_user_activation_by_invitation_code = True
|
||||
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"username": f"u21.{tiny_id}",
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
"email": f"u21.{tiny_id}@lnbits.com",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400, "User creation blocked without activation code."
|
||||
assert response.json().get("detail") == "Invitation code cannot be empty."
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_register_invalid_activation_code(
|
||||
http_client: AsyncClient, settings: Settings
|
||||
):
|
||||
settings.lnbits_require_user_activation = True
|
||||
settings.lnbits_user_activation_by_invitation_code = True
|
||||
settings.lnbits_register_reusable_activation_code = "foo"
|
||||
settings.lnbits_register_one_time_activation_codes = ["baz", "qux"]
|
||||
|
||||
tiny_id = shortuuid.uuid()[:8]
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"username": f"u21.{tiny_id}",
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
"email": f"u21.{tiny_id}@lnbits.com",
|
||||
"invitation_code": "bar",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json().get("detail") == "Invalid invitation code."
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_register_reusable_activation_code(
|
||||
http_client: AsyncClient, settings: Settings
|
||||
):
|
||||
settings.lnbits_require_user_activation = True
|
||||
settings.lnbits_user_activation_by_invitation_code = True
|
||||
settings.lnbits_register_reusable_activation_code = "foo"
|
||||
|
||||
tiny_id = shortuuid.uuid()[:8]
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"username": f"u21.{tiny_id}",
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
"email": f"u21.{tiny_id}@lnbits.com",
|
||||
"invitation_code": "foo",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, "User created with reusable code."
|
||||
assert response.json().get("access_token") is not None
|
||||
|
||||
# Register again with the same code
|
||||
tiny_id = shortuuid.uuid()[:8]
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"username": f"u21.{tiny_id}",
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
"email": f"u21.{tiny_id}@lnbits.com",
|
||||
"invitation_code": "foo",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, "User created with reusable code."
|
||||
assert response.json().get("access_token") is not None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_register_one_time_activation_code(
|
||||
http_client: AsyncClient, settings: Settings
|
||||
):
|
||||
settings.lnbits_require_user_activation = True
|
||||
settings.lnbits_user_activation_by_invitation_code = True
|
||||
settings.lnbits_register_reusable_activation_code = "foo"
|
||||
settings.lnbits_register_one_time_activation_codes = ["baz", "qux"]
|
||||
|
||||
tiny_id = shortuuid.uuid()[:8]
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"username": f"u21.{tiny_id}",
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
"email": f"u21.{tiny_id}@lnbits.com",
|
||||
"invitation_code": "baz",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, "User created with one-time code."
|
||||
assert response.json().get("access_token") is not None
|
||||
|
||||
# Register again with the same code
|
||||
tiny_id = shortuuid.uuid()[:8]
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"username": f"u21.{tiny_id}",
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
"email": f"u21.{tiny_id}@lnbits.com",
|
||||
"invitation_code": "baz",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400, "Invalid invitation code."
|
||||
assert response.json().get("detail") == "Invalid invitation code."
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_register_email_twice(http_client: AsyncClient):
|
||||
tiny_id = shortuuid.uuid()[:8]
|
||||
|
||||
@@ -341,3 +341,7 @@ def _settings_cleanup(settings: Settings):
|
||||
settings.lnbits_max_outgoing_payment_amount_sats = 10_000_000_100
|
||||
settings.lnbits_max_incoming_payment_amount_sats = 10_000_000_200
|
||||
settings.stripe_limits = FiatProviderLimits()
|
||||
settings.lnbits_require_user_activation = False
|
||||
settings.lnbits_user_activation_by_invitation_code = False
|
||||
settings.lnbits_register_reusable_activation_code = ""
|
||||
settings.lnbits_register_one_time_activation_codes = []
|
||||
|
||||
Reference in New Issue
Block a user