diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 8bb93a027..0ec9f2d99 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -1,10 +1,10 @@ import json +from datetime import datetime from time import time from typing import Literal, Optional from uuid import uuid4 import shortuuid -from passlib.context import CryptContext from lnbits.core.db import db from lnbits.core.extensions.models import ( @@ -32,92 +32,25 @@ from .models import ( UpdateUserPassword, UpdateUserPubkey, User, - UserConfig, Wallet, WebPushSubscription, ) async def create_account( - user_id: Optional[str] = None, - username: Optional[str] = None, - pubkey: Optional[str] = None, - email: Optional[str] = None, - password: Optional[str] = None, - user_config: Optional[UserConfig] = None, + account: Optional[Account] = None, conn: Optional[Connection] = None, -) -> User: - user_id = user_id or uuid4().hex - extra = json.dumps(dict(user_config)) if user_config else "{}" - now = int(time()) - now_ph = db.timestamp_placeholder("now") - await (conn or db).execute( - f""" - INSERT INTO accounts - (id, username, pass, email, pubkey, extra, created_at, updated_at) - VALUES - (:user, :username, :password, :email, :pubkey, :extra, {now_ph}, {now_ph}) - """, - { - "user": user_id, - "username": username, - "password": password, - "email": email, - "pubkey": pubkey, - "extra": extra, - "now": now, - }, - ) - - new_account = await get_account(user_id=user_id, conn=conn) - assert new_account, "Newly created account couldn't be retrieved" - - return new_account +) -> Account: + if not account: + now = datetime.now() + account = Account(id=uuid4().hex, created_at=now, updated_at=now) + await (conn or db).insert("accounts", account) + return account -async def update_account( - user_id: str, - username: Optional[str] = None, - email: Optional[str] = None, - user_config: Optional[UserConfig] = None, -) -> Optional[User]: - user = await get_account(user_id) - assert user, "User not found" - - if email: - assert not user.email or email == user.email, "Cannot change email." - account = await get_account_by_email(email) - assert not account or account.id == user_id, "Email already in use." - - if username: - assert not user.username or username == user.username, "Cannot change username." - account = await get_account_by_username(username) - assert not account or account.id == user_id, "Username already exists." - - username = user.username or username - email = user.email or email - extra = user_config or user.config - - now = int(time()) - now_ph = db.timestamp_placeholder("now") - await db.execute( - f""" - UPDATE accounts SET (username, email, extra, updated_at) = - (:username, :email, :extra, {now_ph}) - WHERE id = :user - """, - { - "username": username, - "email": email, - "extra": json.dumps(dict(extra)) if extra else "{}", - "now": now, - "user": user_id, - }, - ) - - user = await get_user(user_id) - assert user, "Updated account couldn't be retrieved" - return user +async def update_account(account: Account) -> None: + account.updated_at = datetime.now() + await db.update("accounts", account) async def delete_account(user_id: str, conn: Optional[Connection] = None) -> None: @@ -162,16 +95,12 @@ async def get_accounts( async def get_account( user_id: str, conn: Optional[Connection] = None -) -> Optional[User]: - user = await (conn or db).fetchone( - """ - SELECT id, email, username, pubkey, created_at, updated_at, extra - FROM accounts WHERE id = :id - """, +) -> Optional[Account]: + return await (conn or db).fetchone( + "SELECT * FROM accounts WHERE id = :id", {"id": user_id}, - User, + Account, ) - return user async def delete_accounts_no_wallets( @@ -193,24 +122,6 @@ async def delete_accounts_no_wallets( ) -async def get_user_password(user_id: str) -> Optional[str]: - row = await db.fetchone( - "SELECT pass FROM accounts WHERE id = :user", - {"user": user_id}, - ) - return row.get("pass") - - -# TODO: refactor not a crud function -async def verify_user_password(user_id: str, password: str) -> bool: - existing_password = await get_user_password(user_id) - if not existing_password: - return False - - pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - return pwd_context.verify(password, existing_password) - - async def update_user_password(data: UpdateUserPassword, last_login_time: int) -> User: assert 0 <= time() - last_login_time <= settings.auth_credetials_update_threshold, ( @@ -272,93 +183,58 @@ async def update_user_pubkey(data: UpdateUserPubkey, last_login_time: int) -> Us async def get_account_by_username( username: str, conn: Optional[Connection] = None -) -> Optional[User]: - row = await (conn or db).fetchone( - """ - SELECT id, username, pubkey, email, created_at, updated_at - FROM accounts WHERE username = :username - """, +) -> Optional[Account]: + return await (conn or db).fetchone( + "SELECT * FROM accounts WHERE username = :username", {"username": username}, + Account, ) - return User(**row) if row else None - async def get_account_by_pubkey( pubkey: str, conn: Optional[Connection] = None -) -> Optional[User]: - row = await (conn or db).fetchone( - """ - SELECT id, username, pubkey, email, created_at, updated_at - FROM accounts WHERE pubkey = :pubkey - """, +) -> Optional[Account]: + return await (conn or db).fetchone( + "SELECT * FROM accounts WHERE pubkey = :pubkey", {"pubkey": pubkey}, + Account, ) - return User(**row) if row else None - - async def get_account_by_email( email: str, conn: Optional[Connection] = None -) -> Optional[User]: - row = await (conn or db).fetchone( - """ - SELECT id, username, pubkey, email, created_at, updated_at - FROM accounts WHERE email = :email - """, +) -> Optional[Account]: + return await (conn or db).fetchone( + "SELECT * FROM accounts WHERE email = :email", {"email": email}, + Account, ) - return User(**row) if row else None - async def get_account_by_username_or_email( username_or_email: str, conn: Optional[Connection] = None -) -> Optional[User]: - user = await get_account_by_username(username_or_email, conn) - if not user: - user = await get_account_by_email(username_or_email, conn) - return user - - -async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[User]: - user = await (conn or db).fetchone( - """ - SELECT id, email, username, pubkey, pass, extra, created_at, updated_at - FROM accounts WHERE id = :id - """, - {"id": user_id}, +) -> Optional[Account]: + return await (conn or db).fetchone( + "SELECT * FROM accounts WHERE email = :value or username = :value", + {"value": username_or_email}, + Account, ) - if user: - extensions = await get_user_active_extensions_ids(user_id, conn) - wallets = await (conn or db).fetchall( - """ - SELECT *, COALESCE(( - SELECT balance FROM balances WHERE wallet = wallets.id - ), 0) AS balance_msat - FROM wallets - WHERE "user" = :user and wallets.deleted = false - """, - {"user": user_id}, - ) - else: - return None +async def get_user(account: Account, conn: Optional[Connection] = None) -> User: + extensions = await get_user_active_extensions_ids(account.id, conn) + wallets = await get_wallets(account.id, conn) return User( - id=user["id"], - email=user["email"], - username=user["username"], - pubkey=user["pubkey"], - extensions=[ - e for e in extensions if User.is_extension_for_user(e[0], user["id"]) - ], - wallets=[Wallet(**w) for w in wallets], - admin=user["id"] == settings.super_user - or user["id"] in settings.lnbits_admin_users, - super_user=user["id"] == settings.super_user, - has_password=True if user["pass"] else False, - config=UserConfig(**json.loads(user["extra"])) if user["extra"] else None, + id=account.id, + email=account.email, + username=account.username, + extra=account.extra, + created_at=account.created_at, + updated_at=account.updated_at, + extensions=extensions, + wallets=wallets, + admin=account.is_super_user or account.is_admin or False, + super_user=account.is_super_user or False, + has_password=account.password_hash is not None, ) @@ -437,7 +313,7 @@ async def delete_installed_extension( ) -async def drop_extension_db(*, ext_id: str, conn: Optional[Connection] = None) -> None: +async def drop_extension_db(ext_id: str, conn: Optional[Connection] = None) -> None: db_version = await (conn or db).fetchone( "SELECT * FROM dbversions WHERE db = :id", {"id": ext_id}, @@ -504,19 +380,17 @@ async def get_user_extensions( ) +async def create_user_extension( + user_extension: UserExtension, conn: Optional[Connection] = None +) -> None: + await (conn or db).insert("extensions", user_extension) + + async def update_user_extension( user_extension: UserExtension, conn: Optional[Connection] = None ) -> None: where = """extension = :extension AND "user" = :user""" await (conn or db).update("extensions", user_extension, where) - # await (conn or db).execute( - # """ - # INSERT INTO extensions ("user", extension, active) - # VALUES (:user, :ext, :active) - # ON CONFLICT ("user", extension) DO UPDATE SET active = :active - # """, - # {"user": user_id, "ext": extension, "active": active}, - # ) async def get_user_active_extensions_ids( @@ -623,7 +497,7 @@ async def force_delete_wallet( async def delete_wallet_by_id( - *, wallet_id: str, conn: Optional[Connection] = None + wallet_id: str, conn: Optional[Connection] = None ) -> Optional[int]: now = int(time()) result = await (conn or db).execute( @@ -675,38 +549,34 @@ async def get_wallet( async def get_wallets(user_id: str, conn: Optional[Connection] = None) -> list[Wallet]: - rows = await (conn or db).fetchall( + return await (conn or db).fetchall( """ SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat FROM wallets WHERE "user" = :user """, {"user": user_id}, + Wallet, ) - return [Wallet(**row) for row in rows] - async def get_wallet_for_key( key: str, conn: Optional[Connection] = None, ) -> Optional[Wallet]: - row = await (conn or db).fetchone( + return await (conn or db).fetchone( """ SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat FROM wallets WHERE (adminkey = :key OR inkey = :key) AND deleted = false """, {"key": key}, + Wallet, ) - if not row: - return None - - return Wallet(**row) - async def get_total_balance(conn: Optional[Connection] = None): - row = await (conn or db).fetchone("SELECT SUM(balance) FROM balances") + result = await (conn or db).execute("SELECT SUM(balance) FROM balances") + row = result.mappings().first() return row.get("balance", 0) @@ -759,8 +629,10 @@ async def get_wallet_payment( return payment -async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: int = 5): - rows = await db.fetchall( +async def get_latest_payments_by_extension( + ext_name: str, ext_id: str, limit: int = 5 +) -> list[Payment]: + return await db.fetchall( f""" SELECT * FROM apipayments WHERE status = '{PaymentState.SUCCESS}' @@ -769,10 +641,9 @@ async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: in ORDER BY time DESC LIMIT {limit} """, {"ext_name": f"%{ext_name}%", "ext_id": f"%{ext_id}%"}, + Payment, ) - return rows - async def get_payments_paginated( *, @@ -1254,19 +1125,19 @@ async def create_tinyurl(domain: str, endless: bool, wallet: str): async def get_tinyurl(tinyurl_id: str) -> Optional[TinyURL]: - row = await db.fetchone( + return await db.fetchone( "SELECT * FROM tiny_url WHERE id = :tinyurl", {"tinyurl": tinyurl_id}, + TinyURL, ) - return TinyURL.from_row(row) if row else None async def get_tinyurl_by_url(url: str) -> list[TinyURL]: - rows = await db.fetchall( + return await db.fetchall( "SELECT * FROM tiny_url WHERE url = :url", {"url": url}, + TinyURL, ) - return [TinyURL.from_row(row) for row in rows] async def delete_tinyurl(tinyurl_id: str): diff --git a/lnbits/core/extensions/models.py b/lnbits/core/extensions/models.py index 8273c8deb..d762f43d3 100644 --- a/lnbits/core/extensions/models.py +++ b/lnbits/core/extensions/models.py @@ -120,6 +120,7 @@ class UserExtensionInfo(BaseModel): class UserExtension(BaseModel): + user: str extension: str active: bool extra: Optional[UserExtensionInfo] = None diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index d79e072e2..3a6de6ea0 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -562,3 +562,4 @@ async def m023_add_column_column_to_apipayments(db): await db.execute("DROP INDEX by_hash") await db.execute("ALTER TABLE apipayments RENAME COLUMN hash TO payment_hash") await db.execute("ALTER TABLE apipayments RENAME COLUMN wallet TO wallet_id") + await db.execute("ALTER TABLE accounts RENAME COLUMN pass TO password_hash") diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 227a627fc..451c68d35 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -1,15 +1,16 @@ from __future__ import annotations -import datetime import hashlib import hmac import time from dataclasses import dataclass +from datetime import datetime from enum import Enum from typing import Callable, Optional from ecdsa import SECP256k1, SigningKey from fastapi import Query +from passlib.context import CryptContext from pydantic import BaseModel, validator from lnbits.db import FilterModel @@ -104,14 +105,37 @@ class UserExtra(BaseModel): class Account(BaseModel): id: str - is_super_user: Optional[bool] = False - is_admin: Optional[bool] = False username: Optional[str] = None + password_hash: Optional[str] = None email: Optional[str] = None balance_msat: Optional[int] = 0 transaction_count: Optional[int] = 0 wallet_count: Optional[int] = 0 - last_payment: Optional[datetime.datetime] = None + last_payment: Optional[datetime] = None + extra: Optional[UserExtra] = None + created_at: datetime = datetime.now() + updated_at: datetime = datetime.now() + + @property + def is_super_user(self) -> bool: + return self.id == settings.super_user + + @property + def is_admin(self) -> bool: + return self.id in settings.lnbits_admin_users + + def hash_password(self, password: str) -> str: + """sets and returns the hashed password""" + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + self.password_hash = pwd_context.hash(password) + return self.password_hash + + def verify_password(self, password: str) -> bool: + """returns True if the password matches the hash""" + if not self.password_hash: + return False + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + return pwd_context.verify(password, self.password_hash) class AccountFilters(FilterModel): @@ -126,7 +150,7 @@ class AccountFilters(FilterModel): ] id: str - last_payment: Optional[datetime.datetime] = None + last_payment: Optional[datetime] = None transaction_count: Optional[int] = None wallet_count: Optional[int] = None username: Optional[str] = None @@ -135,6 +159,8 @@ class AccountFilters(FilterModel): class User(BaseModel): id: str + created_at: datetime + updated_at: datetime email: Optional[str] = None username: Optional[str] = None pubkey: Optional[str] = None @@ -144,8 +170,6 @@ class User(BaseModel): super_user: bool = False has_password: bool = False extra: Optional[UserExtra] = None - created_at: Optional[int] = None - updated_at: Optional[int] = None @property def wallet_ids(self) -> list[str]: @@ -237,7 +261,7 @@ class CreatePayment(BaseModel): amount: int memo: str preimage: Optional[str] = None - expiry: Optional[datetime.datetime] = None + expiry: Optional[datetime] = None extra: Optional[dict] = None webhook: Optional[str] = None fee: int = 0 @@ -336,11 +360,11 @@ class PaymentFilters(FilterModel): amount: int fee: int memo: Optional[str] - time: datetime.datetime + time: datetime bolt11: str preimage: str payment_hash: str - expiry: Optional[datetime.datetime] + expiry: Optional[datetime] extra: dict = {} wallet_id: str webhook: Optional[str] @@ -348,7 +372,7 @@ class PaymentFilters(FilterModel): class PaymentHistoryPoint(BaseModel): - date: datetime.datetime + date: datetime income: int spending: int balance: int diff --git a/lnbits/core/services.py b/lnbits/core/services.py index cc0ddccfc..2791e7ddb 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,11 +1,12 @@ import asyncio import json import time +from datetime import datetime from io import BytesIO from pathlib import Path from typing import Optional from urllib.parse import parse_qs, urlparse -from uuid import UUID, uuid4 +from uuid import uuid4 import httpx from bolt11 import MilliSatoshi @@ -13,7 +14,6 @@ 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 @@ -52,8 +52,6 @@ from .crud import ( create_payment, create_wallet, get_account, - get_account_by_email, - get_account_by_username, get_payments, get_standalone_payment, get_super_settings, @@ -64,16 +62,15 @@ 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 ( + Account, BalanceDelta, CreatePayment, Payment, PaymentState, - User, - UserConfig, + UserExtra, Wallet, ) @@ -762,7 +759,7 @@ async def check_admin_settings(): send_admin_user_to_saas() account = await get_account(settings.super_user) - if account and account.config and account.config.provider == "env": + if account and account.extra and account.extra.provider == "env": settings.first_install = True logger.success( @@ -809,59 +806,28 @@ def update_cached_settings(sets_dict: dict): async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings: + async def new_account(account_id: str) -> Account: + now = datetime.now() + account = Account( + id=account_id, + extra=UserExtra(provider="env"), + created_at=now, + updated_at=now, + ) + await create_account(account) + return account + account = None if super_user: account = await get_account(super_user) if not account: - account = await create_account( - user_id=super_user, user_config=UserConfig(provider="env") - ) - if not account.wallets or len(account.wallets) == 0: + account = await new_account(super_user or uuid4().hex) await create_wallet(user_id=account.id) editable_settings = EditableSettings.from_dict(settings.dict()) - 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, - pubkey: Optional[str] = None, - password: Optional[str] = None, - wallet_name: 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, pubkey, email, password, user_config - ) - wallet = await create_wallet(user_id=account.id, wallet_name=wallet_name) - account.wallets = [wallet] - - 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/views/api.py b/lnbits/core/views/api.py index af7e10d64..18eba2804 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -3,7 +3,6 @@ import json from http import HTTPStatus from io import BytesIO from time import time -from typing import Dict, List from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse import httpx @@ -13,8 +12,9 @@ from fastapi import ( Depends, ) from fastapi.exceptions import HTTPException -from starlette.responses import StreamingResponse +from fastapi.responses import StreamingResponse +from lnbits.core.crud import create_account, create_wallet from lnbits.core.models import ( BaseWallet, ConversionData, @@ -38,11 +38,7 @@ from lnbits.utils.exchange_rates import ( satoshis_amount_as_fiat, ) -from ..services import create_user_account, perform_lnurlauth - -# backwards compatibility for extension -# TODO: remove api_payment and pay_invoice imports from extensions -from .payment_api import api_payment, pay_invoice # noqa: F401 +from ..services import perform_lnurlauth api_router = APIRouter(tags=["Core"]) @@ -61,7 +57,7 @@ async def health() -> dict: name="Wallets", description="Get basic info for all of user's wallets.", ) -async def api_wallets(user: User = Depends(check_user_exists)) -> List[BaseWallet]: +async def api_wallets(user: User = Depends(check_user_exists)) -> list[BaseWallet]: return [BaseWallet(**w.dict()) for w in user.wallets] @@ -72,8 +68,9 @@ async def api_create_account(data: CreateWallet) -> Wallet: status_code=HTTPStatus.FORBIDDEN, detail="Account creation is disabled.", ) - account = await create_user_account(wallet_name=data.name) - return account.wallets[0] + account = await create_account() + wallet = await create_wallet(user_id=account.id, wallet_name=data.name) + return wallet @api_router.get("/api/v1/lnurlscan/{code}") @@ -101,7 +98,7 @@ async def api_lnurlscan( ) from exc # params is what will be returned to the client - params: Dict = {"domain": domain} + params: dict = {"domain": domain} if "tag=login" in url: params.update(kind="auth") @@ -150,7 +147,7 @@ async def api_lnurlscan( # callback with k1 already in it parsed_callback: ParseResult = urlparse(data["callback"]) - qs: Dict = parse_qs(parsed_callback.query) + qs: dict = parse_qs(parsed_callback.query) qs["k1"] = data["k1"] # balanceCheck/balanceNotify @@ -207,13 +204,13 @@ async def api_perform_lnurlauth( @api_router.get("/api/v1/rate/{currency}") -async def api_check_fiat_rate(currency: str) -> Dict[str, float]: +async def api_check_fiat_rate(currency: str) -> dict[str, float]: rate = await get_fiat_rate_satoshis(currency) return {"rate": rate} @api_router.get("/api/v1/currencies") -async def api_list_currencies_available() -> List[str]: +async def api_list_currencies_available() -> list[str]: return allowed_currencies() diff --git a/lnbits/core/views/auth_api.py b/lnbits/core/views/auth_api.py index d64a84ac9..4ba8097d9 100644 --- a/lnbits/core/views/auth_api.py +++ b/lnbits/core/views/auth_api.py @@ -2,18 +2,13 @@ import base64 import importlib import json from time import time +from http import HTTPStatus from typing import Callable, Optional -from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import JSONResponse, RedirectResponse from fastapi_sso.sso.base import OpenID, SSOBase from loguru import logger -from starlette.status import ( - HTTP_400_BAD_REQUEST, - HTTP_401_UNAUTHORIZED, - HTTP_403_FORBIDDEN, - HTTP_500_INTERNAL_SERVER_ERROR, -) from lnbits.core.services import create_user_account from lnbits.decorators import access_token_payload, check_user_exists @@ -23,14 +18,17 @@ from lnbits.helpers import ( encrypt_internal_message, is_valid_email_address, is_valid_username, + urlsafe_short_hash, ) from lnbits.settings import AuthMethods, settings from lnbits.utils.nostr import normalize_public_key, verify_event from ..crud import ( + create_account, get_account, get_account_by_email, get_account_by_pubkey, + get_account_by_username, get_account_by_username_or_email, get_user, get_user_password, @@ -39,8 +37,10 @@ from ..crud import ( update_user_pubkey, verify_user_password, ) + from ..models import ( AccessTokenPayload, + Account, CreateUser, LoginUsernamePassword, LoginUsr, @@ -50,7 +50,7 @@ from ..models import ( UpdateUserPassword, UpdateUserPubkey, User, - UserConfig, + UserExtra, ) auth_router = APIRouter(prefix="/api/v1/auth", tags=["Auth"]) @@ -65,23 +65,14 @@ async def get_auth_user(user: User = Depends(check_user_exists)) -> User: async def login(data: LoginUsernamePassword) -> JSONResponse: if not settings.is_auth_method_allowed(AuthMethods.username_and_password): raise HTTPException( - HTTP_401_UNAUTHORIZED, "Login by 'Username and Password' not allowed." + HTTPStatus.UNAUTHORIZED, "Login by 'Username and Password' not allowed." ) - - try: - user = await get_account_by_username_or_email(data.username) - - if not user: - raise HTTPException(HTTP_401_UNAUTHORIZED, "Invalid credentials.") - if not await verify_user_password(user.id, data.password): - raise HTTPException(HTTP_401_UNAUTHORIZED, "Invalid credentials.") - - return _auth_success_response(user.username, user.id, user.email) - except HTTPException as exc: - raise exc - except Exception as exc: - logger.debug(exc) - raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.") from exc + account = await get_account_by_username_or_email(data.username) + if not account or not account.verify_password(data.password): + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid credentials." + ) + return _auth_success_response(account.username, account.id) @auth_router.post("/nostr", description="Login via Nostr") @@ -111,19 +102,16 @@ async def nostr_login(request: Request) -> JSONResponse: @auth_router.post("/usr", description="Login via the User ID") async def login_usr(data: LoginUsr) -> JSONResponse: if not settings.is_auth_method_allowed(AuthMethods.user_id_only): - raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'User ID' not allowed.") - - try: - user = await get_user(data.usr) - if not user: - raise HTTPException(HTTP_401_UNAUTHORIZED, "User ID does not exist.") - - return _auth_success_response(user.username or "", user.id, user.email) - except HTTPException as exc: - raise exc - except Exception as exc: - logger.debug(exc) - raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.") from exc + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, + detail="Login by 'User ID' not allowed.", + ) + account = await get_account(data.usr) + if not account: + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, detail="User ID does not exist." + ) + return _auth_success_response(account.username, account.id) @auth_router.get("/{provider}", description="SSO Provider") @@ -133,7 +121,8 @@ async def login_with_sso_provider( provider_sso = _new_sso(provider) if not provider_sso: raise HTTPException( - HTTP_401_UNAUTHORIZED, f"Login by '{provider}' not allowed." + status_code=HTTPStatus.UNAUTHORIZED, + detail=f"Login by '{provider}' not allowed.", ) provider_sso.redirect_uri = str(request.base_url) + f"api/v1/auth/{provider}/token" @@ -147,7 +136,8 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons provider_sso = _new_sso(provider) if not provider_sso: raise HTTPException( - HTTP_401_UNAUTHORIZED, f"Login by '{provider}' not allowed." + status_code=HTTPStatus.UNAUTHORIZED, + detail=f"Login by '{provider}' not allowed.", ) try: @@ -160,18 +150,18 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons except HTTPException as exc: raise exc except ValueError as exc: - raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc + raise HTTPException(HTTPStatus.FORBIDDEN, str(exc)) from exc except Exception as exc: logger.debug(exc) raise HTTPException( - HTTP_500_INTERNAL_SERVER_ERROR, - f"Cannot authenticate user with {provider} Auth.", + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=f"Cannot authenticate user with {provider} Auth.", ) from exc @auth_router.post("/logout") async def logout() -> JSONResponse: - response = JSONResponse({"status": "success"}, status_code=status.HTTP_200_OK) + response = JSONResponse({"status": "success"}, status_code=HTTPStatus.OK) response.delete_cookie("cookie_access_token") response.delete_cookie("is_lnbits_user_authorized") response.delete_cookie("is_access_token_expired") @@ -184,62 +174,36 @@ async def logout() -> JSONResponse: async def register(data: CreateUser) -> JSONResponse: if not settings.is_auth_method_allowed(AuthMethods.username_and_password): raise HTTPException( - HTTP_401_UNAUTHORIZED, "Register by 'Username and Password' not allowed." + status_code=HTTPStatus.UNAUTHORIZED, + detail="Register by 'Username and Password' not allowed.", ) if data.password != data.password_repeat: - raise HTTPException(HTTP_400_BAD_REQUEST, "Passwords do not match.") + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Passwords do not match." + ) if not data.username: - raise HTTPException(HTTP_400_BAD_REQUEST, "Missing username.") + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Missing username." + ) if not is_valid_username(data.username): - raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid username.") + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Invalid username." + ) if data.email and not is_valid_email_address(data.email): - raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.") + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid email.") - try: - user = await create_user_account( - email=data.email, username=data.username, password=data.password - ) - return _auth_success_response(user.username, user.id, user.email) + account = Account( + id=urlsafe_short_hash(), + email=data.email, + username=data.username, + ) + account.hash_password(data.password) + await create_account(account) + return _auth_success_response(account.username) - except ValueError as exc: - raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc - except Exception as exc: - logger.debug(exc) - raise HTTPException( - HTTP_500_INTERNAL_SERVER_ERROR, "Cannot create user." - ) from exc - - -@auth_router.put("/password") -async def update_password( - data: UpdateUserPassword, - user: User = Depends(check_user_exists), - payload: AccessTokenPayload = Depends(access_token_payload), -) -> Optional[User]: - if data.user_id != user.id: - raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid user ID.") - - try: - if data.username and not user.username: - await update_account(user_id=user.id, username=data.username) - - # old accounts do not have a pasword - if await get_user_password(data.user_id): - assert data.password_old, "Missing old password" - old_pwd_ok = await verify_user_password(data.user_id, data.password_old) - assert old_pwd_ok, "Invalid credentials." - - return await update_user_password(data, payload.auth_time or 0) - except AssertionError as exc: - raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc - except Exception as exc: - logger.debug(exc) - raise HTTPException( - HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user password." - ) from exc @auth_router.put("/pubkey") @@ -259,52 +223,68 @@ async def update_pubkey( raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc except Exception as exc: logger.debug(exc) + + +@auth_router.put("/password") +async def update_password( + data: UpdateUserPassword, + user: User = Depends(check_user_exists), + payload: AccessTokenPayload = Depends(access_token_payload), +) -> Optional[User]: + if data.user_id != user.id: raise HTTPException( - HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user pubkey." - ) from exc + status_code=HTTPStatus.BAD_REQUEST, detail="Invalid user ID." + ) + if data.username and await get_account_by_username(data.username): + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Username already exists." + ) + + account = await get_account(user.id) + if not account: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Account not found." + ) + + account.username = data.username + account.hash_password(data.password) + await update_account(account) + return await get_user(account) @auth_router.put("/reset") async def reset_password(data: ResetUserPassword) -> JSONResponse: if not settings.is_auth_method_allowed(AuthMethods.username_and_password): raise HTTPException( - HTTP_401_UNAUTHORIZED, "Auth by 'Username and Password' not allowed." + HTTPStatus.UNAUTHORIZED, "Auth by 'Username and Password' not allowed." ) - try: - assert data.reset_key[:10] == "reset_key_", "This is not a reset key." + assert data.reset_key[:10] == "reset_key_", "This is not a reset key." - reset_data_json = decrypt_internal_message( - base64.b64decode(data.reset_key[10:]).decode() - ) - assert reset_data_json, "Cannot process reset key." + reset_data_json = decrypt_internal_message( + base64.b64decode(data.reset_key[10:]).decode() + ) + assert reset_data_json, "Cannot process reset key." - action, user_id, request_time = json.loads(reset_data_json) - assert action == "reset", "Expected reset action." - assert user_id is not None, "Missing user ID." - assert request_time is not None, "Missing reset time." + action, user_id, request_time = json.loads(reset_data_json) + assert action == "reset", "Expected reset action." + assert user_id is not None, "Missing user ID." + assert request_time is not None, "Missing reset time." - user = await get_account(user_id) - assert user, "User not found." + user = await get_account(user_id) + assert user, "User not found." - update_pwd = UpdateUserPassword( - user_id=user.id, - username=user.username or "", - password=data.password, - password_repeat=data.password_repeat, - ) - user = await update_user_password(update_pwd, request_time) + update_pwd = UpdateUserPassword( + user_id=user.id, + username=user.username or "", + password=data.password, + password_repeat=data.password_repeat, + ) + user = await update_user_password(update_pwd, request_time) - return _auth_success_response( - username=user.username, user_id=user_id, email=user.email - ) - except AssertionError as exc: - raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc - except Exception as exc: - logger.warning(exc) - raise HTTPException( - HTTP_500_INTERNAL_SERVER_ERROR, "Cannot reset user password." - ) from exc + return _auth_success_response( + username=user.username, user_id=user_id, email=user.email + ) @auth_router.put("/update") @@ -312,80 +292,92 @@ async def update( data: UpdateUser, user: User = Depends(check_user_exists) ) -> Optional[User]: if data.user_id != user.id: - raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid user ID.") - if data.username and not is_valid_username(data.username): - raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid username.") - if data.email != user.email: - raise HTTPException(HTTP_400_BAD_REQUEST, "Email mismatch.") - - try: - return await update_account(user.id, data.username, None, data.config) - except AssertionError as exc: - raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc - except Exception as exc: - logger.debug(exc) raise HTTPException( - HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user." - ) from exc + status_code=HTTPStatus.BAD_REQUEST, detail="Invalid user ID." + ) + if data.username and not is_valid_username(data.username): + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Invalid username." + ) + if data.email != user.email: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Email mismatch.", + ) + account = await get_account(user.id) + if not account: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Account not found." + ) + if data.username and await get_account_by_username(data.username): + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Username already exists." + ) + if data.email and await get_account_by_email(data.email): + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Email already exists." + ) + + account = await get_account(user.id) + if not account: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Account not found." + ) + + if data.username: + account.username = data.username + if data.email: + account.email = data.email + if data.extra: + account.extra = data.extra + + await update_account(account) + return await get_user(account) @auth_router.put("/first_install") async def first_install(data: UpdateSuperuserPassword) -> JSONResponse: if not settings.first_install: - raise HTTPException(HTTP_401_UNAUTHORIZED, "This is not your first install") - try: - await update_account( - user_id=settings.super_user, - username=data.username, - user_config=UserConfig(provider="lnbits"), - ) - super_user = UpdateUserPassword( - user_id=settings.super_user, - password=data.password, - password_repeat=data.password_repeat, - username=data.username, - ) - user = await update_user_password(super_user, int(time())) - settings.first_install = False - return _auth_success_response(user.username, user.id, user.email) - except AssertionError as exc: - raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc - except Exception as exc: - logger.debug(exc) - raise HTTPException( - HTTP_500_INTERNAL_SERVER_ERROR, "Cannot init user password." - ) from exc + raise HTTPException(HTTPStatus.UNAUTHORIZED, "This is not your first install") + account = await get_account(settings.super_user) + if not account: + raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.") + account.username = data.username + account.extra = account.extra or UserExtra() + account.extra.provider = "lnbits" + account.hash_password(data.password) + await update_account(account) + settings.first_install = False + return _auth_success_response(username=account.username) async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] = None): email = userinfo.email if not email or not is_valid_email_address(email): - raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.") + raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.") redirect_path = "/wallet" - user_config = UserConfig(**dict(userinfo)) - user_config.email_verified = True - account = await get_account_by_email(email) if verified_user_id: if account: - raise HTTPException(HTTP_401_UNAUTHORIZED, "Email already used.") + raise HTTPException(HTTPStatus.UNAUTHORIZED, "Email already used.") account = await get_account(verified_user_id) if not account: - raise HTTPException(HTTP_401_UNAUTHORIZED, "Cannot verify user email.") + raise HTTPException(HTTPStatus.UNAUTHORIZED, "Cannot verify user email.") redirect_path = "/account" if account: - user = await update_account(account.id, email=email, user_config=user_config) + account.extra = account.extra or UserExtra() + account.extra.email_verified = True + await update_account(account) else: if not settings.new_accounts_allowed: - raise HTTPException(HTTP_400_BAD_REQUEST, "Account creation is disabled.") - user = await create_user_account(email=email, user_config=user_config) - - if not user: - raise HTTPException(HTTP_401_UNAUTHORIZED, "User not found.") - + raise HTTPException(HTTPStatus.BAD_REQUEST, "Account creation is disabled.") + account = Account( + id=urlsafe_short_hash(), email=email, extra=UserExtra(email_verified=True) + ) + await create_account(account) return _auth_redirect_response(redirect_path, email) diff --git a/lnbits/core/views/extension_api.py b/lnbits/core/views/extension_api.py index 71bd3215f..12399e482 100644 --- a/lnbits/core/views/extension_api.py +++ b/lnbits/core/views/extension_api.py @@ -1,7 +1,4 @@ from http import HTTPStatus -from typing import ( - List, -) from bolt11 import decode as bolt11_decode from fastapi import ( @@ -25,6 +22,7 @@ from lnbits.core.extensions.models import ( InstallableExtension, PayToEnableInfo, ReleasePaymentInfo, + UserExtension, UserExtensionInfo, ) from lnbits.core.models import ( @@ -38,6 +36,7 @@ from lnbits.decorators import ( ) from ..crud import ( + create_user_extension, delete_dbversion, drop_extension_db, get_dbversions, @@ -46,7 +45,6 @@ from ..crud import ( get_user_extension, update_extension_pay_to_enable, update_user_extension, - update_user_extension_extra, ) extension_router = APIRouter( @@ -176,18 +174,24 @@ async def api_enable_extension( assert ext, f"Extension '{ext_id}' is not installed." assert ext.active, f"Extension '{ext_id}' is not activated." + user_ext = await get_user_extension(user.id, ext_id) + if not user_ext: + user_ext = UserExtension(user=user.id, extension=ext_id, active=False) + await create_user_extension(user_ext) + if user.admin or not ext.requires_payment: - await update_user_extension(user_id=user.id, extension=ext_id, active=True) + user_ext.active = True + await update_user_extension(user_ext) return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.") - user_ext = await get_user_extension(user.id, ext_id) - if not (user_ext and user_ext.extra and user_ext.extra.payment_hash_to_enable): + if not (user_ext.extra and user_ext.extra.payment_hash_to_enable): raise HTTPException( HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment." ) if user_ext.is_paid: - await update_user_extension(user_id=user.id, extension=ext_id, active=True) + user_ext.active = True + await update_user_extension(user_ext) return SimpleStatus( success=True, message=f"Paid extension '{ext_id}' enabled." ) @@ -207,10 +211,9 @@ async def api_enable_extension( f"Invoice generated but not paid for enabeling extension '{ext_id}'.", ) + user_ext.active = True user_ext.extra.paid_to_enable = True - await update_user_extension_extra(user.id, ext_id, user_ext.extra) - - await update_user_extension(user_id=user.id, extension=ext_id, active=True) + await update_user_extension(user_ext) return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.") except AssertionError as exc: @@ -233,16 +236,15 @@ async def api_disable_extension( raise HTTPException( HTTPStatus.BAD_REQUEST, f"Extension '{ext_id}' doesn't exist." ) - try: - logger.info(f"Disabeling extension: {ext_id}.") - await update_user_extension(user_id=user.id, extension=ext_id, active=False) - return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.") - except Exception as exc: - logger.warning(exc) - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=(f"Failed to disable '{ext_id}'."), - ) from exc + user_ext = await get_user_extension(user.id, ext_id) + if not user_ext or not user_ext.active: + return SimpleStatus( + success=True, message=f"Extension '{ext_id}' already disabled." + ) + logger.info(f"Disabeling extension: {ext_id}.") + user_ext.active = False + await update_user_extension(user_ext) + return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.") @extension_router.put("/{ext_id}/activate", dependencies=[Depends(check_admin)]) @@ -319,9 +321,9 @@ async def api_uninstall_extension(ext_id: str) -> SimpleStatus: @extension_router.get("/{ext_id}/releases", dependencies=[Depends(check_admin)]) -async def get_extension_releases(ext_id: str) -> List[ExtensionRelease]: +async def get_extension_releases(ext_id: str) -> list[ExtensionRelease]: try: - extension_releases: List[ExtensionRelease] = ( + extension_releases: list[ExtensionRelease] = ( await InstallableExtension.get_extension_releases(ext_id) ) @@ -386,45 +388,59 @@ async def get_pay_to_install_invoice( async def get_pay_to_enable_invoice( ext_id: str, data: PayToEnableInfo, user: User = Depends(check_user_exists) ): - try: - assert data.amount and data.amount > 0, "A non-zero amount must be specified." - - ext = await get_installed_extension(ext_id) - assert ext, f"Extension '{ext_id}' not found." - assert ext.pay_to_enable, f"Payment Info not found for extension '{ext_id}'." - assert ( - ext.pay_to_enable.required - ), f"Payment not required for extension '{ext_id}'." - assert ext.pay_to_enable.wallet and ext.pay_to_enable.amount, ( - f"Payment wallet or amount missing for extension '{ext_id}'." - "Please contact the administrator." - ) - assert ( - data.amount >= ext.pay_to_enable.amount - ), f"Minimum amount is {ext.pay_to_enable.amount} sats." - - payment_hash, payment_request = await create_invoice( - wallet_id=ext.pay_to_enable.wallet, - amount=data.amount, - memo=f"Enable '{ext.name}' extension.", - ) - - user_ext = await get_user_extension(user.id, ext_id) - user_ext_info = ( - user_ext.extra if user_ext and user_ext.extra else UserExtensionInfo() - ) - user_ext_info.payment_hash_to_enable = payment_hash - await update_user_extension_extra(user.id, ext_id, user_ext_info) - - return {"payment_hash": payment_hash, "payment_request": payment_request} - - except AssertionError as exc: - raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc - except Exception as exc: - logger.warning(exc) + if not data.amount or data.amount <= 0: raise HTTPException( - HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot request invoice." - ) from exc + status_code=HTTPStatus.BAD_REQUEST, detail="Amount must be greater than 0." + ) + + ext = await get_installed_extension(ext_id) + if not ext: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail=f"Extension '{ext_id}' not found." + ) + + if not ext.pay_to_enable: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"Payment info not found for extension '{ext_id}'.", + ) + + if not ext.pay_to_enable.required: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"Payment not required for extension '{ext_id}'.", + ) + + if not ext.pay_to_enable.wallet or not ext.pay_to_enable.amount: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"Payment wallet or amount missing for extension '{ext_id}'.", + ) + + if data.amount < ext.pay_to_enable.amount: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=( + f"Amount {data.amount} sats is less than required " + f"{ext.pay_to_enable.amount} sats." + ), + ) + + payment_hash, payment_request = await create_invoice( + wallet_id=ext.pay_to_enable.wallet, + amount=data.amount, + memo=f"Enable '{ext.name}' extension.", + ) + + user_ext = await get_user_extension(user.id, ext_id) + if not user_ext: + user_ext = UserExtension(user=user.id, extension=ext_id, active=False) + await create_user_extension(user_ext) + user_ext_info = user_ext.extra if user_ext.extra else UserExtensionInfo() + user_ext_info.payment_hash_to_enable = payment_hash + user_ext.extra = user_ext_info + await update_user_extension(user_ext) + return {"payment_hash": payment_hash, "payment_request": payment_request} @extension_router.get( diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 4b725151c..4a70962e2 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -25,9 +25,11 @@ from ...utils.exchange_rates import allowed_currencies, currencies from ..crud import ( create_account, create_wallet, + get_account, get_dbversions, get_installed_extensions, get_user, + get_wallet, ) generic_router = APIRouter( @@ -136,7 +138,8 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)): ] # refresh user state. Eg: enabled extensions. - user = await get_user(user.id) or user + # TODO: refactor + # user = await get_user(user.id) or user return template_renderer().TemplateResponse( request, @@ -165,18 +168,16 @@ async def wallet( wal: Optional[UUID4] = Query(None), ): if wal: - wallet_id = wal.hex + wallet = await get_wallet(wal.hex) elif len(user.wallets) == 0: wallet = await create_wallet(user_id=user.id) - user = await get_user(user_id=user.id) or user - wallet_id = wallet.id + user.wallets.append(wallet) elif lnbits_last_active_wallet and user.get_wallet(lnbits_last_active_wallet): - wallet_id = lnbits_last_active_wallet + wallet = await get_wallet(lnbits_last_active_wallet) else: - wallet_id = user.wallets[0].id + wallet = user.wallets[0] - user_wallet = user.get_wallet(wallet_id) - if not user_wallet or user_wallet.deleted: + if not wallet or wallet.deleted: return template_renderer().TemplateResponse( request, "error.html", {"err": "Wallet not found"}, HTTPStatus.NOT_FOUND ) @@ -186,14 +187,14 @@ async def wallet( "core/wallet.html", { "user": user.dict(), - "wallet": user_wallet.dict(), + "wallet": wallet.dict(), "currencies": allowed_currencies(), "service_fee": settings.lnbits_service_fee, "service_fee_max": settings.lnbits_service_fee_max, "web_manifest": f"/manifest/{user.id}.webmanifest", }, ) - resp.set_cookie("lnbits_last_active_wallet", wallet_id) + resp.set_cookie("lnbits_last_active_wallet", wallet.id) return resp @@ -228,11 +229,10 @@ async def service_worker(request: Request): @generic_router.get("/manifest/{usr}.webmanifest") async def manifest(request: Request, usr: str): host = urlparse(str(request.url)).netloc - - user = await get_user(usr) - if not user: + account = await get_account(usr) + if not account: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - + user = await get_user(account) return { "short_name": settings.lnbits_site_title, "name": settings.lnbits_site_title + " Wallet", diff --git a/lnbits/core/views/user_api.py b/lnbits/core/views/user_api.py index 9318ed76a..4f51753b3 100644 --- a/lnbits/core/views/user_api.py +++ b/lnbits/core/views/user_api.py @@ -41,17 +41,7 @@ users_router = APIRouter(prefix="/users/api/v1", dependencies=[Depends(check_adm async def api_get_users( filters: Filters = Depends(parse_filters(AccountFilters)), ) -> Page[Account]: - try: - filtered = await get_accounts(filters=filters) - for user in filtered.data: - user.is_super_user = user.id == settings.super_user - user.is_admin = user.id in settings.lnbits_admin_users or user.is_super_user - return filtered - except Exception as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Could not fetch users. {exc!s}", - ) from exc + return await get_accounts(filters=filters) @users_router.delete("/user/{user_id}", status_code=HTTPStatus.OK) diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 6e9e32911..4fe4377c6 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -65,7 +65,7 @@ class KeyChecker(SecurityBase): name="X-API-KEY", description="Wallet API Key - HEADER", ) - self.model: APIKey = openapi_model + self.model: APIKey = openapi_model # type: ignore async def __call__(self, request: Request) -> WalletTypeInfo: @@ -147,11 +147,8 @@ async def check_user_exists( if not account or not settings.is_user_allowed(account.id): raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not allowed.") - user = await get_user(account.id) - assert user, "User not found for account." - + user = await get_user(account) await _check_user_extension_access(user.id, r["path"]) - return user diff --git a/tests/conftest.py b/tests/conftest.py index e2ca6fad8..67e9d2a60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,7 @@ from lnbits.core.crud import ( create_account, create_wallet, get_account_by_username, + get_account, get_user, update_payment_status, ) @@ -148,7 +149,9 @@ def from_super_user(from_user): @pytest_asyncio.fixture(scope="session") async def superuser(): - user = await get_user(settings.super_user) + account = await get_account(settings.super_user) + assert account, "Superuser not found" + user = await get_user(account) yield user