diff --git a/lnbits/core/templates/core/account.html b/lnbits/core/templates/core/account.html index c7ce79b11..6f81bab85 100644 --- a/lnbits/core/templates/core/account.html +++ b/lnbits/core/templates/core/account.html @@ -38,9 +38,9 @@
@@ -133,9 +133,9 @@
@@ -236,9 +236,9 @@
- + JSONResponse: raise HTTPException( HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed." ) - - try: - event = _nostr_nip98_event(request) - 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(account.username or "", account.id, account.email) - except HTTPException as exc: - raise exc - except AssertionError as exc: - raise HTTPException(HTTPStatus.UNAUTHORIZED, str(exc)) from exc - except Exception as exc: - logger.warning(exc) - raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot login.") from exc + event = _nostr_nip98_event(request) + 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(account.username or "", account.id, account.email) @auth_router.post("/usr", description="Login via the User ID") @@ -139,23 +129,15 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons detail=f"Login by '{provider}' not allowed.", ) - try: - with provider_sso: - userinfo = await provider_sso.verify_and_process(request) - assert userinfo is not None - user_id = decrypt_internal_message(provider_sso.state) - request.session.pop("user", None) - return await _handle_sso_login(userinfo, user_id) - except HTTPException as exc: - raise exc - except ValueError as exc: - raise HTTPException(HTTPStatus.FORBIDDEN, str(exc)) from exc - except Exception as exc: - logger.debug(exc) - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Cannot authenticate user with {provider} Auth.", - ) from exc + with provider_sso: + userinfo = await provider_sso.verify_and_process(request) + if not userinfo: + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid user info." + ) + user_id = decrypt_internal_message(provider_sso.state) + request.session.pop("user", None) + return await _handle_sso_login(userinfo, user_id) @auth_router.post("/logout") @@ -191,6 +173,11 @@ async def register(data: CreateUser) -> JSONResponse: status_code=HTTPStatus.BAD_REQUEST, detail="Invalid username." ) + if await get_account_by_username(data.username): + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Username already exists." + ) + if data.email and not is_valid_email_address(data.email): raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid email.") @@ -212,17 +199,19 @@ async def update_pubkey( ) -> Optional[User]: if data.user_id != user.id: raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.") - + if ( + data.pubkey + and data.pubkey != user.pubkey + and await get_account_by_pubkey(data.pubkey) + ): + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Public key already in use." + ) 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) @@ -239,7 +228,11 @@ async def update_password( raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail="Invalid user ID." ) - if data.username and await get_account_by_username(data.username): + if ( + data.username + and user.username != data.username + and await get_account_by_username(data.username) + ): raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail="Username already exists." ) @@ -324,16 +317,19 @@ async def update( 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): + if ( + data.username + and user.username != 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): + if ( + data.email + and data.email != user.email + and await get_account_by_email(data.email) + ): raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail="Email already exists." ) @@ -473,37 +469,43 @@ def _find_auth_provider_class(provider: str) -> Callable: def _nostr_nip98_event(request: Request) -> dict: auth_header = request.headers.get("Authorization") - assert auth_header, "Nostr Auth header missing." - + if not auth_header: + raise HTTPException(HTTPStatus.UNAUTHORIZED, "Nostr Auth header missing.") scheme, token = auth_header.split() - assert scheme.lower() == "nostr", "Authorization header is not nostr." - + if scheme.lower() != "nostr": + raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid Authorization scheme.") event = None try: event_json = base64.b64decode(token.encode("ascii")) event = json.loads(event_json) except Exception as exc: logger.warning(exc) - - assert event, "Nostr login event cannot be parsed." - - assert verify_event(event), "Nostr login event is not valid." - - assert event["kind"] == 27_235, "Invalid event kind." + if not event: + raise HTTPException( + HTTPStatus.BAD_REQUEST, "Nostr login event cannot be parsed." + ) + if not verify_event(event): + raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr login event is not valid.") + if event["kind"] != 27_235: + raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid event kind.") auth_threshold = settings.auth_credetials_update_threshold - assert ( - abs(time() - event["created_at"]) < auth_threshold - ), f"More than {auth_threshold} seconds have passed since the event was signed." - + if abs(time() - event["created_at"]) > auth_threshold: + raise HTTPException( + HTTPStatus.BAD_REQUEST, + f"{auth_threshold} seconds have passed since the event was signed.", + ) method: Optional[str] = next((v for k, v in event["tags"] if k == "method"), None) - assert method, "Tag 'method' is missing." - assert method.upper() == "POST", "Incorrect value for tag 'method'." + if not method: + raise HTTPException(HTTPStatus.BAD_REQUEST, "Tag 'method' is missing.") + if method.upper() != "POST": + raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid value for tag 'method'.") url = next((v for k, v in event["tags"] if k == "u"), None) - assert url, "Tag 'u' for URL is missing." + if not url: + raise HTTPException(HTTPStatus.BAD_REQUEST, "Tag 'u' for URL is missing.") accepted_urls = [f"{u}/nostr" for u in settings.nostr_absolute_request_urls] - assert url in accepted_urls, f"Incorrect value for tag 'u': '{url}'." - + if url not in accepted_urls: + raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid value for tag 'u'.") return event diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 220273534..462a86a36 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -1,6 +1,6 @@ import json import re -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import Any, Optional, Type @@ -184,7 +184,7 @@ def is_valid_username(username: str) -> bool: def create_access_token(data: dict): - expire = datetime.utcnow() + timedelta(minutes=settings.auth_token_expire_minutes) + expire = datetime.now(UTC) + timedelta(minutes=settings.auth_token_expire_minutes) to_encode = data.copy() to_encode.update({"exp": expire}) return jwt.encode(to_encode, settings.auth_secret_key, "HS256") diff --git a/lnbits/static/js/account.js b/lnbits/static/js/account.js index 82df5a745..d353ad44d 100644 --- a/lnbits/static/js/account.js +++ b/lnbits/static/js/account.js @@ -92,7 +92,7 @@ window.app = Vue.createApp({ user_id: this.user.id, username: this.user.username, email: this.user.email, - config: this.user.config + extra: this.user.extra } ) this.user = data @@ -183,7 +183,7 @@ window.app = Vue.createApp({ const {data} = await LNbits.api.getAuthenticatedUser() this.user = data this.hasUsername = !!data.username - if (!this.user.config) this.user.config = {} + if (!this.user.extra) this.user.extra = {} } catch (e) { LNbits.utils.notifyApiError(e) } diff --git a/tests/conftest.py b/tests/conftest.py index a2adc792e..264bef7d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,16 @@ # ruff: noqa: E402 import asyncio -from time import time import uvloop -from asgi_lifespan import LifespanManager -uvloop.install() +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) +from time import time from uuid import uuid4 import pytest import pytest_asyncio +from asgi_lifespan import LifespanManager from fastapi.testclient import TestClient from httpx import ASGITransport, AsyncClient @@ -110,7 +110,6 @@ async def user_alan(): username="alan", ) account.hash_password("secret1234") - await create_account(account) yield account