From be72e359141925ecc98f41bfb4df4967ef8c06b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 30 Sep 2024 17:47:50 +0200 Subject: [PATCH] fixup! --- lnbits/core/crud.py | 65 ++------------------------------ lnbits/core/models.py | 1 + lnbits/core/views/auth_api.py | 70 +++++++++++++++++++++-------------- tests/conftest.py | 22 +++++++---- 4 files changed, 61 insertions(+), 97 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 9cc203a8d..bb80bf005 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -29,8 +29,6 @@ from .models import ( PaymentFilters, PaymentHistoryPoint, TinyURL, - UpdateUserPassword, - UpdateUserPubkey, User, Wallet, WebPushSubscription, @@ -48,9 +46,10 @@ async def create_account( return account -async def update_account(account: Account) -> None: +async def update_account(account: Account) -> Account: account.updated_at = datetime.now() await db.update("accounts", account) + return account async def delete_account(user_id: str, conn: Optional[Connection] = None) -> None: @@ -122,65 +121,6 @@ async def delete_accounts_no_wallets( ) -async def update_user_password(data: UpdateUserPassword, last_login_time: int) -> User: - - assert 0 <= time() - last_login_time <= settings.auth_credetials_update_threshold, ( - "You can only update your credentials in the first" - f" {settings.auth_credetials_update_threshold} seconds." - " Please login again or ask a new reset key!" - ) - assert data.password == data.password_repeat, "Passwords do not match." - - pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - - await db.execute( - f""" - UPDATE accounts - SET pass = :pass, updated_at = {db.timestamp_placeholder("now")} - WHERE id = :user - """, - { - "pass": pwd_context.hash(data.password), - "now": int(time()), - "user": data.user_id, - }, - ) - - user = await get_user(data.user_id) - assert user, "Updated account couldn't be retrieved." - return user - - -async def update_user_pubkey(data: UpdateUserPubkey, last_login_time: int) -> User: - - assert 0 <= time() - last_login_time <= settings.auth_credetials_update_threshold, ( - "You can only update your credentials in the first" - f" {settings.auth_credetials_update_threshold} seconds after login." - " Please login again!" - ) - - user = await get_account_by_pubkey(data.pubkey) - if user: - assert user.id == data.user_id, "Public key already in use." - - await db.execute( - f""" - UPDATE accounts - SET pubkey = :pubkey, updated_at = {db.timestamp_placeholder("now")} - WHERE id = :user - """, - { - "pubkey": data.pubkey, - "now": int(time()), - "user": data.user_id, - }, - ) - - user = await get_user(data.user_id) - assert user, "Updated account couldn't be retrieved" - return user - - async def get_account_by_username( username: str, conn: Optional[Connection] = None ) -> Optional[Account]: @@ -200,6 +140,7 @@ async def get_account_by_pubkey( Account, ) + async def get_account_by_email( email: str, conn: Optional[Connection] = None ) -> Optional[Account]: diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 81a6c3ce9..81f9950e0 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -112,6 +112,7 @@ class Account(BaseModel): id: str username: Optional[str] = None password_hash: Optional[str] = None + pubkey: Optional[str] = None email: Optional[str] = None extra: UserExtra = UserExtra() created_at: datetime = datetime.now() diff --git a/lnbits/core/views/auth_api.py b/lnbits/core/views/auth_api.py index 807017426..796ec5f49 100644 --- a/lnbits/core/views/auth_api.py +++ b/lnbits/core/views/auth_api.py @@ -1,16 +1,16 @@ import base64 import importlib import json -from time import time from http import HTTPStatus +from time import time from typing import Callable, Optional +from uuid import uuid4 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 lnbits.core.services import create_user_account from lnbits.decorators import access_token_payload, check_user_exists from lnbits.helpers import ( create_access_token, @@ -31,13 +31,8 @@ from ..crud import ( get_account_by_username, get_account_by_username_or_email, get_user, - get_user_password, update_account, - update_user_password, - update_user_pubkey, - verify_user_password, ) - from ..models import ( AccessTokenPayload, Account, @@ -78,25 +73,29 @@ async def login(data: LoginUsernamePassword) -> JSONResponse: @auth_router.post("/nostr", description="Login via Nostr") async def nostr_login(request: Request) -> JSONResponse: if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98): - raise HTTPException(HTTP_401_UNAUTHORIZED, "Login with Nostr Auth not allowed.") + raise HTTPException( + HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed." + ) try: event = _nostr_nip98_event(request) - - user = await get_account_by_pubkey(event["pubkey"]) - if not user: - user = await create_user_account( - pubkey=event["pubkey"], user_config=UserConfig(provider="nostr") + account = await get_account_by_pubkey(event["pubkey"]) + if not account: + account = Account( + id=uuid4().hex, + pubkey=event["pubkey"], + extra=UserExtra(provider="nostr"), ) + await create_account(account) - return _auth_success_response(user.username or "", user.id, user.email) + return _auth_success_response(account.username or "", account.id, account.email) except HTTPException as exc: raise exc except AssertionError as exc: - raise HTTPException(HTTP_401_UNAUTHORIZED, str(exc)) from exc + raise HTTPException(HTTPStatus.UNAUTHORIZED, str(exc)) from exc except Exception as exc: logger.warning(exc) - raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.") from exc + raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot login.") from exc @auth_router.post("/usr", description="Login via the User ID") @@ -205,7 +204,6 @@ async def register(data: CreateUser) -> JSONResponse: return _auth_success_response(account.username) - @auth_router.put("/pubkey") async def update_pubkey( data: UpdateUserPubkey, @@ -213,16 +211,22 @@ async def update_pubkey( payload: AccessTokenPayload = Depends(access_token_payload), ) -> Optional[User]: if data.user_id != user.id: - raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid user ID.") + raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.") - try: - data.pubkey = normalize_public_key(data.pubkey) - return await update_user_pubkey(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) + account = await get_account(user.id) + if not account: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Account not found." + ) + account_existing = await get_account_by_pubkey(data.pubkey) + if account_existing and account_existing.id != account.id: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Public key already in use." + ) + _validate_auth_timeout(payload.auth_time) + account.pubkey = normalize_public_key(data.pubkey) + await update_account(account) + return await get_user(account) @auth_router.put("/password") @@ -257,6 +261,8 @@ async def update_password( status_code=HTTPStatus.BAD_REQUEST, detail="Invalid credentials." ) + _validate_auth_timeout(payload.auth_time) + account.username = data.username account.hash_password(data.password) await update_account(account) @@ -389,7 +395,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] = if not settings.new_accounts_allowed: raise HTTPException(HTTPStatus.BAD_REQUEST, "Account creation is disabled.") account = Account( - id=urlsafe_short_hash(), email=email, extra=UserExtra(email_verified=True) + id=uuid4().hex, email=email, extra=UserExtra(email_verified=True) ) await create_account(account) return _auth_redirect_response(redirect_path, email) @@ -499,3 +505,13 @@ def _nostr_nip98_event(request: Request) -> dict: assert url in accepted_urls, f"Incorrect value for tag 'u': '{url}'." return event + + +def _validate_auth_timeout(auth_time: Optional[int] = None): + if int(time()) - int(auth_time or 0) > settings.auth_credetials_update_threshold: + raise HTTPException( + HTTPStatus.BAD_REQUEST, + "You can only update your credentials in the first" + f" {settings.auth_credetials_update_threshold} seconds after login." + " Please login again!", + ) diff --git a/tests/conftest.py b/tests/conftest.py index 6359c679c..ef830b4e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,8 @@ from lnbits.wallets.fake import FakeWallet uvloop.install() +from uuid import uuid4 + import pytest import pytest_asyncio from fastapi.testclient import TestClient @@ -18,13 +20,13 @@ from lnbits.app import create_app from lnbits.core.crud import ( create_account, create_wallet, - get_account_by_username, get_account, + get_account_by_username, get_user, update_payment_status, ) -from lnbits.core.models import CreateInvoice, PaymentState -from lnbits.core.services import create_user_account, update_wallet_balance +from lnbits.core.models import Account, CreateInvoice, PaymentState +from lnbits.core.services import update_wallet_balance from lnbits.core.views.payment_api import api_payments_create_invoice from lnbits.db import DB_TYPE, SQLITE, Database from lnbits.settings import AuthMethods, settings @@ -102,12 +104,16 @@ async def db(): @pytest_asyncio.fixture(scope="package") async def user_alan(): - user = await get_account_by_username("alan") - if not user: - user = await create_user_account( - email="alan@lnbits.com", username="alan", password="secret1234" + account = await get_account_by_username("alan") + if not account: + account = Account( + id=uuid4().hex, + email="alan@lnbits.com", + username="alan", ) - yield user + account.hash_password("secret1234") + await create_account(account) + yield account @pytest_asyncio.fixture(scope="session")