Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8705650d8 | ||
|
|
8a9a96a52c | ||
|
|
ad028b303c | ||
|
|
b744af1de5 |
+21
-75
@@ -4,12 +4,10 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||
from lnbits.core.crud.wallets import clear_wallet_cache, create_wallet, get_wallets
|
||||
from lnbits.core.crud.wallets import create_wallet, get_wallets
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import UserAcls
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
from lnbits.helpers import sha256s
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
from ..models import (
|
||||
Account,
|
||||
@@ -43,7 +41,6 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
||||
"DELETE from accounts WHERE id = :user",
|
||||
{"user": user_id},
|
||||
)
|
||||
await clear_user_id_cache(user_id)
|
||||
|
||||
|
||||
async def get_accounts(
|
||||
@@ -72,7 +69,6 @@ async def get_accounts(
|
||||
accounts.email,
|
||||
accounts.pubkey,
|
||||
accounts.external_id,
|
||||
accounts.activated,
|
||||
SUM(COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
), 0)) as balance_msat,
|
||||
@@ -97,18 +93,12 @@ async def get_accounts(
|
||||
)
|
||||
|
||||
|
||||
async def get_account(
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
|
||||
if len(user_id) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE id = :id AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"id": user_id, "activated": active_only},
|
||||
"SELECT * FROM accounts WHERE id = :id",
|
||||
{"id": user_id},
|
||||
Account,
|
||||
)
|
||||
|
||||
@@ -134,79 +124,55 @@ async def delete_accounts_no_wallets(
|
||||
|
||||
|
||||
async def get_account_by_username(
|
||||
username: str, active_only: bool = True, conn: Connection | None = None
|
||||
username: str, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(username) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(username) = :username
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"username": username.lower(), "activated": active_only},
|
||||
"SELECT * FROM accounts WHERE LOWER(username) = :username",
|
||||
{"username": username.lower()},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_pubkey(
|
||||
pubkey: str, active_only: bool = True, conn: Connection | None = None
|
||||
pubkey: str, 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)
|
||||
""",
|
||||
{"pubkey": pubkey.lower(), "activated": active_only},
|
||||
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
||||
{"pubkey": pubkey.lower()},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_email(
|
||||
email: str, active_only: bool = True, conn: Connection | None = None
|
||||
email: str, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(email) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(email) = :email
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"email": email.lower(), "activated": active_only},
|
||||
"SELECT * FROM accounts WHERE LOWER(email) = :email",
|
||||
{"email": email.lower()},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_username_or_email(
|
||||
username_or_email: str,
|
||||
active_only: bool = True,
|
||||
conn: Connection | None = None,
|
||||
username_or_email: str, 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
|
||||
""",
|
||||
{"value": username_or_email.lower(), "activated": active_only},
|
||||
{"value": username_or_email.lower()},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_user(
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> User | None:
|
||||
async def get_user(user_id: str, 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, conn=conn)
|
||||
if not account:
|
||||
return None
|
||||
return await get_user_from_account(account, conn=conn)
|
||||
@@ -225,7 +191,6 @@ async def get_user_from_account(
|
||||
|
||||
return User(
|
||||
id=account.id,
|
||||
activated=account.activated,
|
||||
email=account.email,
|
||||
username=account.username,
|
||||
pubkey=account.pubkey,
|
||||
@@ -251,31 +216,12 @@ 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, 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)
|
||||
""",
|
||||
{"user_id": user_id, "activated": active_only},
|
||||
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
||||
{"id": user_id},
|
||||
UserAcls,
|
||||
)
|
||||
|
||||
return user_acls or UserAcls(id=user_id)
|
||||
|
||||
|
||||
async def clear_user_id_cache(user_id: str):
|
||||
user = await get_user(user_id, active_only=True)
|
||||
if user:
|
||||
clear_user_cache(user)
|
||||
|
||||
|
||||
def clear_user_cache(user: User):
|
||||
user_cache_key: str | None = cache.pop(
|
||||
f"auth:user:cache_key:{sha256s(user.id)}", None
|
||||
)
|
||||
if user_cache_key:
|
||||
cache.pop(user_cache_key)
|
||||
for wallet in user.wallets:
|
||||
clear_wallet_cache(wallet)
|
||||
|
||||
@@ -50,7 +50,7 @@ async def delete_wallet(
|
||||
deleted: bool = True,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
clear_wallet_id_cache(wallet_id)
|
||||
_clear_wallet_cache(wallet_id)
|
||||
now = int(time())
|
||||
|
||||
await (conn or db).execute(
|
||||
@@ -65,7 +65,7 @@ async def delete_wallet(
|
||||
|
||||
|
||||
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
||||
clear_wallet_id_cache(wallet_id)
|
||||
_clear_wallet_cache(wallet_id)
|
||||
await (conn or db).execute(
|
||||
"DELETE FROM wallets WHERE id = :wallet",
|
||||
{"wallet": wallet_id},
|
||||
@@ -75,7 +75,7 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
|
||||
async def delete_wallet_by_id(
|
||||
wallet_id: str, conn: Connection | None = None
|
||||
) -> int | None:
|
||||
clear_wallet_id_cache(wallet_id)
|
||||
_clear_wallet_cache(wallet_id)
|
||||
now = int(time())
|
||||
result = await (conn or db).execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
@@ -226,14 +226,11 @@ async def get_wallet_for_key(
|
||||
) -> Wallet | None:
|
||||
wallet = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT wallets.*, COALESCE((
|
||||
SELECT *, COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
), 0)
|
||||
AS balance_msat FROM wallets
|
||||
INNER JOIN accounts ON wallets.user = accounts.id
|
||||
WHERE (adminkey = :key OR inkey = :key)
|
||||
AND deleted = false
|
||||
AND accounts.activated = true
|
||||
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
||||
""",
|
||||
{"key": key},
|
||||
Wallet,
|
||||
@@ -253,11 +250,8 @@ async def get_base_wallet_for_key(
|
||||
) -> BaseWallet | None:
|
||||
wallet = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets
|
||||
INNER JOIN accounts ON wallets.user = accounts.id
|
||||
WHERE (adminkey = :key OR inkey = :key)
|
||||
AND deleted = false
|
||||
AND accounts.activated = true
|
||||
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
|
||||
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
||||
""",
|
||||
{"key": key},
|
||||
BaseWallet,
|
||||
@@ -300,14 +294,8 @@ async def get_total_balance(conn: Connection | None = None):
|
||||
return row.get("balance", 0) or 0
|
||||
|
||||
|
||||
def clear_wallet_id_cache(wallet_id: str):
|
||||
def _clear_wallet_cache(wallet_id):
|
||||
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
||||
if cached_wallet:
|
||||
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
||||
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
||||
|
||||
|
||||
def clear_wallet_cache(wallet: Wallet):
|
||||
cache.pop(f"auth:wallet:{wallet.id}")
|
||||
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
||||
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
||||
|
||||
@@ -856,11 +856,3 @@ async def m043_add_ui_customization_to_accounts(db: Connection):
|
||||
Used for server side persistence of UI customization settings.
|
||||
"""
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
||||
|
||||
|
||||
async def m044_add_activated_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds activated column to accounts.
|
||||
Used for account activation status.
|
||||
"""
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
|
||||
|
||||
@@ -181,7 +181,6 @@ class AccountId(BaseModel):
|
||||
|
||||
|
||||
class Account(AccountId):
|
||||
activated: bool = True
|
||||
external_id: str | None = None # for external account linking
|
||||
username: str | None = None
|
||||
password_hash: str | None = None
|
||||
@@ -242,7 +241,6 @@ class Account(AccountId):
|
||||
|
||||
|
||||
class AccountOverview(Account):
|
||||
activated: bool = True
|
||||
transaction_count: int | None = 0
|
||||
wallet_count: int | None = 0
|
||||
balance_msat: int | None = 0
|
||||
@@ -278,7 +276,6 @@ class AccountFilters(FilterModel):
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
activated: bool = True
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
email: str | None = None
|
||||
@@ -318,7 +315,6 @@ class RegisterUser(BaseModel):
|
||||
username: str = Query(default=..., min_length=2, max_length=20)
|
||||
password: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||
invitation_code: str | None = Query(default=None, min_length=1, max_length=256)
|
||||
|
||||
|
||||
class CreateUser(BaseModel):
|
||||
|
||||
@@ -3,10 +3,8 @@ from uuid import uuid4
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.crud.settings import set_settings_field
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.extensions import UserExtension
|
||||
from lnbits.core.models.users import RegisterUser
|
||||
from lnbits.db import Connection
|
||||
from lnbits.settings import (
|
||||
EditableSettings,
|
||||
@@ -194,25 +192,3 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
||||
|
||||
editable_settings = EditableSettings.from_dict(settings.dict())
|
||||
return await create_admin_settings(account.id, editable_settings.dict())
|
||||
|
||||
|
||||
async def check_register_activation_settings(data: RegisterUser):
|
||||
if not settings.lnbits_require_user_activation:
|
||||
return None
|
||||
if settings.lnbits_user_activation_by_invitation_code:
|
||||
code = data.invitation_code.strip() if data.invitation_code else ""
|
||||
if len(code) == 0:
|
||||
raise ValueError("Invitation code cannot be empty.")
|
||||
|
||||
if code == settings.lnbits_register_reusable_activation_code:
|
||||
return None
|
||||
if code in settings.lnbits_register_one_time_activation_codes:
|
||||
settings.lnbits_register_one_time_activation_codes.remove(code)
|
||||
await set_settings_field(
|
||||
"lnbits_register_one_time_activation_codes",
|
||||
settings.lnbits_register_one_time_activation_codes,
|
||||
)
|
||||
return None
|
||||
raise ValueError("Invalid invitation code.")
|
||||
|
||||
raise ValueError("No activation method provided.")
|
||||
|
||||
@@ -26,10 +26,7 @@ from lnbits.core.models.users import (
|
||||
UpdateAccessControlList,
|
||||
)
|
||||
from lnbits.core.services import create_user_account
|
||||
from lnbits.core.services.users import (
|
||||
check_register_activation_settings,
|
||||
update_user_account,
|
||||
)
|
||||
from lnbits.core.services.users import update_user_account
|
||||
from lnbits.decorators import (
|
||||
access_token_payload,
|
||||
check_account_exists,
|
||||
@@ -89,7 +86,6 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
||||
account = await get_account_by_username_or_email(data.username)
|
||||
if not account or not account.verify_password(data.password):
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||
|
||||
return _auth_success_response(account.username, account.id, account.email)
|
||||
|
||||
|
||||
@@ -98,7 +94,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 +102,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,14 +357,12 @@ 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):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||
|
||||
await check_register_activation_settings(data)
|
||||
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
email=data.email,
|
||||
@@ -535,7 +527,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:
|
||||
|
||||
@@ -20,7 +20,6 @@ from lnbits.core.crud import (
|
||||
update_admin_settings,
|
||||
update_wallet,
|
||||
)
|
||||
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
|
||||
from lnbits.core.crud.wallets import delete_wallet_by_id
|
||||
from lnbits.core.models import (
|
||||
AccountFilters,
|
||||
@@ -74,7 +73,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)
|
||||
if not user:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||
return user
|
||||
@@ -198,7 +197,7 @@ async def api_users_reset_password(user_id: str) -> str:
|
||||
return f"reset_key_{reset_key_b64}"
|
||||
|
||||
|
||||
@users_router.put(
|
||||
@users_router.get(
|
||||
"/user/{user_id}/admin",
|
||||
dependencies=[Depends(check_super_user)],
|
||||
name="Give or revoke admin permsisions to a user",
|
||||
@@ -221,43 +220,6 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
||||
)
|
||||
|
||||
|
||||
@users_router.put(
|
||||
"/user/{user_id}/activate",
|
||||
name="Activate or deactivate a user",
|
||||
)
|
||||
async def api_users_toggle_activated(
|
||||
user_id: str, admin_account: Account = Depends(check_admin)
|
||||
) -> SimpleStatus:
|
||||
if user_id == settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Cannot deactivate super user.",
|
||||
)
|
||||
if user_id == admin_account.id:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="You cannot deactivate yourself.",
|
||||
)
|
||||
|
||||
if settings.is_admin_user(user_id):
|
||||
settings.lnbits_admin_users.remove(user_id)
|
||||
|
||||
user_account = await get_account(user_id, active_only=False)
|
||||
if not user_account:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="User not found.",
|
||||
)
|
||||
user_account.activated = not user_account.activated
|
||||
await update_account(user_account)
|
||||
await clear_user_id_cache(user_id)
|
||||
|
||||
return SimpleStatus(
|
||||
success=True,
|
||||
message=f"User {'activated' if user_account.activated else 'deactivated'}.",
|
||||
)
|
||||
|
||||
|
||||
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
||||
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
||||
return await get_wallets(user_id, deleted=None)
|
||||
|
||||
@@ -9,7 +9,6 @@ from fastapi import (
|
||||
)
|
||||
|
||||
from lnbits.core.crud.wallets import (
|
||||
clear_wallet_cache,
|
||||
create_wallet,
|
||||
get_wallets_paginated,
|
||||
)
|
||||
@@ -38,6 +37,7 @@ from lnbits.decorators import (
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import generate_filter_params_openapi
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
from ..crud import (
|
||||
delete_wallet,
|
||||
@@ -134,7 +134,9 @@ async def api_reset_wallet_keys(
|
||||
if not wallet or wallet.user != account_id.id:
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||
|
||||
clear_wallet_cache(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}")
|
||||
|
||||
wallet.adminkey = uuid4().hex
|
||||
wallet.inkey = uuid4().hex
|
||||
|
||||
@@ -261,7 +261,6 @@ async def check_account_id_exists(
|
||||
account_id,
|
||||
expiry=settings.auth_authentication_cache_minutes * 60,
|
||||
)
|
||||
cache.set(f"auth:user:cache_key:{sha256s(account.id)}", cache_key)
|
||||
|
||||
return account_id
|
||||
|
||||
|
||||
@@ -41,14 +41,6 @@ class UsersSettings(LNbitsSettings):
|
||||
lnbits_admin_users: list[str] = Field(default=[])
|
||||
lnbits_allowed_users: list[str] = Field(default=[])
|
||||
lnbits_allow_new_accounts: bool = Field(default=True)
|
||||
lnbits_require_user_activation: bool = Field(default=False)
|
||||
|
||||
lnbits_user_activation_by_email: bool = Field(default=False)
|
||||
lnbits_user_activation_by_payment: bool = Field(default=False)
|
||||
lnbits_user_activation_by_invitation_code: bool = Field(default=False)
|
||||
|
||||
lnbits_register_reusable_activation_code: str = Field(default="")
|
||||
lnbits_register_one_time_activation_codes: list[str] = Field(default=[])
|
||||
|
||||
@property
|
||||
def new_accounts_allowed(self) -> bool:
|
||||
@@ -1196,11 +1188,6 @@ class PublicSettings(BaseModel):
|
||||
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
|
||||
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
|
||||
wallet_featured_button_icon: str | None = Field(alias="walletFeaturedButtonIcon")
|
||||
lnbits_user_activation_by_email: bool = Field(alias="userActivationByEmail")
|
||||
lnbits_user_activation_by_payment: bool = Field(alias="userActivationByPayment")
|
||||
lnbits_user_activation_by_invitation_code: bool = Field(
|
||||
alias="userActivationByInvitationCode"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings: Settings):
|
||||
@@ -1252,9 +1239,6 @@ class PublicSettings(BaseModel):
|
||||
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
|
||||
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
|
||||
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
|
||||
userActivationByEmail=settings.lnbits_user_activation_by_email,
|
||||
userActivationByPayment=settings.lnbits_user_activation_by_payment,
|
||||
userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+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,7 +171,6 @@ window.localisation.en = {
|
||||
drop_db: 'Remove Data',
|
||||
enable: 'Enable',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
pay_to_enable: 'Pay To Enable',
|
||||
enable_extension_details: 'Enable extension for current user',
|
||||
disable: 'Disable',
|
||||
@@ -179,8 +178,6 @@ window.localisation.en = {
|
||||
installed: 'Installed',
|
||||
activated: 'Activated',
|
||||
deactivated: 'Deactivated',
|
||||
activate: 'Activate',
|
||||
deactivate: 'Deactivate',
|
||||
release_notes: 'Release Notes',
|
||||
activate_extension_details: 'Make extension available/unavailable for users',
|
||||
featured: 'Featured',
|
||||
@@ -189,8 +186,6 @@ window.localisation.en = {
|
||||
only_admins_can_create_extensions:
|
||||
'Only admin accounts can create extensions',
|
||||
admin_only: 'Admin Only',
|
||||
make_user_admin: 'Make User Admin',
|
||||
revoke_admin: 'Revoke Admin',
|
||||
new_version: 'New Version',
|
||||
reviews_url: 'Reviews URL',
|
||||
reviews_url_label: 'Reviews server URL',
|
||||
@@ -285,7 +280,7 @@ window.localisation.en = {
|
||||
'Nip5 identifier to send notifications to',
|
||||
notifications_nostr_identifiers: 'Nostr Identifiers',
|
||||
notifications_nostr_identifiers_desc:
|
||||
'List of identifiers to send notifications to.',
|
||||
'List of identifiers to send notifications to',
|
||||
|
||||
notifications_telegram_config: 'Telegram Configuration',
|
||||
notifications_enable_telegram: 'Enable Telegram',
|
||||
@@ -709,25 +704,6 @@ window.localisation.en = {
|
||||
allowed_users_label: 'User ID',
|
||||
allow_creation_user: 'Allow creation of new users',
|
||||
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
||||
require_user_activation: 'Require user activation',
|
||||
require_user_activation_desc:
|
||||
'New users will be activated only after they pass one of the confirmation methods. Admins can activate users manually from the admin panel.',
|
||||
reusable_activation_code: 'Reusable activation code',
|
||||
reusable_activation_code_label: 'Reusable activation code',
|
||||
reusable_activation_code_hint:
|
||||
'This activation code can be used multiple times by different users.',
|
||||
one_time_activation_code: 'One-time activation codes',
|
||||
one_time_activation_code_label: 'Add activation code',
|
||||
one_time_activation_code_hint:
|
||||
'List of one-time activation codes. Each code can be used only once, then will be reomved from the list.',
|
||||
invitation_code: 'Invitation Code',
|
||||
invitation_code_hint: 'The invitation code that you have received.',
|
||||
email: 'Email',
|
||||
email_confirmation_hint: 'Email address to send the confirmation code to.',
|
||||
nostr_identifier: 'Nostr Identifier',
|
||||
nostr_identifier_hint:
|
||||
'Nostr nip5 identifier or <npub> to send the confirmation code to.',
|
||||
|
||||
new_user_not_allowed: 'Registration is disabled.',
|
||||
start_user_impersonation: 'Impersonate this user',
|
||||
stop_user_impersonation: 'Stop User Impersonation',
|
||||
|
||||
@@ -66,7 +66,7 @@ window._lnbitsApi = {
|
||||
name: name
|
||||
})
|
||||
},
|
||||
register(username, email, password, password_repeat, invitation_code) {
|
||||
register(username, email, password, password_repeat) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/register',
|
||||
@@ -74,8 +74,7 @@ window._lnbitsApi = {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
password_repeat,
|
||||
invitation_code
|
||||
password_repeat
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -148,7 +147,7 @@ window._lnbitsApi = {
|
||||
name: name,
|
||||
wallet_type: walletType,
|
||||
...opts
|
||||
})
|
||||
}).catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
updateWallet(name, wallet) {
|
||||
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
||||
|
||||
@@ -432,10 +432,8 @@ window.app.component('username-password', {
|
||||
username: String,
|
||||
password_1: String,
|
||||
password_2: String,
|
||||
invitationCode: String,
|
||||
resetKey: String
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
oauth: [
|
||||
@@ -447,11 +445,7 @@ window.app.component('username-password', {
|
||||
username: this.userName,
|
||||
password: this.password_1,
|
||||
passwordRepeat: this.password_2,
|
||||
reset_key: this.resetKey,
|
||||
confirmationMethod: 'code',
|
||||
confirmationEmail: '',
|
||||
confirmationCode: this.invitationCode || '',
|
||||
showConfirmationCode: false
|
||||
reset_key: this.resetKey
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -464,7 +458,6 @@ window.app.component('username-password', {
|
||||
this.$emit('update:userName', this.username)
|
||||
this.$emit('update:password_1', this.password)
|
||||
this.$emit('update:password_2', this.passwordRepeat)
|
||||
this.$emit('update:invitationCode', this.confirmationCode)
|
||||
this.$emit('register')
|
||||
},
|
||||
reset() {
|
||||
@@ -556,25 +549,6 @@ window.app.component('username-password', {
|
||||
computed: {
|
||||
showOauth() {
|
||||
return this.oauth.some(m => this.authMethods.includes(m))
|
||||
},
|
||||
disableRegister() {
|
||||
const usernameOK = !!this.username
|
||||
const passwordOK = !!this.password && this.password.length >= 8
|
||||
const passwordsMatch = this.password === this.passwordRepeat
|
||||
const codeOk =
|
||||
this.confirmationMethodsCount === 0 ||
|
||||
this.confirmationMethod !== 'code' ||
|
||||
this.confirmationCode.length > 0
|
||||
|
||||
return !usernameOK || !passwordOK || !passwordsMatch || !codeOk
|
||||
},
|
||||
confirmationMethodsCount() {
|
||||
const methods = [
|
||||
this.g.settings.userActivationByEmail,
|
||||
this.g.settings.userActivationByPayment,
|
||||
this.g.settings.userActivationByInvitationCode
|
||||
]
|
||||
return methods.filter(Boolean).length
|
||||
}
|
||||
},
|
||||
created() {}
|
||||
|
||||
@@ -4,9 +4,7 @@ window.app.component('lnbits-admin-users', {
|
||||
data() {
|
||||
return {
|
||||
formAddUser: '',
|
||||
formAddAdmin: '',
|
||||
formAddActivationCode: '',
|
||||
showReusableActivationCode: false
|
||||
formAddAdmin: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -33,24 +31,6 @@ window.app.component('lnbits-admin-users', {
|
||||
removeAdminUser(user) {
|
||||
let admin_users = this.formData.lnbits_admin_users
|
||||
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
||||
},
|
||||
addOneTimeActivationCode() {
|
||||
const code = this.formAddActivationCode
|
||||
const activationCodes =
|
||||
this.formData.lnbits_register_one_time_activation_codes
|
||||
if (code?.length && !activationCodes.includes(code)) {
|
||||
this.formData.lnbits_register_one_time_activation_codes = [
|
||||
...activationCodes,
|
||||
code
|
||||
]
|
||||
this.formAddActivationCode = ''
|
||||
}
|
||||
},
|
||||
removeOneTimeActivationCode(code) {
|
||||
const codes = this.formData.lnbits_register_one_time_activation_codes
|
||||
this.formData.lnbits_register_one_time_activation_codes = codes.filter(
|
||||
u => u !== code
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -97,7 +97,6 @@ window.app.component('lnbits-wallet-new', {
|
||||
this.g.lastWalletId = res.data.id
|
||||
this.$router.push(`/wallet/${res.data.id}`)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
@@ -11,7 +11,6 @@ window.PageHome = {
|
||||
email: '',
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
invitationCode: '',
|
||||
walletName: '',
|
||||
signup: false
|
||||
}
|
||||
@@ -41,7 +40,6 @@ window.PageHome = {
|
||||
this.username = null
|
||||
this.password = null
|
||||
this.passwordRepeat = null
|
||||
this.invitationCode = null
|
||||
|
||||
this.authAction = 'register'
|
||||
this.authMethod = authMethod
|
||||
@@ -53,8 +51,7 @@ window.PageHome = {
|
||||
this.username,
|
||||
this.email,
|
||||
this.password,
|
||||
this.passwordRepeat,
|
||||
this.invitationCode
|
||||
this.passwordRepeat
|
||||
)
|
||||
this.refreshAuthUser()
|
||||
} catch (e) {
|
||||
|
||||
@@ -70,10 +70,10 @@ window.PageUsers = {
|
||||
usersTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'activated',
|
||||
name: 'admin',
|
||||
align: 'left',
|
||||
label: this.$t('activated'),
|
||||
field: 'activated',
|
||||
label: 'Admin',
|
||||
field: 'admin',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
@@ -401,7 +401,7 @@ window.PageUsers = {
|
||||
|
||||
toggleAdmin(userId) {
|
||||
LNbits.api
|
||||
.request('PUT', `/users/api/v1/user/${userId}/admin`)
|
||||
.request('GET', `/users/api/v1/user/${userId}/admin`)
|
||||
.then(() => {
|
||||
this.fetchUsers()
|
||||
Quasar.Notify.create({
|
||||
@@ -412,19 +412,6 @@ window.PageUsers = {
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
toggleUserActivated(userId) {
|
||||
LNbits.api
|
||||
.request('PUT', `/users/api/v1/user/${userId}/activate`)
|
||||
.then(res => {
|
||||
this.fetchUsers()
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: res.data.message,
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
async showAccountPage(user_id) {
|
||||
this.activeUser.showPassword = false
|
||||
this.activeUser.showUserId = false
|
||||
|
||||
@@ -814,90 +814,16 @@ include('components/lnbits-error.vue') %}
|
||||
type="password"
|
||||
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
></q-input>
|
||||
<div
|
||||
v-if="confirmationMethodsCount > 1"
|
||||
class="row justify-center q-mb-md"
|
||||
>
|
||||
<q-tabs
|
||||
v-model="confirmationMethod"
|
||||
dense
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
>
|
||||
<q-tab
|
||||
v-if="g.settings.userActivationByInvitationCode"
|
||||
name="code"
|
||||
icon="confirmation_number"
|
||||
label="Code"
|
||||
></q-tab>
|
||||
<q-tab
|
||||
v-if="g.settings.userActivationByPayment"
|
||||
name="payment"
|
||||
icon="bolt"
|
||||
label="Payment"
|
||||
></q-tab>
|
||||
<q-tab
|
||||
v-if="g.settings.userActivationByEmail"
|
||||
name="email"
|
||||
icon="email"
|
||||
label="Email"
|
||||
></q-tab>
|
||||
</q-tabs>
|
||||
</div>
|
||||
<div v-if="confirmationMethodsCount > 0" class="q-mb-md">
|
||||
<q-tab-panels v-model="confirmationMethod">
|
||||
<q-tab-panel name="code" class="q-pa-none">
|
||||
<div
|
||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
||||
>
|
||||
<q-icon name="info" color="orange-4" class="q-mr-xs"></q-icon>
|
||||
You need an invitation code to register.
|
||||
</div>
|
||||
<div>
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="confirmationCode"
|
||||
:label="$t('invitation_code')"
|
||||
:type="showConfirmationCode ? 'text' : 'password'"
|
||||
:hint="$t('invitation_code_hint')"
|
||||
>
|
||||
<q-btn
|
||||
@click="showConfirmationCode = !showConfirmationCode"
|
||||
dense
|
||||
flat
|
||||
:icon="
|
||||
showConfirmationCode ? 'visibility_off' : 'visibility'
|
||||
"
|
||||
color="grey"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="payment">
|
||||
<div>payment</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="email" class="q-pa-none">
|
||||
<div>
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="confirmationEmail"
|
||||
:label="$t('email')"
|
||||
:hint="$t('email_confirmation_hint')"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
|
||||
<div class="row justify-end">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="disableRegister"
|
||||
:disable="
|
||||
!password ||
|
||||
!passwordRepeat ||
|
||||
!username ||
|
||||
password !== passwordRepeat
|
||||
"
|
||||
type="submit"
|
||||
class="full-width"
|
||||
:label="$t('create_account')"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
>
|
||||
</q-chip>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('allowed_users')"></span></p>
|
||||
@@ -56,10 +57,7 @@
|
||||
>
|
||||
</q-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 q-mt-sm">
|
||||
<br />
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('allow_creation_user')"></q-item-label>
|
||||
@@ -78,237 +76,8 @@
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</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>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<div v-if="formData.lnbits_require_user_activation" class="row">
|
||||
<div class="col">
|
||||
<q-list bordered class="rounded-borders">
|
||||
<q-expansion-item header-class="text-primary text-bold">
|
||||
<template v-slot:header>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="confirmation_number" size="md"></q-icon>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section> Invitation Code </q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="row items-center">
|
||||
<q-toggle
|
||||
size="md"
|
||||
:label="
|
||||
formData.lnbits_user_activation_by_invitation_code
|
||||
? $t('enabled')
|
||||
: $t('disabled')
|
||||
"
|
||||
v-model="formData.lnbits_user_activation_by_invitation_code"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</template>
|
||||
|
||||
<q-card class="q-pb-xl">
|
||||
<q-card-section>
|
||||
<div
|
||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
||||
>
|
||||
<q-icon
|
||||
name="info"
|
||||
color="orange-4"
|
||||
size="18px"
|
||||
class="q-mr-xs"
|
||||
></q-icon>
|
||||
Users will need to provide a valid invitation code during
|
||||
registration to activate their account.
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 q-pr-sm">
|
||||
<p><span v-text="$t('reusable_activation_code')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_register_reusable_activation_code"
|
||||
:type="showReusableActivationCode ? 'text' : 'password'"
|
||||
:label="$t('reusable_activation_code_label')"
|
||||
:hint="$t('reusable_activation_code_hint')"
|
||||
>
|
||||
<q-btn
|
||||
@click="
|
||||
showReusableActivationCode = !showReusableActivationCode
|
||||
"
|
||||
dense
|
||||
flat
|
||||
:icon="
|
||||
showReusableActivationCode
|
||||
? 'visibility_off'
|
||||
: 'visibility'
|
||||
"
|
||||
color="grey"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
@click="
|
||||
utils.copyText(
|
||||
formData.lnbits_register_reusable_activation_code
|
||||
)
|
||||
"
|
||||
dense
|
||||
flat
|
||||
icon="content_copy"
|
||||
color="grey"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('one_time_activation_code')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAddActivationCode"
|
||||
@keydown.enter="addOneTimeActivationCode"
|
||||
type="text"
|
||||
:label="$t('one_time_activation_code_label')"
|
||||
:hint="$t('one_time_activation_code_hint')"
|
||||
>
|
||||
<q-btn
|
||||
@click="addOneTimeActivationCode"
|
||||
dense
|
||||
flat
|
||||
icon="add"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
<q-chip
|
||||
v-for="code in formData.lnbits_register_one_time_activation_codes"
|
||||
:key="code"
|
||||
removable
|
||||
@remove="removeOneTimeActivationCode(code)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
:label="code"
|
||||
class="ellipsis"
|
||||
>
|
||||
</q-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-expansion-item header-class="text-primary text-bold">
|
||||
<template v-slot:header>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<q-icon name="bolt" size="md"></q-icon>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section> Payment </q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="row items-center">
|
||||
<q-toggle
|
||||
size="md"
|
||||
disable
|
||||
:label="
|
||||
formData.lnbits_user_activation_by_payment
|
||||
? $t('enabled')
|
||||
: $t('disabled')
|
||||
"
|
||||
v-model="formData.lnbits_user_activation_by_payment"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</template>
|
||||
|
||||
<q-card class="q-pb-xl">
|
||||
<q-card-section>
|
||||
<div
|
||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
||||
>
|
||||
<q-icon
|
||||
name="info"
|
||||
color="orange-4"
|
||||
size="18px"
|
||||
class="q-mr-xs"
|
||||
></q-icon>
|
||||
Users will need to make a payment during registration to
|
||||
activate their account.
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item header-class="text-primary text-bold">
|
||||
<template v-slot:header>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<q-icon name="email"></q-icon>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section> Email </q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="row items-center">
|
||||
<q-toggle
|
||||
disable
|
||||
size="md"
|
||||
:label="
|
||||
formData.lnbits_user_activation_by_email
|
||||
? $t('enabled')
|
||||
: $t('disabled')
|
||||
"
|
||||
v-model="formData.lnbits_user_activation_by_email"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</template>
|
||||
|
||||
<q-card class="q-pb-xl">
|
||||
<q-card-section>
|
||||
<div
|
||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
||||
>
|
||||
<q-icon
|
||||
name="info"
|
||||
color="orange-4"
|
||||
size="18px"
|
||||
class="q-mr-xs"
|
||||
></q-icon>
|
||||
Users will receive a confirmation email.
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
v-model:user-name="username"
|
||||
v-model:password_1="password"
|
||||
v-model:password_2="passwordRepeat"
|
||||
v-model:invitation-code="invitationCode"
|
||||
v-model:reset-key="reset_key"
|
||||
@login="login"
|
||||
@register="register"
|
||||
|
||||
@@ -218,26 +218,6 @@
|
||||
:label="$t('update_account')"
|
||||
class="q-ml-md"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
v-if="activeUser.data.id"
|
||||
outline
|
||||
color="primary"
|
||||
class="q-ml-md"
|
||||
>
|
||||
<q-toggle
|
||||
size="xs"
|
||||
color="secondary"
|
||||
v-model="activeUser.data.admin"
|
||||
@update:model-value="toggleAdmin(activeUser.data.id)"
|
||||
:label="
|
||||
activeUser.data.admin
|
||||
? $t('revoke_admin')
|
||||
: $t('make_user_admin')
|
||||
"
|
||||
>
|
||||
</q-toggle>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
@click="createUser()"
|
||||
@@ -610,9 +590,9 @@
|
||||
<q-btn
|
||||
@click="showAccountPage(props.row.id)"
|
||||
round
|
||||
:icon="props.row.is_admin ? 'admin_panel_settings' : 'edit'"
|
||||
icon="edit"
|
||||
size="sm"
|
||||
:color="props.row.is_admin ? 'primary' : 'secondary'"
|
||||
color="secondary"
|
||||
class="q-ml-xs"
|
||||
>
|
||||
<q-tooltip>
|
||||
@@ -625,18 +605,10 @@
|
||||
size="xs"
|
||||
v-if="!props.row.is_super_user"
|
||||
color="secondary"
|
||||
v-model="props.row.activated"
|
||||
@update:model-value="toggleUserActivated(props.row.id)"
|
||||
v-model="props.row.is_admin"
|
||||
@update:model-value="toggleAdmin(props.row.id)"
|
||||
>
|
||||
<q-tooltip
|
||||
><span
|
||||
v-text="
|
||||
props.row.activated
|
||||
? $t('deactivate')
|
||||
: $t('activate')
|
||||
"
|
||||
></span
|
||||
></q-tooltip>
|
||||
<q-tooltip>Toggle Admin</q-tooltip>
|
||||
</q-toggle>
|
||||
<q-btn
|
||||
round
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "lnbits"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
import shortuuid
|
||||
from httpx import AsyncClient
|
||||
|
||||
from lnbits.core.crud.wallets import get_wallets
|
||||
from lnbits.core.models import AccessTokenPayload, Payment
|
||||
from lnbits.core.models.users import Account, User
|
||||
from lnbits.core.services.users import create_user_account
|
||||
from lnbits.settings import Settings
|
||||
@@ -612,127 +610,3 @@ async def test_delete_and_undelete_wallet(http_client: AsyncClient, superuser_to
|
||||
undeleted_wallet = next((w for w in wallets if w.id == wallet_id), None)
|
||||
assert undeleted_wallet is not None
|
||||
assert undeleted_wallet.deleted is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_user_activation(
|
||||
http_client: AsyncClient, invoice: Payment, settings: Settings, superuser_token: str
|
||||
):
|
||||
|
||||
# Register a new user
|
||||
username = f"u21.{shortuuid.uuid()[:8]}"
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"username": username,
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
"email": f"{username}@lnbits.com",
|
||||
},
|
||||
)
|
||||
access_token = response.json().get("access_token")
|
||||
assert response.status_code == 200, "User created."
|
||||
assert response.json().get("access_token") is not None
|
||||
|
||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
||||
access_token_payload = AccessTokenPayload(**payload)
|
||||
user_id = access_token_payload.usr
|
||||
assert user_id is not None
|
||||
|
||||
# Login works
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth", json={"username": username, "password": "secret1234"}
|
||||
)
|
||||
assert response.status_code == 200, "User logs in OK"
|
||||
|
||||
# Deactivate the user
|
||||
respones = await http_client.put(
|
||||
f"/users/api/v1/user/{user_id}/activate",
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
|
||||
assert respones.status_code == 200, "User deactivated."
|
||||
assert respones.json().get("message") == "User deactivated."
|
||||
|
||||
# Login should now fail
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth", json={"username": username, "password": "secret1234"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.json().get("detail") == "Invalid credentials."
|
||||
|
||||
response = await http_client.get(
|
||||
"/api/v1/auth",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.json().get("detail") == "User not found."
|
||||
|
||||
wallets = await get_wallets(user_id=user_id)
|
||||
assert len(wallets) == 1, "User's wallet still exists."
|
||||
wallet = wallets[0]
|
||||
|
||||
response = await http_client.get(
|
||||
"/api/v1/payments/paginated",
|
||||
params={"limit": 2},
|
||||
headers={"x-Api-Key": wallet.inkey},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json().get("detail") == "Wallet not found."
|
||||
|
||||
response = await http_client.post(
|
||||
"/api/v1/payments",
|
||||
json={
|
||||
"out": False,
|
||||
"amount": 1000,
|
||||
"memo": "test payment",
|
||||
},
|
||||
headers={"x-Api-Key": wallet.inkey},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert response.json().get("detail") == "Wallet not found."
|
||||
|
||||
data = {"out": True, "bolt11": invoice.bolt11}
|
||||
response = await http_client.post(
|
||||
"/api/v1/payments",
|
||||
json=data,
|
||||
headers={"x-Api-Key": wallet.adminkey},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json().get("detail") == "Wallet not found."
|
||||
|
||||
# Reactivate the user
|
||||
response = await http_client.put(
|
||||
f"/users/api/v1/user/{user_id}/activate",
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
print("### response", response.text)
|
||||
assert response.status_code == 200
|
||||
assert response.json().get("message") == "User activated."
|
||||
|
||||
# Login should now pass
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth", json={"username": username, "password": "secret1234"}
|
||||
)
|
||||
assert response.status_code == 200, "User logs in OK again."
|
||||
|
||||
response = await http_client.get(
|
||||
"/api/v1/payments/paginated",
|
||||
params={"limit": 2},
|
||||
headers={"x-Api-Key": wallet.inkey},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
response = await http_client.post(
|
||||
"/api/v1/payments",
|
||||
json={
|
||||
"out": False,
|
||||
"amount": 1000,
|
||||
"memo": "test payment",
|
||||
},
|
||||
headers={"x-Api-Key": wallet.inkey},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user