fixup!
This commit is contained in:
+3
-62
@@ -29,8 +29,6 @@ from .models import (
|
|||||||
PaymentFilters,
|
PaymentFilters,
|
||||||
PaymentHistoryPoint,
|
PaymentHistoryPoint,
|
||||||
TinyURL,
|
TinyURL,
|
||||||
UpdateUserPassword,
|
|
||||||
UpdateUserPubkey,
|
|
||||||
User,
|
User,
|
||||||
Wallet,
|
Wallet,
|
||||||
WebPushSubscription,
|
WebPushSubscription,
|
||||||
@@ -48,9 +46,10 @@ async def create_account(
|
|||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def update_account(account: Account) -> None:
|
async def update_account(account: Account) -> Account:
|
||||||
account.updated_at = datetime.now()
|
account.updated_at = datetime.now()
|
||||||
await db.update("accounts", account)
|
await db.update("accounts", account)
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def delete_account(user_id: str, conn: Optional[Connection] = None) -> None:
|
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(
|
async def get_account_by_username(
|
||||||
username: str, conn: Optional[Connection] = None
|
username: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[Account]:
|
) -> Optional[Account]:
|
||||||
@@ -200,6 +140,7 @@ async def get_account_by_pubkey(
|
|||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_email(
|
async def get_account_by_email(
|
||||||
email: str, conn: Optional[Connection] = None
|
email: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[Account]:
|
) -> Optional[Account]:
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ class Account(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
username: Optional[str] = None
|
username: Optional[str] = None
|
||||||
password_hash: Optional[str] = None
|
password_hash: Optional[str] = None
|
||||||
|
pubkey: Optional[str] = None
|
||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
created_at: datetime = datetime.now()
|
created_at: datetime = datetime.now()
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import base64
|
import base64
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
from time import time
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from time import time
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from fastapi.responses import JSONResponse, RedirectResponse
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
from fastapi_sso.sso.base import OpenID, SSOBase
|
from fastapi_sso.sso.base import OpenID, SSOBase
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.services import create_user_account
|
|
||||||
from lnbits.decorators import access_token_payload, check_user_exists
|
from lnbits.decorators import access_token_payload, check_user_exists
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
@@ -31,13 +31,8 @@ from ..crud import (
|
|||||||
get_account_by_username,
|
get_account_by_username,
|
||||||
get_account_by_username_or_email,
|
get_account_by_username_or_email,
|
||||||
get_user,
|
get_user,
|
||||||
get_user_password,
|
|
||||||
update_account,
|
update_account,
|
||||||
update_user_password,
|
|
||||||
update_user_pubkey,
|
|
||||||
verify_user_password,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
AccessTokenPayload,
|
AccessTokenPayload,
|
||||||
Account,
|
Account,
|
||||||
@@ -78,25 +73,29 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
|||||||
@auth_router.post("/nostr", description="Login via Nostr")
|
@auth_router.post("/nostr", description="Login via Nostr")
|
||||||
async def nostr_login(request: Request) -> JSONResponse:
|
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(HTTP_401_UNAUTHORIZED, "Login with Nostr Auth not allowed.")
|
raise HTTPException(
|
||||||
|
HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed."
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
event = _nostr_nip98_event(request)
|
event = _nostr_nip98_event(request)
|
||||||
|
account = await get_account_by_pubkey(event["pubkey"])
|
||||||
user = await get_account_by_pubkey(event["pubkey"])
|
if not account:
|
||||||
if not user:
|
account = Account(
|
||||||
user = await create_user_account(
|
id=uuid4().hex,
|
||||||
pubkey=event["pubkey"], user_config=UserConfig(provider="nostr")
|
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:
|
except HTTPException as exc:
|
||||||
raise exc
|
raise exc
|
||||||
except AssertionError as 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:
|
except Exception as exc:
|
||||||
logger.warning(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")
|
@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)
|
return _auth_success_response(account.username)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/pubkey")
|
@auth_router.put("/pubkey")
|
||||||
async def update_pubkey(
|
async def update_pubkey(
|
||||||
data: UpdateUserPubkey,
|
data: UpdateUserPubkey,
|
||||||
@@ -213,16 +211,22 @@ async def update_pubkey(
|
|||||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||||
) -> Optional[User]:
|
) -> Optional[User]:
|
||||||
if data.user_id != user.id:
|
if data.user_id != user.id:
|
||||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid user ID.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
||||||
|
|
||||||
try:
|
account = await get_account(user.id)
|
||||||
data.pubkey = normalize_public_key(data.pubkey)
|
if not account:
|
||||||
return await update_user_pubkey(data, payload.auth_time or 0)
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Account not found."
|
||||||
except AssertionError as exc:
|
)
|
||||||
raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc
|
account_existing = await get_account_by_pubkey(data.pubkey)
|
||||||
except Exception as exc:
|
if account_existing and account_existing.id != account.id:
|
||||||
logger.debug(exc)
|
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")
|
@auth_router.put("/password")
|
||||||
@@ -257,6 +261,8 @@ async def update_password(
|
|||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Invalid credentials."
|
status_code=HTTPStatus.BAD_REQUEST, detail="Invalid credentials."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_validate_auth_timeout(payload.auth_time)
|
||||||
|
|
||||||
account.username = data.username
|
account.username = data.username
|
||||||
account.hash_password(data.password)
|
account.hash_password(data.password)
|
||||||
await update_account(account)
|
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:
|
if not settings.new_accounts_allowed:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Account creation is disabled.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Account creation is disabled.")
|
||||||
account = Account(
|
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)
|
await create_account(account)
|
||||||
return _auth_redirect_response(redirect_path, email)
|
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}'."
|
assert url in accepted_urls, f"Incorrect value for tag 'u': '{url}'."
|
||||||
|
|
||||||
return event
|
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!",
|
||||||
|
)
|
||||||
|
|||||||
+14
-8
@@ -9,6 +9,8 @@ from lnbits.wallets.fake import FakeWallet
|
|||||||
|
|
||||||
uvloop.install()
|
uvloop.install()
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
@@ -18,13 +20,13 @@ from lnbits.app import create_app
|
|||||||
from lnbits.core.crud import (
|
from lnbits.core.crud import (
|
||||||
create_account,
|
create_account,
|
||||||
create_wallet,
|
create_wallet,
|
||||||
get_account_by_username,
|
|
||||||
get_account,
|
get_account,
|
||||||
|
get_account_by_username,
|
||||||
get_user,
|
get_user,
|
||||||
update_payment_status,
|
update_payment_status,
|
||||||
)
|
)
|
||||||
from lnbits.core.models import CreateInvoice, PaymentState
|
from lnbits.core.models import Account, CreateInvoice, PaymentState
|
||||||
from lnbits.core.services import create_user_account, update_wallet_balance
|
from lnbits.core.services import update_wallet_balance
|
||||||
from lnbits.core.views.payment_api import api_payments_create_invoice
|
from lnbits.core.views.payment_api import api_payments_create_invoice
|
||||||
from lnbits.db import DB_TYPE, SQLITE, Database
|
from lnbits.db import DB_TYPE, SQLITE, Database
|
||||||
from lnbits.settings import AuthMethods, settings
|
from lnbits.settings import AuthMethods, settings
|
||||||
@@ -102,12 +104,16 @@ async def db():
|
|||||||
|
|
||||||
@pytest_asyncio.fixture(scope="package")
|
@pytest_asyncio.fixture(scope="package")
|
||||||
async def user_alan():
|
async def user_alan():
|
||||||
user = await get_account_by_username("alan")
|
account = await get_account_by_username("alan")
|
||||||
if not user:
|
if not account:
|
||||||
user = await create_user_account(
|
account = Account(
|
||||||
email="alan@lnbits.com", username="alan", password="secret1234"
|
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")
|
@pytest_asyncio.fixture(scope="session")
|
||||||
|
|||||||
Reference in New Issue
Block a user