diff --git a/.env.example b/.env.example index b481a3c6c..5e9c8214e 100644 --- a/.env.example +++ b/.env.example @@ -162,6 +162,8 @@ LNBITS_ADMIN_USERS="" # Extensions only admin can access LNBITS_ADMIN_EXTENSIONS="ngrok, admin" +# Extensions enabled by default when a user is created +LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp" # Start LNbits core only. The extensions are not loaded. # LNBITS_EXTENSIONS_DEACTIVATE_ALL=true diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 7e51ed35d..51a8cf4e0 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -2,7 +2,7 @@ import datetime import json from time import time from typing import Any, Dict, List, Literal, Optional -from uuid import UUID, uuid4 +from uuid import uuid4 import shortuuid from passlib.context import CryptContext @@ -26,7 +26,6 @@ from lnbits.settings import ( from .models import ( Account, AccountFilters, - CreateUser, Payment, PaymentFilters, PaymentHistoryPoint, @@ -42,63 +41,23 @@ from .models import ( # -------- -async def create_user( - data: CreateUser, user_config: Optional[UserConfig] = None -) -> User: - if not settings.new_accounts_allowed: - raise ValueError("Account creation is disabled.") - if await get_account_by_username(data.username): - raise ValueError("Username already exists.") - - if data.email and await get_account_by_email(data.email): - raise ValueError("Email already exists.") - - pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - - user_id = uuid4().hex - tsph = db.timestamp_placeholder - now = int(time()) - await db.execute( - f""" - INSERT INTO accounts - (id, email, username, pass, extra, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, {tsph}, {tsph}) - """, - ( - user_id, - data.email, - data.username, - pwd_context.hash(data.password), - json.dumps(dict(user_config)) if user_config else "{}", - now, - now, - ), - ) - new_account = await get_account(user_id=user_id) - assert new_account, "Newly created account couldn't be retrieved" - return new_account - - async def create_account( - conn: Optional[Connection] = None, user_id: Optional[str] = None, + username: Optional[str] = None, email: Optional[str] = None, + password: Optional[str] = None, user_config: Optional[UserConfig] = None, + conn: Optional[Connection] = None, ) -> User: - if user_id: - user_uuid4 = UUID(hex=user_id, version=4) - assert user_uuid4.hex == user_id, "User ID is not valid UUID4 hex string" - else: - user_id = uuid4().hex - + user_id = user_id or uuid4().hex extra = json.dumps(dict(user_config)) if user_config else "{}" now = int(time()) await (conn or db).execute( f""" - INSERT INTO accounts (id, email, extra, created_at, updated_at) - VALUES (?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder}) + INSERT INTO accounts (id, username, pass, email, extra, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder}) """, - (user_id, email, extra, now, now), + (user_id, username, password, email, extra, now, now), ) new_account = await get_account(user_id=user_id, conn=conn) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index e9237aa20..155ff470c 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -6,12 +6,14 @@ from io import BytesIO from pathlib import Path from typing import Dict, List, Optional, Tuple, TypedDict from urllib.parse import parse_qs, urlparse +from uuid import UUID, uuid4 import httpx from bolt11 import decode as bolt11_decode from cryptography.hazmat.primitives import serialization from fastapi import Depends, WebSocket from loguru import logger +from passlib.context import CryptContext from py_vapid import Vapid from py_vapid.utils import b64urlencode @@ -50,6 +52,8 @@ from .crud import ( create_wallet, delete_wallet_payment, get_account, + get_account_by_email, + get_account_by_username, get_payments, get_standalone_payment, get_super_settings, @@ -60,9 +64,10 @@ from .crud import ( update_payment_details, update_payment_status, update_super_user, + update_user_extension, ) from .helpers import to_valid_user_id -from .models import BalanceDelta, Payment, UserConfig, Wallet +from .models import BalanceDelta, Payment, User, UserConfig, Wallet class PaymentError(Exception): @@ -775,6 +780,38 @@ async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings return await create_admin_settings(account.id, editable_settings.dict()) +async def create_user_account( + user_id: Optional[str] = None, + email: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None, + user_config: Optional[UserConfig] = None, +) -> User: + if not settings.new_accounts_allowed: + raise ValueError("Account creation is disabled.") + if username and await get_account_by_username(username): + raise ValueError("Username already exists.") + + if email and await get_account_by_email(email): + raise ValueError("Email already exists.") + + if user_id: + user_uuid4 = UUID(hex=user_id, version=4) + assert user_uuid4.hex == user_id, "User ID is not valid UUID4 hex string" + else: + user_id = uuid4().hex + + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + password = pwd_context.hash(password) if password else None + + account = await create_account(user_id, username, email, password, user_config) + + for ext_id in settings.lnbits_user_default_extensions: + await update_user_extension(user_id=account.id, extension=ext_id, active=True) + + return account + + class WebsocketConnectionManager: def __init__(self) -> None: self.active_connections: List[WebSocket] = [] diff --git a/lnbits/core/templates/admin/_tab_server.html b/lnbits/core/templates/admin/_tab_server.html index 8c265a464..33843c51c 100644 --- a/lnbits/core/templates/admin/_tab_server.html +++ b/lnbits/core/templates/admin/_tab_server.html @@ -45,56 +45,7 @@
-
-
-

