Compare commits

..
Author SHA1 Message Date
Vlad Stan e0cf512d3e fix: remove nostr message 2026-02-03 15:17:49 +02:00
Vlad Stan b86720f59e feat: better wording 2026-02-03 14:40:46 +02:00
Vlad Stan 13c370fb08 feat: check invitation code 2026-02-03 11:21:47 +02:00
Vlad Stan 3b4e8ace6a feat: update settings 2026-02-03 10:29:53 +02:00
Vlad Stan 390c616998 feat: send invitation code to backend 2026-02-03 10:24:00 +02:00
Vlad Stan e5e6ee3628 feat: add confirmation options UI 2026-02-02 18:36:49 +02:00
Vlad Stan f962502610 feat: configure ui 2026-02-02 16:35:57 +02:00
Vlad Stan f40cb536ba feat: configure activation codes 2026-02-02 16:29:52 +02:00
Vlad Stan db9a9eb8c2 feat: add some info 2026-02-02 16:29:52 +02:00
Vlad Stan 8ba662ff99 refactor: reorder fields 2026-02-02 16:29:52 +02:00
Vlad Stan e5acd6188e feat: add ui config 2026-02-02 16:29:52 +02:00
Vlad Stan 46ebb6ce27 refactor: code cleanup 2026-02-02 16:29:35 +02:00
Vlad Stan c7398f6314 chore: clean-up 2026-02-02 12:37:48 +02:00
Vlad Stan efa6398c51 chore: bundle 2026-02-02 12:35:34 +02:00
Vlad Stan 7179dbfeef refactor: simplify queries 2026-02-02 12:34:57 +02:00
Vlad Stan 4021648082 refactor: use normal update 2026-02-02 12:34:57 +02:00
Vlad Stan d5236aa88f test: add more check 2026-02-02 12:34:55 +02:00
Vlad Stan ff4132ef95 test: login after user disabled does not work 2026-02-02 12:34:55 +02:00
Vlad Stan 7b2805ef46 fix: column selection 2026-02-02 12:34:47 +02:00
Vlad Stan 3bbfc2a58c fix: better message 2026-02-02 12:34:47 +02:00
Vlad Stan 00795a186d feat: add back toggle admin 2026-02-02 12:34:47 +02:00
Vlad Stan fa0c57ce3d fix: only fetch keys for activated users 2026-02-02 12:34:47 +02:00
Vlad Stan ff6959095f fix: clear cache 2026-02-02 12:34:47 +02:00
Vlad Stan e951d02c3d feat: clear cache on user deactivation 2026-02-02 12:34:47 +02:00
Vlad Stan fadad38c99 feat: basic account activate/deactivate 2026-02-02 12:34:47 +02:00
9 changed files with 42 additions and 203 deletions
+26 -31
View File
@@ -98,17 +98,19 @@ async def get_accounts(
async def get_account(
user_id: str, active_only: bool = True, conn: Connection | None = None
user_id: str, activated: bool | None = 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 AND (activated = true OR activated = :activated)
""",
{"id": user_id, "activated": active_only},
WHERE id = :id {activate_clause}
""", # noqa: S608
{"id": user_id, "activated": activated},
Account,
)
@@ -134,7 +136,7 @@ async def delete_accounts_no_wallets(
async def get_account_by_username(
username: str, active_only: bool = True, conn: Connection | None = None
username: str, activated: bool = True, conn: Connection | None = None
) -> Account | None:
if len(username) == 0:
return None
@@ -142,32 +144,28 @@ async def get_account_by_username(
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
LOWER(username) = :username
AND (activated = true OR activated = :activated)
WHERE LOWER(username) = :username AND activated = :activated
""",
{"username": username.lower(), "activated": active_only},
{"username": username.lower(), "activated": activated},
Account,
)
async def get_account_by_pubkey(
pubkey: str, active_only: bool = True, conn: Connection | None = None
pubkey: str, activated: bool | None = True, conn: Connection | None = None
) -> Account | None:
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
LOWER(pubkey) = :pubkey
AND (activated = true OR activated = :activated)
WHERE LOWER(pubkey) = :pubkey AND activated = :activated
""",
{"pubkey": pubkey.lower(), "activated": active_only},
{"pubkey": pubkey.lower(), "activated": activated},
Account,
)
async def get_account_by_email(
email: str, active_only: bool = True, conn: Connection | None = None
email: str, activated: bool = True, conn: Connection | None = None
) -> Account | None:
if len(email) == 0:
return None
@@ -175,38 +173,35 @@ async def get_account_by_email(
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
LOWER(email) = :email
AND (activated = true OR activated = :activated)
WHERE LOWER(email) = :email AND activated = :activated
""",
{"email": email.lower(), "activated": active_only},
{"email": email.lower(), "activated": activated},
Account,
)
async def get_account_by_username_or_email(
username_or_email: str,
active_only: bool = True,
activated: 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 = true OR activated = :activated)
WHERE (LOWER(email) = :value or LOWER(username) = :value)
AND activated = :activated
""",
{"value": username_or_email.lower(), "activated": active_only},
{"value": username_or_email.lower(), "activated": activated},
Account,
)
async def get_user(
user_id: str, active_only: bool = True, conn: Connection | None = None
user_id: str, activated: bool | None = 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, active_only, conn=conn)
account = await get_account(user_id, activated=activated, conn=conn)
if not account:
return None
return await get_user_from_account(account, conn=conn)
@@ -251,14 +246,14 @@ async def update_user_access_control_list(
async def get_user_access_control_lists(
user_id: str, active_only: bool = True, conn: Connection | None = None
user_id: str, activated: 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 = true OR activated = :activated)
WHERE id = :user_id AND activated = :activated
""",
{"user_id": user_id, "activated": active_only},
{"user_id": user_id, "activated": activated},
UserAcls,
)
@@ -266,7 +261,7 @@ async def get_user_access_control_lists(
async def clear_user_id_cache(user_id: str):
user = await get_user(user_id, active_only=True)
user = await get_user(user_id, activated=None)
if user:
clear_user_cache(user)
+2 -13
View File
@@ -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}")
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
send_payment_notification_in_background(wallet, payment)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue
@@ -849,8 +849,7 @@ async def _pay_external_invoice(
payment = await update_payment_success_status(
payment, payment_response, conn=conn
)
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
send_payment_notification_in_background(wallet, payment)
logger.success(f"payment successful {payment_response.checking_id}")
payment.checking_id = payment_response.checking_id
@@ -1058,13 +1057,3 @@ 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)
+2 -5
View File
@@ -199,11 +199,8 @@ 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:
code = data.invitation_code.strip() if data.invitation_code else ""
if len(code) == 0:
raise ValueError("Invitation code cannot be empty.")
if settings.lnbits_user_activation_by_invitation_code and data.invitation_code:
code = data.invitation_code.strip()
if code == settings.lnbits_register_reusable_activation_code:
return None
if code in settings.lnbits_register_one_time_activation_codes:
+3 -5
View File
@@ -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"], active_only=False)
account = await get_account_by_pubkey(event["pubkey"])
if not account:
account = Account(
id=uuid4().hex,
@@ -106,8 +106,6 @@ 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)
@@ -363,7 +361,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, active_only=False):
if await get_account_by_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
if data.email and not is_valid_email_address(data.email):
@@ -535,7 +533,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, active_only=False)
account = await get_account_by_email(email)
if verified_user_id:
if account:
+2 -2
View File
@@ -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, active_only=False)
user = await get_user(user_id, activated=None)
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, active_only=False)
user_account = await get_account(user_id, activated=None)
if not user_account:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
File diff suppressed because one or more lines are too long
+6
View File
@@ -566,6 +566,12 @@ 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() {
-142
View File
@@ -297,148 +297,6 @@ 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]
-4
View File
@@ -341,7 +341,3 @@ def _settings_cleanup(settings: Settings):
settings.lnbits_max_outgoing_payment_amount_sats = 10_000_000_100
settings.lnbits_max_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 = []