Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8705650d8 | ||
|
|
8a9a96a52c | ||
|
|
ad028b303c | ||
|
|
b744af1de5 |
+21
-75
@@ -4,12 +4,10 @@ from typing import Any
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||||
from lnbits.core.crud.wallets import 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.db import db
|
||||||
from lnbits.core.models import UserAcls
|
from lnbits.core.models import UserAcls
|
||||||
from lnbits.db import Connection, Filters, Page
|
from lnbits.db import Connection, Filters, Page
|
||||||
from lnbits.helpers import sha256s
|
|
||||||
from lnbits.utils.cache import cache
|
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
Account,
|
Account,
|
||||||
@@ -43,7 +41,6 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
|||||||
"DELETE from accounts WHERE id = :user",
|
"DELETE from accounts WHERE id = :user",
|
||||||
{"user": user_id},
|
{"user": user_id},
|
||||||
)
|
)
|
||||||
await clear_user_id_cache(user_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_accounts(
|
async def get_accounts(
|
||||||
@@ -72,7 +69,6 @@ async def get_accounts(
|
|||||||
accounts.email,
|
accounts.email,
|
||||||
accounts.pubkey,
|
accounts.pubkey,
|
||||||
accounts.external_id,
|
accounts.external_id,
|
||||||
accounts.activated,
|
|
||||||
SUM(COALESCE((
|
SUM(COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)) as balance_msat,
|
), 0)) as balance_msat,
|
||||||
@@ -97,18 +93,12 @@ async def get_accounts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account(
|
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
|
||||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
|
||||||
) -> Account | None:
|
|
||||||
if len(user_id) == 0:
|
if len(user_id) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"SELECT * FROM accounts WHERE id = :id",
|
||||||
SELECT * FROM accounts
|
{"id": user_id},
|
||||||
WHERE id = :id AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
|
||||||
{"id": user_id, "activated": active_only},
|
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,79 +124,55 @@ async def delete_accounts_no_wallets(
|
|||||||
|
|
||||||
|
|
||||||
async def get_account_by_username(
|
async def get_account_by_username(
|
||||||
username: str, active_only: bool = True, conn: Connection | None = None
|
username: str, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
if len(username) == 0:
|
if len(username) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"SELECT * FROM accounts WHERE LOWER(username) = :username",
|
||||||
SELECT * FROM accounts
|
{"username": username.lower()},
|
||||||
WHERE
|
|
||||||
LOWER(username) = :username
|
|
||||||
AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
|
||||||
{"username": username.lower(), "activated": active_only},
|
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_pubkey(
|
async def get_account_by_pubkey(
|
||||||
pubkey: str, active_only: bool = True, conn: Connection | None = None
|
pubkey: str, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
||||||
SELECT * FROM accounts
|
{"pubkey": pubkey.lower()},
|
||||||
WHERE
|
|
||||||
LOWER(pubkey) = :pubkey
|
|
||||||
AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
|
||||||
{"pubkey": pubkey.lower(), "activated": active_only},
|
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_email(
|
async def get_account_by_email(
|
||||||
email: str, active_only: bool = True, conn: Connection | None = None
|
email: str, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
if len(email) == 0:
|
if len(email) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"SELECT * FROM accounts WHERE LOWER(email) = :email",
|
||||||
SELECT * FROM accounts
|
{"email": email.lower()},
|
||||||
WHERE
|
|
||||||
LOWER(email) = :email
|
|
||||||
AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
|
||||||
{"email": email.lower(), "activated": active_only},
|
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_username_or_email(
|
async def get_account_by_username_or_email(
|
||||||
username_or_email: str,
|
username_or_email: str, conn: Connection | None = None
|
||||||
active_only: bool = True,
|
|
||||||
conn: Connection | None = None,
|
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM accounts
|
SELECT * FROM accounts
|
||||||
WHERE
|
WHERE LOWER(email) = :value or LOWER(username) = :value
|
||||||
(LOWER(email) = :value or LOWER(username) = :value)
|
|
||||||
AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
""",
|
||||||
{"value": username_or_email.lower(), "activated": active_only},
|
{"value": username_or_email.lower()},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_user(
|
async def get_user(user_id: str, conn: Connection | None = None) -> User | 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:
|
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:
|
if not account:
|
||||||
return None
|
return None
|
||||||
return await get_user_from_account(account, conn=conn)
|
return await get_user_from_account(account, conn=conn)
|
||||||
@@ -225,7 +191,6 @@ async def get_user_from_account(
|
|||||||
|
|
||||||
return User(
|
return User(
|
||||||
id=account.id,
|
id=account.id,
|
||||||
activated=account.activated,
|
|
||||||
email=account.email,
|
email=account.email,
|
||||||
username=account.username,
|
username=account.username,
|
||||||
pubkey=account.pubkey,
|
pubkey=account.pubkey,
|
||||||
@@ -251,31 +216,12 @@ async def update_user_access_control_list(
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_access_control_lists(
|
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:
|
) -> UserAcls:
|
||||||
user_acls = await (conn or db).fetchone(
|
user_acls = await (conn or db).fetchone(
|
||||||
"""
|
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
||||||
SELECT id, access_control_list FROM accounts
|
{"id": user_id},
|
||||||
WHERE id = :user_id AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
|
||||||
{"user_id": user_id, "activated": active_only},
|
|
||||||
UserAcls,
|
UserAcls,
|
||||||
)
|
)
|
||||||
|
|
||||||
return user_acls or UserAcls(id=user_id)
|
return user_acls or UserAcls(id=user_id)
|
||||||
|
|
||||||
|
|
||||||
async def clear_user_id_cache(user_id: str):
|
|
||||||
user = await get_user(user_id, active_only=True)
|
|
||||||
if user:
|
|
||||||
clear_user_cache(user)
|
|
||||||
|
|
||||||
|
|
||||||
def clear_user_cache(user: User):
|
|
||||||
user_cache_key: str | None = cache.pop(
|
|
||||||
f"auth:user:cache_key:{sha256s(user.id)}", None
|
|
||||||
)
|
|
||||||
if user_cache_key:
|
|
||||||
cache.pop(user_cache_key)
|
|
||||||
for wallet in user.wallets:
|
|
||||||
clear_wallet_cache(wallet)
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async def delete_wallet(
|
|||||||
deleted: bool = True,
|
deleted: bool = True,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
clear_wallet_id_cache(wallet_id)
|
_clear_wallet_cache(wallet_id)
|
||||||
now = int(time())
|
now = int(time())
|
||||||
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
@@ -65,7 +65,7 @@ async def delete_wallet(
|
|||||||
|
|
||||||
|
|
||||||
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
||||||
clear_wallet_id_cache(wallet_id)
|
_clear_wallet_cache(wallet_id)
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE FROM wallets WHERE id = :wallet",
|
"DELETE FROM wallets WHERE id = :wallet",
|
||||||
{"wallet": wallet_id},
|
{"wallet": wallet_id},
|
||||||
@@ -75,7 +75,7 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
|
|||||||
async def delete_wallet_by_id(
|
async def delete_wallet_by_id(
|
||||||
wallet_id: str, conn: Connection | None = None
|
wallet_id: str, conn: Connection | None = None
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
clear_wallet_id_cache(wallet_id)
|
_clear_wallet_cache(wallet_id)
|
||||||
now = int(time())
|
now = int(time())
|
||||||
result = await (conn or db).execute(
|
result = await (conn or db).execute(
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
@@ -226,14 +226,11 @@ async def get_wallet_for_key(
|
|||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
wallet = await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT wallets.*, COALESCE((
|
SELECT *, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)
|
), 0)
|
||||||
AS balance_msat FROM wallets
|
AS balance_msat FROM wallets
|
||||||
INNER JOIN accounts ON wallets.user = accounts.id
|
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
||||||
WHERE (adminkey = :key OR inkey = :key)
|
|
||||||
AND deleted = false
|
|
||||||
AND accounts.activated = true
|
|
||||||
""",
|
""",
|
||||||
{"key": key},
|
{"key": key},
|
||||||
Wallet,
|
Wallet,
|
||||||
@@ -253,11 +250,8 @@ async def get_base_wallet_for_key(
|
|||||||
) -> BaseWallet | None:
|
) -> BaseWallet | None:
|
||||||
wallet = await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets
|
SELECT 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
|
||||||
WHERE (adminkey = :key OR inkey = :key)
|
|
||||||
AND deleted = false
|
|
||||||
AND accounts.activated = true
|
|
||||||
""",
|
""",
|
||||||
{"key": key},
|
{"key": key},
|
||||||
BaseWallet,
|
BaseWallet,
|
||||||
@@ -300,14 +294,8 @@ async def get_total_balance(conn: Connection | None = None):
|
|||||||
return row.get("balance", 0) or 0
|
return row.get("balance", 0) or 0
|
||||||
|
|
||||||
|
|
||||||
def clear_wallet_id_cache(wallet_id: str):
|
def _clear_wallet_cache(wallet_id):
|
||||||
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
||||||
if cached_wallet:
|
if cached_wallet:
|
||||||
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
||||||
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
||||||
|
|
||||||
|
|
||||||
def clear_wallet_cache(wallet: Wallet):
|
|
||||||
cache.pop(f"auth:wallet:{wallet.id}")
|
|
||||||
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
|
||||||
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
|
||||||
|
|||||||
@@ -856,11 +856,3 @@ async def m043_add_ui_customization_to_accounts(db: Connection):
|
|||||||
Used for server side persistence of UI customization settings.
|
Used for server side persistence of UI customization settings.
|
||||||
"""
|
"""
|
||||||
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
||||||
|
|
||||||
|
|
||||||
async def m044_add_activated_to_accounts(db: Connection):
|
|
||||||
"""
|
|
||||||
Adds activated column to accounts.
|
|
||||||
Used for account activation status.
|
|
||||||
"""
|
|
||||||
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
|
|
||||||
|
|||||||
@@ -181,7 +181,6 @@ class AccountId(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Account(AccountId):
|
class Account(AccountId):
|
||||||
activated: bool = True
|
|
||||||
external_id: str | None = None # for external account linking
|
external_id: str | None = None # for external account linking
|
||||||
username: str | None = None
|
username: str | None = None
|
||||||
password_hash: str | None = None
|
password_hash: str | None = None
|
||||||
@@ -242,7 +241,6 @@ class Account(AccountId):
|
|||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
activated: bool = True
|
|
||||||
transaction_count: int | None = 0
|
transaction_count: int | None = 0
|
||||||
wallet_count: int | None = 0
|
wallet_count: int | None = 0
|
||||||
balance_msat: int | None = 0
|
balance_msat: int | None = 0
|
||||||
@@ -278,7 +276,6 @@ class AccountFilters(FilterModel):
|
|||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
activated: bool = True
|
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
@@ -318,7 +315,6 @@ class RegisterUser(BaseModel):
|
|||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
invitation_code: str | None = Query(default=None, min_length=1, max_length=256)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateUser(BaseModel):
|
class CreateUser(BaseModel):
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud.settings import set_settings_field
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.extensions import UserExtension
|
from lnbits.core.models.extensions import UserExtension
|
||||||
from lnbits.core.models.users import RegisterUser
|
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
@@ -194,25 +192,3 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
|||||||
|
|
||||||
editable_settings = EditableSettings.from_dict(settings.dict())
|
editable_settings = EditableSettings.from_dict(settings.dict())
|
||||||
return await create_admin_settings(account.id, editable_settings.dict())
|
return await create_admin_settings(account.id, editable_settings.dict())
|
||||||
|
|
||||||
|
|
||||||
async def check_register_activation_settings(data: RegisterUser):
|
|
||||||
if not settings.lnbits_require_user_activation:
|
|
||||||
return None
|
|
||||||
if settings.lnbits_user_activation_by_invitation_code:
|
|
||||||
code = data.invitation_code.strip() if data.invitation_code else ""
|
|
||||||
if len(code) == 0:
|
|
||||||
raise ValueError("Invitation code cannot be empty.")
|
|
||||||
|
|
||||||
if code == settings.lnbits_register_reusable_activation_code:
|
|
||||||
return None
|
|
||||||
if code in settings.lnbits_register_one_time_activation_codes:
|
|
||||||
settings.lnbits_register_one_time_activation_codes.remove(code)
|
|
||||||
await set_settings_field(
|
|
||||||
"lnbits_register_one_time_activation_codes",
|
|
||||||
settings.lnbits_register_one_time_activation_codes,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
raise ValueError("Invalid invitation code.")
|
|
||||||
|
|
||||||
raise ValueError("No activation method provided.")
|
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ from lnbits.core.models.users import (
|
|||||||
UpdateAccessControlList,
|
UpdateAccessControlList,
|
||||||
)
|
)
|
||||||
from lnbits.core.services import create_user_account
|
from lnbits.core.services import create_user_account
|
||||||
from lnbits.core.services.users import (
|
from lnbits.core.services.users import update_user_account
|
||||||
check_register_activation_settings,
|
|
||||||
update_user_account,
|
|
||||||
)
|
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
access_token_payload,
|
access_token_payload,
|
||||||
check_account_exists,
|
check_account_exists,
|
||||||
@@ -89,7 +86,6 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
|||||||
account = await get_account_by_username_or_email(data.username)
|
account = await get_account_by_username_or_email(data.username)
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@@ -98,7 +94,7 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
||||||
event = _nostr_nip98_event(request)
|
event = _nostr_nip98_event(request)
|
||||||
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
|
account = await get_account_by_pubkey(event["pubkey"])
|
||||||
if not account:
|
if not account:
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
@@ -106,8 +102,6 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
extra=UserExtra(provider="nostr"),
|
extra=UserExtra(provider="nostr"),
|
||||||
)
|
)
|
||||||
await create_user_account(account)
|
await create_user_account(account)
|
||||||
if not account.activated:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
|
|
||||||
return _auth_success_response(account.username or "", account.id, account.email)
|
return _auth_success_response(account.username or "", account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@@ -363,14 +357,12 @@ async def register(data: RegisterUser) -> JSONResponse:
|
|||||||
if not is_valid_username(data.username):
|
if not is_valid_username(data.username):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
||||||
|
|
||||||
if await get_account_by_username(data.username, active_only=False):
|
if await get_account_by_username(data.username):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||||
|
|
||||||
if data.email and not is_valid_email_address(data.email):
|
if data.email and not is_valid_email_address(data.email):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||||
|
|
||||||
await check_register_activation_settings(data)
|
|
||||||
|
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
email=data.email,
|
email=data.email,
|
||||||
@@ -535,7 +527,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
|||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||||
|
|
||||||
redirect_path = "/wallet"
|
redirect_path = "/wallet"
|
||||||
account = await get_account_by_email(email, active_only=False)
|
account = await get_account_by_email(email)
|
||||||
|
|
||||||
if verified_user_id:
|
if verified_user_id:
|
||||||
if account:
|
if account:
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from lnbits.core.crud import (
|
|||||||
update_admin_settings,
|
update_admin_settings,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
|
|
||||||
from lnbits.core.crud.wallets import delete_wallet_by_id
|
from lnbits.core.crud.wallets import delete_wallet_by_id
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccountFilters,
|
AccountFilters,
|
||||||
@@ -74,7 +73,7 @@ async def api_get_users(
|
|||||||
summary="Get user by Id",
|
summary="Get user by Id",
|
||||||
)
|
)
|
||||||
async def api_get_user(user_id: str) -> User:
|
async def api_get_user(user_id: str) -> User:
|
||||||
user = await get_user(user_id, active_only=False)
|
user = await get_user(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||||
return user
|
return user
|
||||||
@@ -198,7 +197,7 @@ async def api_users_reset_password(user_id: str) -> str:
|
|||||||
return f"reset_key_{reset_key_b64}"
|
return f"reset_key_{reset_key_b64}"
|
||||||
|
|
||||||
|
|
||||||
@users_router.put(
|
@users_router.get(
|
||||||
"/user/{user_id}/admin",
|
"/user/{user_id}/admin",
|
||||||
dependencies=[Depends(check_super_user)],
|
dependencies=[Depends(check_super_user)],
|
||||||
name="Give or revoke admin permsisions to a user",
|
name="Give or revoke admin permsisions to a user",
|
||||||
@@ -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")
|
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
||||||
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
||||||
return await get_wallets(user_id, deleted=None)
|
return await get_wallets(user_id, deleted=None)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from fastapi import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import (
|
from lnbits.core.crud.wallets import (
|
||||||
clear_wallet_cache,
|
|
||||||
create_wallet,
|
create_wallet,
|
||||||
get_wallets_paginated,
|
get_wallets_paginated,
|
||||||
)
|
)
|
||||||
@@ -38,6 +37,7 @@ from lnbits.decorators import (
|
|||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import generate_filter_params_openapi
|
from lnbits.helpers import generate_filter_params_openapi
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
delete_wallet,
|
delete_wallet,
|
||||||
@@ -134,7 +134,9 @@ async def api_reset_wallet_keys(
|
|||||||
if not wallet or wallet.user != account_id.id:
|
if not wallet or wallet.user != account_id.id:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||||
|
|
||||||
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.adminkey = uuid4().hex
|
||||||
wallet.inkey = uuid4().hex
|
wallet.inkey = uuid4().hex
|
||||||
|
|||||||
+42
-9
@@ -12,6 +12,7 @@ from typing import Any, Generic, Literal, TypeVar, get_origin
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, ValidationError, root_validator
|
from pydantic import BaseModel, ValidationError, root_validator
|
||||||
|
from sqlalchemy import event
|
||||||
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
@@ -55,6 +56,14 @@ def compat_timestamp_placeholder(key: str):
|
|||||||
return f":{key}"
|
return f":{key}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_placeholder(model: Any, field: str) -> str:
|
||||||
|
type_ = model.__fields__[field].type_
|
||||||
|
if type_ == datetime:
|
||||||
|
return compat_timestamp_placeholder(field)
|
||||||
|
else:
|
||||||
|
return f":{field}"
|
||||||
|
|
||||||
|
|
||||||
class Compat:
|
class Compat:
|
||||||
type: str | None = "<inherited>"
|
type: str | None = "<inherited>"
|
||||||
schema: str | None = "<inherited>"
|
schema: str | None = "<inherited>"
|
||||||
@@ -317,7 +326,31 @@ class Database(Compat):
|
|||||||
self.engine: AsyncEngine = create_async_engine(
|
self.engine: AsyncEngine = create_async_engine(
|
||||||
database_uri, echo=settings.debug_database
|
database_uri, echo=settings.debug_database
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.type in {POSTGRES, COCKROACH}:
|
||||||
|
|
||||||
|
@event.listens_for(self.engine.sync_engine, "connect")
|
||||||
|
def register_custom_types(dbapi_connection, *_):
|
||||||
|
def _parse_date(value) -> datetime:
|
||||||
|
if value is None:
|
||||||
|
value = "1970-01-01 00:00:00"
|
||||||
|
f = "%Y-%m-%d %H:%M:%S.%f"
|
||||||
|
if "." not in value:
|
||||||
|
f = "%Y-%m-%d %H:%M:%S"
|
||||||
|
# Parse and add UTC timezone info
|
||||||
|
return datetime.strptime(value, f).replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
dbapi_connection.run_async(
|
||||||
|
lambda connection: connection.set_type_codec(
|
||||||
|
"TIMESTAMP",
|
||||||
|
encoder=datetime,
|
||||||
|
decoder=_parse_date,
|
||||||
|
schema="pg_catalog",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
logger.trace(f"database {self.type} added for {self.name}")
|
logger.trace(f"database {self.type} added for {self.name}")
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -630,7 +663,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
|
|||||||
placeholders = []
|
placeholders = []
|
||||||
keys = model_to_dict(model).keys()
|
keys = model_to_dict(model).keys()
|
||||||
for field in keys:
|
for field in keys:
|
||||||
placeholders.append(f":{field}")
|
placeholders.append(get_placeholder(model, field))
|
||||||
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||||
fields = ", ".join([f'"{key}"' for key in keys])
|
fields = ", ".join([f'"{key}"' for key in keys])
|
||||||
values = ", ".join(placeholders)
|
values = ", ".join(placeholders)
|
||||||
@@ -648,8 +681,9 @@ def update_query(
|
|||||||
"""
|
"""
|
||||||
fields = []
|
fields = []
|
||||||
for field in model_to_dict(model).keys():
|
for field in model_to_dict(model).keys():
|
||||||
|
placeholder = get_placeholder(model, field)
|
||||||
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||||
fields.append(f'"{field}" = :{field}')
|
fields.append(f'"{field}" = {placeholder}')
|
||||||
query = ", ".join(fields)
|
query = ", ".join(fields)
|
||||||
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
|
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
|
||||||
|
|
||||||
@@ -667,12 +701,7 @@ def model_to_dict(model: BaseModel) -> dict:
|
|||||||
if model.__fields__[key].field_info.extra.get("no_database", False):
|
if model.__fields__[key].field_info.extra.get("no_database", False):
|
||||||
continue
|
continue
|
||||||
if isinstance(value, datetime):
|
if isinstance(value, datetime):
|
||||||
if DB_TYPE == SQLITE:
|
_dict[key] = value.timestamp()
|
||||||
_dict[key] = value.timestamp()
|
|
||||||
else:
|
|
||||||
# remove tz. postgres and cockroach TIMESTAMP is not tz aware
|
|
||||||
# so it will throw if we dont remove the UTC.
|
|
||||||
_dict[key] = value.replace(tzinfo=None)
|
|
||||||
continue
|
continue
|
||||||
if (
|
if (
|
||||||
type(type_) is type(BaseModel)
|
type(type_) is type(BaseModel)
|
||||||
@@ -728,7 +757,11 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
|
|||||||
if DB_TYPE == SQLITE:
|
if DB_TYPE == SQLITE:
|
||||||
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
||||||
else:
|
else:
|
||||||
_dict[key] = value.replace(tzinfo=timezone.utc)
|
# 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
|
||||||
continue
|
continue
|
||||||
if issubclass(type_, BaseModel):
|
if issubclass(type_, BaseModel):
|
||||||
_dict[key] = dict_to_submodel(type_, value)
|
_dict[key] = dict_to_submodel(type_, value)
|
||||||
|
|||||||
@@ -261,7 +261,6 @@ async def check_account_id_exists(
|
|||||||
account_id,
|
account_id,
|
||||||
expiry=settings.auth_authentication_cache_minutes * 60,
|
expiry=settings.auth_authentication_cache_minutes * 60,
|
||||||
)
|
)
|
||||||
cache.set(f"auth:user:cache_key:{sha256s(account.id)}", cache_key)
|
|
||||||
|
|
||||||
return account_id
|
return account_id
|
||||||
|
|
||||||
|
|||||||
@@ -41,14 +41,6 @@ class UsersSettings(LNbitsSettings):
|
|||||||
lnbits_admin_users: list[str] = Field(default=[])
|
lnbits_admin_users: list[str] = Field(default=[])
|
||||||
lnbits_allowed_users: list[str] = Field(default=[])
|
lnbits_allowed_users: list[str] = Field(default=[])
|
||||||
lnbits_allow_new_accounts: bool = Field(default=True)
|
lnbits_allow_new_accounts: bool = Field(default=True)
|
||||||
lnbits_require_user_activation: bool = Field(default=False)
|
|
||||||
|
|
||||||
lnbits_user_activation_by_email: bool = Field(default=False)
|
|
||||||
lnbits_user_activation_by_payment: bool = Field(default=False)
|
|
||||||
lnbits_user_activation_by_invitation_code: bool = Field(default=False)
|
|
||||||
|
|
||||||
lnbits_register_reusable_activation_code: str = Field(default="")
|
|
||||||
lnbits_register_one_time_activation_codes: list[str] = Field(default=[])
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def new_accounts_allowed(self) -> bool:
|
def new_accounts_allowed(self) -> bool:
|
||||||
@@ -1196,11 +1188,6 @@ class PublicSettings(BaseModel):
|
|||||||
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
|
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
|
||||||
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
|
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
|
||||||
wallet_featured_button_icon: str | None = Field(alias="walletFeaturedButtonIcon")
|
wallet_featured_button_icon: str | None = Field(alias="walletFeaturedButtonIcon")
|
||||||
lnbits_user_activation_by_email: bool = Field(alias="userActivationByEmail")
|
|
||||||
lnbits_user_activation_by_payment: bool = Field(alias="userActivationByPayment")
|
|
||||||
lnbits_user_activation_by_invitation_code: bool = Field(
|
|
||||||
alias="userActivationByInvitationCode"
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_settings(cls, settings: Settings):
|
def from_settings(cls, settings: Settings):
|
||||||
@@ -1252,9 +1239,6 @@ class PublicSettings(BaseModel):
|
|||||||
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
|
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
|
||||||
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
|
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
|
||||||
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
|
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
|
||||||
userActivationByEmail=settings.lnbits_user_activation_by_email,
|
|
||||||
userActivationByPayment=settings.lnbits_user_activation_by_payment,
|
|
||||||
userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -171,7 +171,6 @@ window.localisation.en = {
|
|||||||
drop_db: 'Remove Data',
|
drop_db: 'Remove Data',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
disabled: 'Disabled',
|
|
||||||
pay_to_enable: 'Pay To Enable',
|
pay_to_enable: 'Pay To Enable',
|
||||||
enable_extension_details: 'Enable extension for current user',
|
enable_extension_details: 'Enable extension for current user',
|
||||||
disable: 'Disable',
|
disable: 'Disable',
|
||||||
@@ -179,8 +178,6 @@ window.localisation.en = {
|
|||||||
installed: 'Installed',
|
installed: 'Installed',
|
||||||
activated: 'Activated',
|
activated: 'Activated',
|
||||||
deactivated: 'Deactivated',
|
deactivated: 'Deactivated',
|
||||||
activate: 'Activate',
|
|
||||||
deactivate: 'Deactivate',
|
|
||||||
release_notes: 'Release Notes',
|
release_notes: 'Release Notes',
|
||||||
activate_extension_details: 'Make extension available/unavailable for users',
|
activate_extension_details: 'Make extension available/unavailable for users',
|
||||||
featured: 'Featured',
|
featured: 'Featured',
|
||||||
@@ -189,8 +186,6 @@ window.localisation.en = {
|
|||||||
only_admins_can_create_extensions:
|
only_admins_can_create_extensions:
|
||||||
'Only admin accounts can create extensions',
|
'Only admin accounts can create extensions',
|
||||||
admin_only: 'Admin Only',
|
admin_only: 'Admin Only',
|
||||||
make_user_admin: 'Make User Admin',
|
|
||||||
revoke_admin: 'Revoke Admin',
|
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
reviews_url: 'Reviews URL',
|
reviews_url: 'Reviews URL',
|
||||||
reviews_url_label: 'Reviews server URL',
|
reviews_url_label: 'Reviews server URL',
|
||||||
@@ -285,7 +280,7 @@ window.localisation.en = {
|
|||||||
'Nip5 identifier to send notifications to',
|
'Nip5 identifier to send notifications to',
|
||||||
notifications_nostr_identifiers: 'Nostr Identifiers',
|
notifications_nostr_identifiers: 'Nostr Identifiers',
|
||||||
notifications_nostr_identifiers_desc:
|
notifications_nostr_identifiers_desc:
|
||||||
'List of identifiers to send notifications to.',
|
'List of identifiers to send notifications to',
|
||||||
|
|
||||||
notifications_telegram_config: 'Telegram Configuration',
|
notifications_telegram_config: 'Telegram Configuration',
|
||||||
notifications_enable_telegram: 'Enable Telegram',
|
notifications_enable_telegram: 'Enable Telegram',
|
||||||
@@ -709,25 +704,6 @@ window.localisation.en = {
|
|||||||
allowed_users_label: 'User ID',
|
allowed_users_label: 'User ID',
|
||||||
allow_creation_user: 'Allow creation of new users',
|
allow_creation_user: 'Allow creation of new users',
|
||||||
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
||||||
require_user_activation: 'Require user activation',
|
|
||||||
require_user_activation_desc:
|
|
||||||
'New users will be activated only after they pass one of the confirmation methods. Admins can activate users manually from the admin panel.',
|
|
||||||
reusable_activation_code: 'Reusable activation code',
|
|
||||||
reusable_activation_code_label: 'Reusable activation code',
|
|
||||||
reusable_activation_code_hint:
|
|
||||||
'This activation code can be used multiple times by different users.',
|
|
||||||
one_time_activation_code: 'One-time activation codes',
|
|
||||||
one_time_activation_code_label: 'Add activation code',
|
|
||||||
one_time_activation_code_hint:
|
|
||||||
'List of one-time activation codes. Each code can be used only once, then will be reomved from the list.',
|
|
||||||
invitation_code: 'Invitation Code',
|
|
||||||
invitation_code_hint: 'The invitation code that you have received.',
|
|
||||||
email: 'Email',
|
|
||||||
email_confirmation_hint: 'Email address to send the confirmation code to.',
|
|
||||||
nostr_identifier: 'Nostr Identifier',
|
|
||||||
nostr_identifier_hint:
|
|
||||||
'Nostr nip5 identifier or <npub> to send the confirmation code to.',
|
|
||||||
|
|
||||||
new_user_not_allowed: 'Registration is disabled.',
|
new_user_not_allowed: 'Registration is disabled.',
|
||||||
start_user_impersonation: 'Impersonate this user',
|
start_user_impersonation: 'Impersonate this user',
|
||||||
stop_user_impersonation: 'Stop User Impersonation',
|
stop_user_impersonation: 'Stop User Impersonation',
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ window._lnbitsApi = {
|
|||||||
name: name
|
name: name
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
register(username, email, password, password_repeat, invitation_code) {
|
register(username, email, password, password_repeat) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/v1/auth/register',
|
url: '/api/v1/auth/register',
|
||||||
@@ -74,8 +74,7 @@ window._lnbitsApi = {
|
|||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
password_repeat,
|
password_repeat
|
||||||
invitation_code
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -148,7 +147,7 @@ window._lnbitsApi = {
|
|||||||
name: name,
|
name: name,
|
||||||
wallet_type: walletType,
|
wallet_type: walletType,
|
||||||
...opts
|
...opts
|
||||||
})
|
}).catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
updateWallet(name, wallet) {
|
updateWallet(name, wallet) {
|
||||||
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
||||||
|
|||||||
@@ -432,10 +432,8 @@ window.app.component('username-password', {
|
|||||||
username: String,
|
username: String,
|
||||||
password_1: String,
|
password_1: String,
|
||||||
password_2: String,
|
password_2: String,
|
||||||
invitationCode: String,
|
|
||||||
resetKey: String
|
resetKey: String
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
oauth: [
|
oauth: [
|
||||||
@@ -447,11 +445,7 @@ window.app.component('username-password', {
|
|||||||
username: this.userName,
|
username: this.userName,
|
||||||
password: this.password_1,
|
password: this.password_1,
|
||||||
passwordRepeat: this.password_2,
|
passwordRepeat: this.password_2,
|
||||||
reset_key: this.resetKey,
|
reset_key: this.resetKey
|
||||||
confirmationMethod: 'code',
|
|
||||||
confirmationEmail: '',
|
|
||||||
confirmationCode: this.invitationCode || '',
|
|
||||||
showConfirmationCode: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -464,7 +458,6 @@ window.app.component('username-password', {
|
|||||||
this.$emit('update:userName', this.username)
|
this.$emit('update:userName', this.username)
|
||||||
this.$emit('update:password_1', this.password)
|
this.$emit('update:password_1', this.password)
|
||||||
this.$emit('update:password_2', this.passwordRepeat)
|
this.$emit('update:password_2', this.passwordRepeat)
|
||||||
this.$emit('update:invitationCode', this.confirmationCode)
|
|
||||||
this.$emit('register')
|
this.$emit('register')
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
@@ -556,25 +549,6 @@ window.app.component('username-password', {
|
|||||||
computed: {
|
computed: {
|
||||||
showOauth() {
|
showOauth() {
|
||||||
return this.oauth.some(m => this.authMethods.includes(m))
|
return this.oauth.some(m => this.authMethods.includes(m))
|
||||||
},
|
|
||||||
disableRegister() {
|
|
||||||
const usernameOK = !!this.username
|
|
||||||
const passwordOK = !!this.password && this.password.length >= 8
|
|
||||||
const passwordsMatch = this.password === this.passwordRepeat
|
|
||||||
const codeOk =
|
|
||||||
this.confirmationMethodsCount === 0 ||
|
|
||||||
this.confirmationMethod !== 'code' ||
|
|
||||||
this.confirmationCode.length > 0
|
|
||||||
|
|
||||||
return !usernameOK || !passwordOK || !passwordsMatch || !codeOk
|
|
||||||
},
|
|
||||||
confirmationMethodsCount() {
|
|
||||||
const methods = [
|
|
||||||
this.g.settings.userActivationByEmail,
|
|
||||||
this.g.settings.userActivationByPayment,
|
|
||||||
this.g.settings.userActivationByInvitationCode
|
|
||||||
]
|
|
||||||
return methods.filter(Boolean).length
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {}
|
created() {}
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ window.app.component('lnbits-admin-users', {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddUser: '',
|
formAddUser: '',
|
||||||
formAddAdmin: '',
|
formAddAdmin: ''
|
||||||
formAddActivationCode: '',
|
|
||||||
showReusableActivationCode: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -33,24 +31,6 @@ window.app.component('lnbits-admin-users', {
|
|||||||
removeAdminUser(user) {
|
removeAdminUser(user) {
|
||||||
let admin_users = this.formData.lnbits_admin_users
|
let admin_users = this.formData.lnbits_admin_users
|
||||||
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
||||||
},
|
|
||||||
addOneTimeActivationCode() {
|
|
||||||
const code = this.formAddActivationCode
|
|
||||||
const activationCodes =
|
|
||||||
this.formData.lnbits_register_one_time_activation_codes
|
|
||||||
if (code?.length && !activationCodes.includes(code)) {
|
|
||||||
this.formData.lnbits_register_one_time_activation_codes = [
|
|
||||||
...activationCodes,
|
|
||||||
code
|
|
||||||
]
|
|
||||||
this.formAddActivationCode = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeOneTimeActivationCode(code) {
|
|
||||||
const codes = this.formData.lnbits_register_one_time_activation_codes
|
|
||||||
this.formData.lnbits_register_one_time_activation_codes = codes.filter(
|
|
||||||
u => u !== code
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -71,10 +71,6 @@ window.app.component('lnbits-wallet-extra', {
|
|||||||
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
||||||
this.g.exchangeRate
|
this.g.exchangeRate
|
||||||
)
|
)
|
||||||
if (this.g.exchangeRate <= 0) {
|
|
||||||
this.g.fiatTracking = false
|
|
||||||
this.g.isFiatPriority = false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(e => console.error(e))
|
.catch(e => console.error(e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ window.app.component('lnbits-wallet-new', {
|
|||||||
this.g.lastWalletId = res.data.id
|
this.g.lastWalletId = res.data.id
|
||||||
this.$router.push(`/wallet/${res.data.id}`)
|
this.$router.push(`/wallet/${res.data.id}`)
|
||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ window.PageHome = {
|
|||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordRepeat: '',
|
passwordRepeat: '',
|
||||||
invitationCode: '',
|
|
||||||
walletName: '',
|
walletName: '',
|
||||||
signup: false
|
signup: false
|
||||||
}
|
}
|
||||||
@@ -41,7 +40,6 @@ window.PageHome = {
|
|||||||
this.username = null
|
this.username = null
|
||||||
this.password = null
|
this.password = null
|
||||||
this.passwordRepeat = null
|
this.passwordRepeat = null
|
||||||
this.invitationCode = null
|
|
||||||
|
|
||||||
this.authAction = 'register'
|
this.authAction = 'register'
|
||||||
this.authMethod = authMethod
|
this.authMethod = authMethod
|
||||||
@@ -53,8 +51,7 @@ window.PageHome = {
|
|||||||
this.username,
|
this.username,
|
||||||
this.email,
|
this.email,
|
||||||
this.password,
|
this.password,
|
||||||
this.passwordRepeat,
|
this.passwordRepeat
|
||||||
this.invitationCode
|
|
||||||
)
|
)
|
||||||
this.refreshAuthUser()
|
this.refreshAuthUser()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ window.PageUsers = {
|
|||||||
usersTable: {
|
usersTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'activated',
|
name: 'admin',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: this.$t('activated'),
|
label: 'Admin',
|
||||||
field: 'activated',
|
field: 'admin',
|
||||||
sortable: false
|
sortable: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -401,7 +401,7 @@ window.PageUsers = {
|
|||||||
|
|
||||||
toggleAdmin(userId) {
|
toggleAdmin(userId) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('PUT', `/users/api/v1/user/${userId}/admin`)
|
.request('GET', `/users/api/v1/user/${userId}/admin`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchUsers()
|
this.fetchUsers()
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -412,19 +412,6 @@ window.PageUsers = {
|
|||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
toggleUserActivated(userId) {
|
|
||||||
LNbits.api
|
|
||||||
.request('PUT', `/users/api/v1/user/${userId}/activate`)
|
|
||||||
.then(res => {
|
|
||||||
this.fetchUsers()
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'positive',
|
|
||||||
message: res.data.message,
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
async showAccountPage(user_id) {
|
async showAccountPage(user_id) {
|
||||||
this.activeUser.showPassword = false
|
this.activeUser.showPassword = false
|
||||||
this.activeUser.showUserId = false
|
this.activeUser.showUserId = false
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ window._lnbitsUtils = {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async notifyApiError(error) {
|
notifyApiError(error) {
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
return console.error(error)
|
return console.error(error)
|
||||||
}
|
}
|
||||||
@@ -154,10 +154,6 @@ window._lnbitsUtils = {
|
|||||||
500: 'negative'
|
500: 'negative'
|
||||||
}
|
}
|
||||||
let messages = error.response.data.detail
|
let messages = error.response.data.detail
|
||||||
if (!messages) {
|
|
||||||
const text = await error.response.data?.text()
|
|
||||||
messages = this.parseJSONSafe(text)?.detail
|
|
||||||
}
|
|
||||||
if (messages) {
|
if (messages) {
|
||||||
messages = Array.isArray(messages)
|
messages = Array.isArray(messages)
|
||||||
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
|
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
|
||||||
|
|||||||
@@ -814,90 +814,16 @@ include('components/lnbits-error.vue') %}
|
|||||||
type="password"
|
type="password"
|
||||||
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
|
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
|
||||||
></q-input>
|
></q-input>
|
||||||
<div
|
|
||||||
v-if="confirmationMethodsCount > 1"
|
|
||||||
class="row justify-center q-mb-md"
|
|
||||||
>
|
|
||||||
<q-tabs
|
|
||||||
v-model="confirmationMethod"
|
|
||||||
dense
|
|
||||||
active-color="primary"
|
|
||||||
indicator-color="primary"
|
|
||||||
>
|
|
||||||
<q-tab
|
|
||||||
v-if="g.settings.userActivationByInvitationCode"
|
|
||||||
name="code"
|
|
||||||
icon="confirmation_number"
|
|
||||||
label="Code"
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
v-if="g.settings.userActivationByPayment"
|
|
||||||
name="payment"
|
|
||||||
icon="bolt"
|
|
||||||
label="Payment"
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
v-if="g.settings.userActivationByEmail"
|
|
||||||
name="email"
|
|
||||||
icon="email"
|
|
||||||
label="Email"
|
|
||||||
></q-tab>
|
|
||||||
</q-tabs>
|
|
||||||
</div>
|
|
||||||
<div v-if="confirmationMethodsCount > 0" class="q-mb-md">
|
|
||||||
<q-tab-panels v-model="confirmationMethod">
|
|
||||||
<q-tab-panel name="code" class="q-pa-none">
|
|
||||||
<div
|
|
||||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
|
||||||
>
|
|
||||||
<q-icon name="info" color="orange-4" class="q-mr-xs"></q-icon>
|
|
||||||
You need an invitation code to register.
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
v-model="confirmationCode"
|
|
||||||
:label="$t('invitation_code')"
|
|
||||||
:type="showConfirmationCode ? 'text' : 'password'"
|
|
||||||
:hint="$t('invitation_code_hint')"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="showConfirmationCode = !showConfirmationCode"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:icon="
|
|
||||||
showConfirmationCode ? 'visibility_off' : 'visibility'
|
|
||||||
"
|
|
||||||
color="grey"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="payment">
|
|
||||||
<div>payment</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
|
|
||||||
<q-tab-panel name="email" class="q-pa-none">
|
|
||||||
<div>
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
v-model="confirmationEmail"
|
|
||||||
:label="$t('email')"
|
|
||||||
:hint="$t('email_confirmation_hint')"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
</q-tab-panels>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row justify-end">
|
<div class="row justify-end">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
:disable="disableRegister"
|
:disable="
|
||||||
|
!password ||
|
||||||
|
!passwordRepeat ||
|
||||||
|
!username ||
|
||||||
|
password !== passwordRepeat
|
||||||
|
"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
:label="$t('create_account')"
|
:label="$t('create_account')"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
>
|
>
|
||||||
</q-chip>
|
</q-chip>
|
||||||
</div>
|
</div>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('allowed_users')"></span></p>
|
<p><span v-text="$t('allowed_users')"></span></p>
|
||||||
@@ -56,10 +57,7 @@
|
|||||||
>
|
>
|
||||||
</q-chip>
|
</q-chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<br />
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-md-6 q-mt-sm">
|
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-text="$t('allow_creation_user')"></q-item-label>
|
<q-item-label v-text="$t('allow_creation_user')"></q-item-label>
|
||||||
@@ -78,237 +76,8 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
<br />
|
||||||
<div class="col-12 col-md-6 q-mt-sm">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('require_user_activation')"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('require_user_activation_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_require_user_activation"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<div v-if="formData.lnbits_require_user_activation" class="row">
|
|
||||||
<div class="col">
|
|
||||||
<q-list bordered class="rounded-borders">
|
|
||||||
<q-expansion-item header-class="text-primary text-bold">
|
|
||||||
<template v-slot:header>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="confirmation_number" size="md"></q-icon>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section> Invitation Code </q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
:label="
|
|
||||||
formData.lnbits_user_activation_by_invitation_code
|
|
||||||
? $t('enabled')
|
|
||||||
: $t('disabled')
|
|
||||||
"
|
|
||||||
v-model="formData.lnbits_user_activation_by_invitation_code"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<q-card class="q-pb-xl">
|
|
||||||
<q-card-section>
|
|
||||||
<div
|
|
||||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="info"
|
|
||||||
color="orange-4"
|
|
||||||
size="18px"
|
|
||||||
class="q-mr-xs"
|
|
||||||
></q-icon>
|
|
||||||
Users will need to provide a valid invitation code during
|
|
||||||
registration to activate their account.
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-md-6 q-pr-sm">
|
|
||||||
<p><span v-text="$t('reusable_activation_code')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_register_reusable_activation_code"
|
|
||||||
:type="showReusableActivationCode ? 'text' : 'password'"
|
|
||||||
:label="$t('reusable_activation_code_label')"
|
|
||||||
:hint="$t('reusable_activation_code_hint')"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="
|
|
||||||
showReusableActivationCode = !showReusableActivationCode
|
|
||||||
"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:icon="
|
|
||||||
showReusableActivationCode
|
|
||||||
? 'visibility_off'
|
|
||||||
: 'visibility'
|
|
||||||
"
|
|
||||||
color="grey"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
@click="
|
|
||||||
utils.copyText(
|
|
||||||
formData.lnbits_register_reusable_activation_code
|
|
||||||
)
|
|
||||||
"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
icon="content_copy"
|
|
||||||
color="grey"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<p><span v-text="$t('one_time_activation_code')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formAddActivationCode"
|
|
||||||
@keydown.enter="addOneTimeActivationCode"
|
|
||||||
type="text"
|
|
||||||
:label="$t('one_time_activation_code_label')"
|
|
||||||
:hint="$t('one_time_activation_code_hint')"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="addOneTimeActivationCode"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
icon="add"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
<div>
|
|
||||||
<q-chip
|
|
||||||
v-for="code in formData.lnbits_register_one_time_activation_codes"
|
|
||||||
:key="code"
|
|
||||||
removable
|
|
||||||
@remove="removeOneTimeActivationCode(code)"
|
|
||||||
color="primary"
|
|
||||||
text-color="white"
|
|
||||||
:label="code"
|
|
||||||
class="ellipsis"
|
|
||||||
>
|
|
||||||
</q-chip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
|
|
||||||
<q-expansion-item header-class="text-primary text-bold">
|
|
||||||
<template v-slot:header>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar>
|
|
||||||
<q-icon name="bolt" size="md"></q-icon>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section> Payment </q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
disable
|
|
||||||
:label="
|
|
||||||
formData.lnbits_user_activation_by_payment
|
|
||||||
? $t('enabled')
|
|
||||||
: $t('disabled')
|
|
||||||
"
|
|
||||||
v-model="formData.lnbits_user_activation_by_payment"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<q-card class="q-pb-xl">
|
|
||||||
<q-card-section>
|
|
||||||
<div
|
|
||||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="info"
|
|
||||||
color="orange-4"
|
|
||||||
size="18px"
|
|
||||||
class="q-mr-xs"
|
|
||||||
></q-icon>
|
|
||||||
Users will need to make a payment during registration to
|
|
||||||
activate their account.
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-expansion-item header-class="text-primary text-bold">
|
|
||||||
<template v-slot:header>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar>
|
|
||||||
<q-icon name="email"></q-icon>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section> Email </q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-toggle
|
|
||||||
disable
|
|
||||||
size="md"
|
|
||||||
:label="
|
|
||||||
formData.lnbits_user_activation_by_email
|
|
||||||
? $t('enabled')
|
|
||||||
: $t('disabled')
|
|
||||||
"
|
|
||||||
v-model="formData.lnbits_user_activation_by_email"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<q-card class="q-pb-xl">
|
|
||||||
<q-card-section>
|
|
||||||
<div
|
|
||||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="info"
|
|
||||||
color="orange-4"
|
|
||||||
size="18px"
|
|
||||||
class="q-mr-xs"
|
|
||||||
></q-icon>
|
|
||||||
Users will receive a confirmation email.
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-list>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -178,7 +178,7 @@
|
|||||||
"
|
"
|
||||||
:clickable="!!reviewsUrl"
|
:clickable="!!reviewsUrl"
|
||||||
@click="openReviews(extension)"
|
@click="openReviews(extension)"
|
||||||
></lnbits-extension-rating>
|
/>
|
||||||
<q-btn-group size="xs" style="margin: 5px 0">
|
<q-btn-group size="xs" style="margin: 5px 0">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="extension.hasFreeRelease"
|
v-if="extension.hasFreeRelease"
|
||||||
|
|||||||
@@ -63,7 +63,6 @@
|
|||||||
v-model:user-name="username"
|
v-model:user-name="username"
|
||||||
v-model:password_1="password"
|
v-model:password_1="password"
|
||||||
v-model:password_2="passwordRepeat"
|
v-model:password_2="passwordRepeat"
|
||||||
v-model:invitation-code="invitationCode"
|
|
||||||
v-model:reset-key="reset_key"
|
v-model:reset-key="reset_key"
|
||||||
@login="login"
|
@login="login"
|
||||||
@register="register"
|
@register="register"
|
||||||
|
|||||||
@@ -218,26 +218,6 @@
|
|||||||
:label="$t('update_account')"
|
:label="$t('update_account')"
|
||||||
class="q-ml-md"
|
class="q-ml-md"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-if="activeUser.data.id"
|
|
||||||
outline
|
|
||||||
color="primary"
|
|
||||||
class="q-ml-md"
|
|
||||||
>
|
|
||||||
<q-toggle
|
|
||||||
size="xs"
|
|
||||||
color="secondary"
|
|
||||||
v-model="activeUser.data.admin"
|
|
||||||
@update:model-value="toggleAdmin(activeUser.data.id)"
|
|
||||||
:label="
|
|
||||||
activeUser.data.admin
|
|
||||||
? $t('revoke_admin')
|
|
||||||
: $t('make_user_admin')
|
|
||||||
"
|
|
||||||
>
|
|
||||||
</q-toggle>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-else
|
v-else
|
||||||
@click="createUser()"
|
@click="createUser()"
|
||||||
@@ -610,9 +590,9 @@
|
|||||||
<q-btn
|
<q-btn
|
||||||
@click="showAccountPage(props.row.id)"
|
@click="showAccountPage(props.row.id)"
|
||||||
round
|
round
|
||||||
:icon="props.row.is_admin ? 'admin_panel_settings' : 'edit'"
|
icon="edit"
|
||||||
size="sm"
|
size="sm"
|
||||||
:color="props.row.is_admin ? 'primary' : 'secondary'"
|
color="secondary"
|
||||||
class="q-ml-xs"
|
class="q-ml-xs"
|
||||||
>
|
>
|
||||||
<q-tooltip>
|
<q-tooltip>
|
||||||
@@ -625,18 +605,10 @@
|
|||||||
size="xs"
|
size="xs"
|
||||||
v-if="!props.row.is_super_user"
|
v-if="!props.row.is_super_user"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
v-model="props.row.activated"
|
v-model="props.row.is_admin"
|
||||||
@update:model-value="toggleUserActivated(props.row.id)"
|
@update:model-value="toggleAdmin(props.row.id)"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip>Toggle Admin</q-tooltip>
|
||||||
><span
|
|
||||||
v-text="
|
|
||||||
props.row.activated
|
|
||||||
? $t('deactivate')
|
|
||||||
: $t('activate')
|
|
||||||
"
|
|
||||||
></span
|
|
||||||
></q-tooltip>
|
|
||||||
</q-toggle>
|
</q-toggle>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
|
|||||||
@@ -292,8 +292,7 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
|||||||
async def btc_price(currency: str) -> float:
|
async def btc_price(currency: str) -> float:
|
||||||
rates = await btc_rates(currency)
|
rates = await btc_rates(currency)
|
||||||
if not rates:
|
if not rates:
|
||||||
logger.warning("Could not fetch any Bitcoin price.")
|
raise ValueError("Could not fetch any Bitcoin price.")
|
||||||
return 0.0
|
|
||||||
elif len(rates) == 1:
|
elif len(rates) == 1:
|
||||||
logger.warning("Could only fetch one Bitcoin price.")
|
logger.warning("Could only fetch one Bitcoin price.")
|
||||||
|
|
||||||
@@ -307,8 +306,7 @@ async def get_fiat_rate_and_price_satoshis(currency: str) -> tuple[float, float]
|
|||||||
f"btc-price-{currency}",
|
f"btc-price-{currency}",
|
||||||
settings.lnbits_exchange_rate_cache_seconds,
|
settings.lnbits_exchange_rate_cache_seconds,
|
||||||
)
|
)
|
||||||
rate = float(100_000_000 / price) if price > 0 else 0.0
|
return float(100_000_000 / price), price
|
||||||
return rate, price
|
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_rate_satoshis(currency: str) -> float:
|
async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||||
@@ -318,13 +316,9 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
|
|||||||
|
|
||||||
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||||
rate = await get_fiat_rate_satoshis(currency)
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
if rate > 0:
|
return int(amount * (rate))
|
||||||
return int(amount * rate)
|
|
||||||
raise ValueError(f"Could not get exchange rate for {currency}.")
|
|
||||||
|
|
||||||
|
|
||||||
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
||||||
rate = await get_fiat_rate_satoshis(currency)
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
if rate > 0:
|
return float(amount / rate)
|
||||||
return float(amount / rate)
|
|
||||||
raise ValueError(f"Could not get exchange rate for {currency}.")
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.4.1"
|
version = "1.4.2"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
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)}."
|
), f"Expected 1 default wallet, not {len(user.wallets)}."
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_register_no_activation_code(
|
|
||||||
http_client: AsyncClient, settings: Settings
|
|
||||||
):
|
|
||||||
settings.lnbits_require_user_activation = True
|
|
||||||
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.json().get("detail") == "No activation method provided."
|
|
||||||
|
|
||||||
settings.lnbits_user_activation_by_invitation_code = True
|
|
||||||
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400, "User creation blocked without activation code."
|
|
||||||
assert response.json().get("detail") == "Invitation code cannot be empty."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_register_invalid_activation_code(
|
|
||||||
http_client: AsyncClient, settings: Settings
|
|
||||||
):
|
|
||||||
settings.lnbits_require_user_activation = True
|
|
||||||
settings.lnbits_user_activation_by_invitation_code = True
|
|
||||||
settings.lnbits_register_reusable_activation_code = "foo"
|
|
||||||
settings.lnbits_register_one_time_activation_codes = ["baz", "qux"]
|
|
||||||
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
"invitation_code": "bar",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.json().get("detail") == "Invalid invitation code."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_register_reusable_activation_code(
|
|
||||||
http_client: AsyncClient, settings: Settings
|
|
||||||
):
|
|
||||||
settings.lnbits_require_user_activation = True
|
|
||||||
settings.lnbits_user_activation_by_invitation_code = True
|
|
||||||
settings.lnbits_register_reusable_activation_code = "foo"
|
|
||||||
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
"invitation_code": "foo",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200, "User created with reusable code."
|
|
||||||
assert response.json().get("access_token") is not None
|
|
||||||
|
|
||||||
# Register again with the same code
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
"invitation_code": "foo",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200, "User created with reusable code."
|
|
||||||
assert response.json().get("access_token") is not None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_register_one_time_activation_code(
|
|
||||||
http_client: AsyncClient, settings: Settings
|
|
||||||
):
|
|
||||||
settings.lnbits_require_user_activation = True
|
|
||||||
settings.lnbits_user_activation_by_invitation_code = True
|
|
||||||
settings.lnbits_register_reusable_activation_code = "foo"
|
|
||||||
settings.lnbits_register_one_time_activation_codes = ["baz", "qux"]
|
|
||||||
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
"invitation_code": "baz",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200, "User created with one-time code."
|
|
||||||
assert response.json().get("access_token") is not None
|
|
||||||
|
|
||||||
# Register again with the same code
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
"invitation_code": "baz",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400, "Invalid invitation code."
|
|
||||||
assert response.json().get("detail") == "Invalid invitation code."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_register_email_twice(http_client: AsyncClient):
|
async def test_register_email_twice(http_client: AsyncClient):
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
tiny_id = shortuuid.uuid()[:8]
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import jwt
|
|
||||||
import pytest
|
import pytest
|
||||||
import shortuuid
|
import shortuuid
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_wallets
|
from lnbits.core.crud.wallets import get_wallets
|
||||||
from lnbits.core.models import AccessTokenPayload, Payment
|
|
||||||
from lnbits.core.models.users import Account, User
|
from lnbits.core.models.users import Account, User
|
||||||
from lnbits.core.services.users import create_user_account
|
from lnbits.core.services.users import create_user_account
|
||||||
from lnbits.settings import Settings
|
from lnbits.settings import Settings
|
||||||
@@ -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)
|
undeleted_wallet = next((w for w in wallets if w.id == wallet_id), None)
|
||||||
assert undeleted_wallet is not None
|
assert undeleted_wallet is not None
|
||||||
assert undeleted_wallet.deleted is False
|
assert undeleted_wallet.deleted is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_user_activation(
|
|
||||||
http_client: AsyncClient, invoice: Payment, settings: Settings, superuser_token: str
|
|
||||||
):
|
|
||||||
|
|
||||||
# Register a new user
|
|
||||||
username = f"u21.{shortuuid.uuid()[:8]}"
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": username,
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"{username}@lnbits.com",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
access_token = response.json().get("access_token")
|
|
||||||
assert response.status_code == 200, "User created."
|
|
||||||
assert response.json().get("access_token") is not None
|
|
||||||
|
|
||||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
|
||||||
access_token_payload = AccessTokenPayload(**payload)
|
|
||||||
user_id = access_token_payload.usr
|
|
||||||
assert user_id is not None
|
|
||||||
|
|
||||||
# Login works
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 200, "User logs in OK"
|
|
||||||
|
|
||||||
# Deactivate the user
|
|
||||||
respones = await http_client.put(
|
|
||||||
f"/users/api/v1/user/{user_id}/activate",
|
|
||||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert respones.status_code == 200, "User deactivated."
|
|
||||||
assert respones.json().get("message") == "User deactivated."
|
|
||||||
|
|
||||||
# Login should now fail
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json().get("detail") == "Invalid credentials."
|
|
||||||
|
|
||||||
response = await http_client.get(
|
|
||||||
"/api/v1/auth",
|
|
||||||
headers={"Authorization": f"Bearer {access_token}"},
|
|
||||||
)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json().get("detail") == "User not found."
|
|
||||||
|
|
||||||
wallets = await get_wallets(user_id=user_id)
|
|
||||||
assert len(wallets) == 1, "User's wallet still exists."
|
|
||||||
wallet = wallets[0]
|
|
||||||
|
|
||||||
response = await http_client.get(
|
|
||||||
"/api/v1/payments/paginated",
|
|
||||||
params={"limit": 2},
|
|
||||||
headers={"x-Api-Key": wallet.inkey},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert response.json().get("detail") == "Wallet not found."
|
|
||||||
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/payments",
|
|
||||||
json={
|
|
||||||
"out": False,
|
|
||||||
"amount": 1000,
|
|
||||||
"memo": "test payment",
|
|
||||||
},
|
|
||||||
headers={"x-Api-Key": wallet.inkey},
|
|
||||||
)
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert response.json().get("detail") == "Wallet not found."
|
|
||||||
|
|
||||||
data = {"out": True, "bolt11": invoice.bolt11}
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/payments",
|
|
||||||
json=data,
|
|
||||||
headers={"x-Api-Key": wallet.adminkey},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert response.json().get("detail") == "Wallet not found."
|
|
||||||
|
|
||||||
# Reactivate the user
|
|
||||||
response = await http_client.put(
|
|
||||||
f"/users/api/v1/user/{user_id}/activate",
|
|
||||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
||||||
)
|
|
||||||
print("### response", response.text)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json().get("message") == "User activated."
|
|
||||||
|
|
||||||
# Login should now pass
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 200, "User logs in OK again."
|
|
||||||
|
|
||||||
response = await http_client.get(
|
|
||||||
"/api/v1/payments/paginated",
|
|
||||||
params={"limit": 2},
|
|
||||||
headers={"x-Api-Key": wallet.inkey},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/payments",
|
|
||||||
json={
|
|
||||||
"out": False,
|
|
||||||
"amount": 1000,
|
|
||||||
"memo": "test payment",
|
|
||||||
},
|
|
||||||
headers={"x-Api-Key": wallet.inkey},
|
|
||||||
)
|
|
||||||
assert response.status_code == 201
|
|
||||||
|
|||||||
@@ -341,7 +341,3 @@ def _settings_cleanup(settings: Settings):
|
|||||||
settings.lnbits_max_outgoing_payment_amount_sats = 10_000_000_100
|
settings.lnbits_max_outgoing_payment_amount_sats = 10_000_000_100
|
||||||
settings.lnbits_max_incoming_payment_amount_sats = 10_000_000_200
|
settings.lnbits_max_incoming_payment_amount_sats = 10_000_000_200
|
||||||
settings.stripe_limits = FiatProviderLimits()
|
settings.stripe_limits = FiatProviderLimits()
|
||||||
settings.lnbits_require_user_activation = False
|
|
||||||
settings.lnbits_user_activation_by_invitation_code = False
|
|
||||||
settings.lnbits_register_reusable_activation_code = ""
|
|
||||||
settings.lnbits_register_one_time_activation_codes = []
|
|
||||||
|
|||||||
Reference in New Issue
Block a user