Admin Extensions

- -
-
-
-

Miscellaneous

- - - Disable Extensions - Disables all extensions - - - - - - - - Hide API - Hides wallet api, extensions can choose to honor - - - - - -
-
-
+
Service Fee
@@ -154,32 +105,94 @@
- +
Extensions
-
-

Extension Sources

- - - -
- +
+
+

Extension Sources

+ + + +
+ +
+
+
+
+
+

Admin Extensions

+ +
+ +
+

User Default Extensions

+ +
+
+

Miscellaneous

+ + + Disable Extensions + Disables all extensions + + + + + + + + Hide API + Hides wallet api, extensions can choose to honor + + + + + +
-
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 2856151ae..9a9f2ac74 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -37,10 +37,9 @@ from lnbits.utils.exchange_rates import ( ) from ..crud import ( - create_account, create_wallet, ) -from ..services import perform_lnurlauth +from ..services import create_user_account, perform_lnurlauth # backwards compatibility for extension # TODO: remove api_payment and pay_invoice imports from extensions @@ -70,7 +69,7 @@ async def api_create_account(data: CreateWallet) -> Wallet: status_code=HTTPStatus.FORBIDDEN, detail="Account creation is disabled.", ) - account = await create_account() + account = await create_user_account() return await create_wallet(user_id=account.id, wallet_name=data.name) diff --git a/lnbits/core/views/auth_api.py b/lnbits/core/views/auth_api.py index ec4bec133..df8c277e1 100644 --- a/lnbits/core/views/auth_api.py +++ b/lnbits/core/views/auth_api.py @@ -12,6 +12,7 @@ from starlette.status import ( HTTP_500_INTERNAL_SERVER_ERROR, ) +from lnbits.core.services import create_user_account from lnbits.decorators import check_user_exists from lnbits.helpers import ( create_access_token, @@ -23,8 +24,6 @@ from lnbits.helpers import ( from lnbits.settings import AuthMethods, settings from ..crud import ( - create_account, - create_user, get_account, get_account_by_email, get_account_by_username_or_email, @@ -166,7 +165,9 @@ async def register(data: CreateUser) -> JSONResponse: raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.") try: - user = await create_user(data) + user = await create_user_account( + email=data.email, username=data.username, password=data.password + ) return _auth_success_response(user.username) except ValueError as exc: @@ -274,7 +275,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] = else: if not settings.new_accounts_allowed: raise HTTPException(HTTP_400_BAD_REQUEST, "Account creation is disabled.") - user = await create_account(email=email, user_config=user_config) + user = await create_user_account(email=email, user_config=user_config) if not user: raise HTTPException(HTTP_401_UNAUTHORIZED, "User not found.") diff --git a/lnbits/settings.py b/lnbits/settings.py index 44b73c328..e25f1a81c 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -47,6 +47,7 @@ class UsersSettings(LNbitsSettings): class ExtensionsSettings(LNbitsSettings): lnbits_admin_extensions: list[str] = Field(default=[]) + lnbits_user_default_extensions: list[str] = Field(default=[]) lnbits_extensions_deactivate_all: bool = Field(default=False) lnbits_extensions_manifests: list[str] = Field( default=[