big fx part2

This commit is contained in:
dni ⚡
2024-10-10 09:07:45 +02:00
parent 383f6ec721
commit 200f5c52a7
12 changed files with 395 additions and 537 deletions
+69 -198
View File
@@ -1,10 +1,10 @@
import json import json
from datetime import datetime
from time import time from time import time
from typing import Literal, Optional from typing import Literal, Optional
from uuid import uuid4 from uuid import uuid4
import shortuuid import shortuuid
from passlib.context import CryptContext
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.extensions.models import ( from lnbits.core.extensions.models import (
@@ -32,92 +32,25 @@ from .models import (
UpdateUserPassword, UpdateUserPassword,
UpdateUserPubkey, UpdateUserPubkey,
User, User,
UserConfig,
Wallet, Wallet,
WebPushSubscription, WebPushSubscription,
) )
async def create_account( async def create_account(
user_id: Optional[str] = None, account: Optional[Account] = None,
username: Optional[str] = None,
pubkey: Optional[str] = None,
email: Optional[str] = None,
password: Optional[str] = None,
user_config: Optional[UserConfig] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> User: ) -> Account:
user_id = user_id or uuid4().hex if not account:
extra = json.dumps(dict(user_config)) if user_config else "{}" now = datetime.now()
now = int(time()) account = Account(id=uuid4().hex, created_at=now, updated_at=now)
now_ph = db.timestamp_placeholder("now") await (conn or db).insert("accounts", account)
await (conn or db).execute( return account
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
async def update_account( async def update_account(account: Account) -> None:
user_id: str, account.updated_at = datetime.now()
username: Optional[str] = None, await db.update("accounts", account)
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 delete_account(user_id: str, conn: Optional[Connection] = None) -> None: async def delete_account(user_id: str, conn: Optional[Connection] = None) -> None:
@@ -162,16 +95,12 @@ async def get_accounts(
async def get_account( async def get_account(
user_id: str, conn: Optional[Connection] = None user_id: str, conn: Optional[Connection] = None
) -> Optional[User]: ) -> Optional[Account]:
user = await (conn or db).fetchone( return await (conn or db).fetchone(
""" "SELECT * FROM accounts WHERE id = :id",
SELECT id, email, username, pubkey, created_at, updated_at, extra
FROM accounts WHERE id = :id
""",
{"id": user_id}, {"id": user_id},
User, Account,
) )
return user
async def delete_accounts_no_wallets( 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: async def update_user_password(data: UpdateUserPassword, last_login_time: int) -> User:
assert 0 <= time() - last_login_time <= settings.auth_credetials_update_threshold, ( 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( async def get_account_by_username(
username: str, conn: Optional[Connection] = None username: str, conn: Optional[Connection] = None
) -> Optional[User]: ) -> Optional[Account]:
row = await (conn or db).fetchone( return await (conn or db).fetchone(
""" "SELECT * FROM accounts WHERE username = :username",
SELECT id, username, pubkey, email, created_at, updated_at
FROM accounts WHERE username = :username
""",
{"username": username}, {"username": username},
Account,
) )
return User(**row) if row else None
async def get_account_by_pubkey( async def get_account_by_pubkey(
pubkey: str, conn: Optional[Connection] = None pubkey: str, conn: Optional[Connection] = None
) -> Optional[User]: ) -> Optional[Account]:
row = await (conn or db).fetchone( return await (conn or db).fetchone(
""" "SELECT * FROM accounts WHERE pubkey = :pubkey",
SELECT id, username, pubkey, email, created_at, updated_at
FROM accounts WHERE pubkey = :pubkey
""",
{"pubkey": pubkey}, {"pubkey": pubkey},
Account,
) )
return User(**row) if row else None
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[User]: ) -> Optional[Account]:
row = await (conn or db).fetchone( return await (conn or db).fetchone(
""" "SELECT * FROM accounts WHERE email = :email",
SELECT id, username, pubkey, email, created_at, updated_at
FROM accounts WHERE email = :email
""",
{"email": email}, {"email": email},
Account,
) )
return User(**row) if row else None
async def get_account_by_username_or_email( async def get_account_by_username_or_email(
username_or_email: str, conn: Optional[Connection] = None username_or_email: str, conn: Optional[Connection] = None
) -> Optional[User]: ) -> Optional[Account]:
user = await get_account_by_username(username_or_email, conn) return await (conn or db).fetchone(
if not user: "SELECT * FROM accounts WHERE email = :value or username = :value",
user = await get_account_by_email(username_or_email, conn) {"value": username_or_email},
return user Account,
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},
) )
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( return User(
id=user["id"], id=account.id,
email=user["email"], email=account.email,
username=user["username"], username=account.username,
pubkey=user["pubkey"], extra=account.extra,
extensions=[ created_at=account.created_at,
e for e in extensions if User.is_extension_for_user(e[0], user["id"]) updated_at=account.updated_at,
], extensions=extensions,
wallets=[Wallet(**w) for w in wallets], wallets=wallets,
admin=user["id"] == settings.super_user admin=account.is_super_user or account.is_admin or False,
or user["id"] in settings.lnbits_admin_users, super_user=account.is_super_user or False,
super_user=user["id"] == settings.super_user, has_password=account.password_hash is not None,
has_password=True if user["pass"] else False,
config=UserConfig(**json.loads(user["extra"])) if user["extra"] else 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( db_version = await (conn or db).fetchone(
"SELECT * FROM dbversions WHERE db = :id", "SELECT * FROM dbversions WHERE db = :id",
{"id": ext_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( async def update_user_extension(
user_extension: UserExtension, conn: Optional[Connection] = None user_extension: UserExtension, conn: Optional[Connection] = None
) -> None: ) -> None:
where = """extension = :extension AND "user" = :user""" where = """extension = :extension AND "user" = :user"""
await (conn or db).update("extensions", user_extension, where) 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( async def get_user_active_extensions_ids(
@@ -623,7 +497,7 @@ async def force_delete_wallet(
async def delete_wallet_by_id( async def delete_wallet_by_id(
*, wallet_id: str, conn: Optional[Connection] = None wallet_id: str, conn: Optional[Connection] = None
) -> Optional[int]: ) -> Optional[int]:
now = int(time()) now = int(time())
result = await (conn or db).execute( 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]: 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) SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0)
AS balance_msat FROM wallets WHERE "user" = :user AS balance_msat FROM wallets WHERE "user" = :user
""", """,
{"user": user_id}, {"user": user_id},
Wallet,
) )
return [Wallet(**row) for row in rows]
async def get_wallet_for_key( async def get_wallet_for_key(
key: str, key: str,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[Wallet]: ) -> 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) SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0)
AS balance_msat FROM wallets AS balance_msat FROM wallets
WHERE (adminkey = :key OR inkey = :key) AND deleted = false WHERE (adminkey = :key OR inkey = :key) AND deleted = false
""", """,
{"key": key}, {"key": key},
Wallet,
) )
if not row:
return None
return Wallet(**row)
async def get_total_balance(conn: Optional[Connection] = None): 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) return row.get("balance", 0)
@@ -759,8 +629,10 @@ async def get_wallet_payment(
return payment return payment
async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: int = 5): async def get_latest_payments_by_extension(
rows = await db.fetchall( ext_name: str, ext_id: str, limit: int = 5
) -> list[Payment]:
return await db.fetchall(
f""" f"""
SELECT * FROM apipayments SELECT * FROM apipayments
WHERE status = '{PaymentState.SUCCESS}' 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} ORDER BY time DESC LIMIT {limit}
""", """,
{"ext_name": f"%{ext_name}%", "ext_id": f"%{ext_id}%"}, {"ext_name": f"%{ext_name}%", "ext_id": f"%{ext_id}%"},
Payment,
) )
return rows
async def get_payments_paginated( 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]: async def get_tinyurl(tinyurl_id: str) -> Optional[TinyURL]:
row = await db.fetchone( return await db.fetchone(
"SELECT * FROM tiny_url WHERE id = :tinyurl", "SELECT * FROM tiny_url WHERE id = :tinyurl",
{"tinyurl": tinyurl_id}, {"tinyurl": tinyurl_id},
TinyURL,
) )
return TinyURL.from_row(row) if row else None
async def get_tinyurl_by_url(url: str) -> list[TinyURL]: 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", "SELECT * FROM tiny_url WHERE url = :url",
{"url": url}, {"url": url},
TinyURL,
) )
return [TinyURL.from_row(row) for row in rows]
async def delete_tinyurl(tinyurl_id: str): async def delete_tinyurl(tinyurl_id: str):
+1
View File
@@ -120,6 +120,7 @@ class UserExtensionInfo(BaseModel):
class UserExtension(BaseModel): class UserExtension(BaseModel):
user: str
extension: str extension: str
active: bool active: bool
extra: Optional[UserExtensionInfo] = None extra: Optional[UserExtensionInfo] = None
+1
View File
@@ -562,3 +562,4 @@ async def m023_add_column_column_to_apipayments(db):
await db.execute("DROP INDEX by_hash") 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 hash TO payment_hash")
await db.execute("ALTER TABLE apipayments RENAME COLUMN wallet TO wallet_id") await db.execute("ALTER TABLE apipayments RENAME COLUMN wallet TO wallet_id")
await db.execute("ALTER TABLE accounts RENAME COLUMN pass TO password_hash")
+35 -11
View File
@@ -1,15 +1,16 @@
from __future__ import annotations from __future__ import annotations
import datetime
import hashlib import hashlib
import hmac import hmac
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from enum import Enum from enum import Enum
from typing import Callable, Optional from typing import Callable, Optional
from ecdsa import SECP256k1, SigningKey from ecdsa import SECP256k1, SigningKey
from fastapi import Query from fastapi import Query
from passlib.context import CryptContext
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from lnbits.db import FilterModel from lnbits.db import FilterModel
@@ -104,14 +105,37 @@ class UserExtra(BaseModel):
class Account(BaseModel): class Account(BaseModel):
id: str id: str
is_super_user: Optional[bool] = False
is_admin: Optional[bool] = False
username: Optional[str] = None username: Optional[str] = None
password_hash: Optional[str] = None
email: Optional[str] = None email: Optional[str] = None
balance_msat: Optional[int] = 0 balance_msat: Optional[int] = 0
transaction_count: Optional[int] = 0 transaction_count: Optional[int] = 0
wallet_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): class AccountFilters(FilterModel):
@@ -126,7 +150,7 @@ class AccountFilters(FilterModel):
] ]
id: str id: str
last_payment: Optional[datetime.datetime] = None last_payment: Optional[datetime] = None
transaction_count: Optional[int] = None transaction_count: Optional[int] = None
wallet_count: Optional[int] = None wallet_count: Optional[int] = None
username: Optional[str] = None username: Optional[str] = None
@@ -135,6 +159,8 @@ class AccountFilters(FilterModel):
class User(BaseModel): class User(BaseModel):
id: str id: str
created_at: datetime
updated_at: datetime
email: Optional[str] = None email: Optional[str] = None
username: Optional[str] = None username: Optional[str] = None
pubkey: Optional[str] = None pubkey: Optional[str] = None
@@ -144,8 +170,6 @@ class User(BaseModel):
super_user: bool = False super_user: bool = False
has_password: bool = False has_password: bool = False
extra: Optional[UserExtra] = None extra: Optional[UserExtra] = None
created_at: Optional[int] = None
updated_at: Optional[int] = None
@property @property
def wallet_ids(self) -> list[str]: def wallet_ids(self) -> list[str]:
@@ -237,7 +261,7 @@ class CreatePayment(BaseModel):
amount: int amount: int
memo: str memo: str
preimage: Optional[str] = None preimage: Optional[str] = None
expiry: Optional[datetime.datetime] = None expiry: Optional[datetime] = None
extra: Optional[dict] = None extra: Optional[dict] = None
webhook: Optional[str] = None webhook: Optional[str] = None
fee: int = 0 fee: int = 0
@@ -336,11 +360,11 @@ class PaymentFilters(FilterModel):
amount: int amount: int
fee: int fee: int
memo: Optional[str] memo: Optional[str]
time: datetime.datetime time: datetime
bolt11: str bolt11: str
preimage: str preimage: str
payment_hash: str payment_hash: str
expiry: Optional[datetime.datetime] expiry: Optional[datetime]
extra: dict = {} extra: dict = {}
wallet_id: str wallet_id: str
webhook: Optional[str] webhook: Optional[str]
@@ -348,7 +372,7 @@ class PaymentFilters(FilterModel):
class PaymentHistoryPoint(BaseModel): class PaymentHistoryPoint(BaseModel):
date: datetime.datetime date: datetime
income: int income: int
spending: int spending: int
balance: int balance: int
+17 -51
View File
@@ -1,11 +1,12 @@
import asyncio import asyncio
import json import json
import time import time
from datetime import datetime
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
from uuid import UUID, uuid4 from uuid import uuid4
import httpx import httpx
from bolt11 import MilliSatoshi from bolt11 import MilliSatoshi
@@ -13,7 +14,6 @@ from bolt11 import decode as bolt11_decode
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from fastapi import Depends, WebSocket from fastapi import Depends, WebSocket
from loguru import logger from loguru import logger
from passlib.context import CryptContext
from py_vapid import Vapid from py_vapid import Vapid
from py_vapid.utils import b64urlencode from py_vapid.utils import b64urlencode
@@ -52,8 +52,6 @@ from .crud import (
create_payment, create_payment,
create_wallet, create_wallet,
get_account, get_account,
get_account_by_email,
get_account_by_username,
get_payments, get_payments,
get_standalone_payment, get_standalone_payment,
get_super_settings, get_super_settings,
@@ -64,16 +62,15 @@ from .crud import (
update_payment_details, update_payment_details,
update_payment_status, update_payment_status,
update_super_user, update_super_user,
update_user_extension,
) )
from .helpers import to_valid_user_id from .helpers import to_valid_user_id
from .models import ( from .models import (
Account,
BalanceDelta, BalanceDelta,
CreatePayment, CreatePayment,
Payment, Payment,
PaymentState, PaymentState,
User, UserExtra,
UserConfig,
Wallet, Wallet,
) )
@@ -762,7 +759,7 @@ async def check_admin_settings():
send_admin_user_to_saas() send_admin_user_to_saas()
account = await get_account(settings.super_user) 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 settings.first_install = True
logger.success( 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 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 account = None
if super_user: if super_user:
account = await get_account(super_user) account = await get_account(super_user)
if not account: if not account:
account = await create_account( account = await new_account(super_user or uuid4().hex)
user_id=super_user, user_config=UserConfig(provider="env")
)
if not account.wallets or len(account.wallets) == 0:
await create_wallet(user_id=account.id) await create_wallet(user_id=account.id)
editable_settings = EditableSettings.from_dict(settings.dict()) editable_settings = EditableSettings.from_dict(settings.dict())
return await create_admin_settings(account.id, editable_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: class WebsocketConnectionManager:
def __init__(self) -> None: def __init__(self) -> None:
self.active_connections: list[WebSocket] = [] self.active_connections: list[WebSocket] = []
+11 -14
View File
@@ -3,7 +3,6 @@ import json
from http import HTTPStatus from http import HTTPStatus
from io import BytesIO from io import BytesIO
from time import time from time import time
from typing import Dict, List
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import httpx import httpx
@@ -13,8 +12,9 @@ from fastapi import (
Depends, Depends,
) )
from fastapi.exceptions import HTTPException 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 ( from lnbits.core.models import (
BaseWallet, BaseWallet,
ConversionData, ConversionData,
@@ -38,11 +38,7 @@ from lnbits.utils.exchange_rates import (
satoshis_amount_as_fiat, satoshis_amount_as_fiat,
) )
from ..services import create_user_account, perform_lnurlauth from ..services import 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
api_router = APIRouter(tags=["Core"]) api_router = APIRouter(tags=["Core"])
@@ -61,7 +57,7 @@ async def health() -> dict:
name="Wallets", name="Wallets",
description="Get basic info for all of user's 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] 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, status_code=HTTPStatus.FORBIDDEN,
detail="Account creation is disabled.", detail="Account creation is disabled.",
) )
account = await create_user_account(wallet_name=data.name) account = await create_account()
return account.wallets[0] wallet = await create_wallet(user_id=account.id, wallet_name=data.name)
return wallet
@api_router.get("/api/v1/lnurlscan/{code}") @api_router.get("/api/v1/lnurlscan/{code}")
@@ -101,7 +98,7 @@ async def api_lnurlscan(
) from exc ) from exc
# params is what will be returned to the client # params is what will be returned to the client
params: Dict = {"domain": domain} params: dict = {"domain": domain}
if "tag=login" in url: if "tag=login" in url:
params.update(kind="auth") params.update(kind="auth")
@@ -150,7 +147,7 @@ async def api_lnurlscan(
# callback with k1 already in it # callback with k1 already in it
parsed_callback: ParseResult = urlparse(data["callback"]) parsed_callback: ParseResult = urlparse(data["callback"])
qs: Dict = parse_qs(parsed_callback.query) qs: dict = parse_qs(parsed_callback.query)
qs["k1"] = data["k1"] qs["k1"] = data["k1"]
# balanceCheck/balanceNotify # balanceCheck/balanceNotify
@@ -207,13 +204,13 @@ async def api_perform_lnurlauth(
@api_router.get("/api/v1/rate/{currency}") @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) rate = await get_fiat_rate_satoshis(currency)
return {"rate": rate} return {"rate": rate}
@api_router.get("/api/v1/currencies") @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() return allowed_currencies()
+163 -171
View File
@@ -2,18 +2,13 @@ import base64
import importlib import importlib
import json import json
from time import time from time import time
from http import HTTPStatus
from typing import Callable, Optional 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.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 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.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
@@ -23,14 +18,17 @@ from lnbits.helpers import (
encrypt_internal_message, encrypt_internal_message,
is_valid_email_address, is_valid_email_address,
is_valid_username, is_valid_username,
urlsafe_short_hash,
) )
from lnbits.settings import AuthMethods, settings from lnbits.settings import AuthMethods, settings
from lnbits.utils.nostr import normalize_public_key, verify_event from lnbits.utils.nostr import normalize_public_key, verify_event
from ..crud import ( from ..crud import (
create_account,
get_account, get_account,
get_account_by_email, get_account_by_email,
get_account_by_pubkey, get_account_by_pubkey,
get_account_by_username,
get_account_by_username_or_email, get_account_by_username_or_email,
get_user, get_user,
get_user_password, get_user_password,
@@ -39,8 +37,10 @@ from ..crud import (
update_user_pubkey, update_user_pubkey,
verify_user_password, verify_user_password,
) )
from ..models import ( from ..models import (
AccessTokenPayload, AccessTokenPayload,
Account,
CreateUser, CreateUser,
LoginUsernamePassword, LoginUsernamePassword,
LoginUsr, LoginUsr,
@@ -50,7 +50,7 @@ from ..models import (
UpdateUserPassword, UpdateUserPassword,
UpdateUserPubkey, UpdateUserPubkey,
User, User,
UserConfig, UserExtra,
) )
auth_router = APIRouter(prefix="/api/v1/auth", tags=["Auth"]) 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: async def login(data: LoginUsernamePassword) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password): if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException( raise HTTPException(
HTTP_401_UNAUTHORIZED, "Login by 'Username and Password' not allowed." HTTPStatus.UNAUTHORIZED, "Login by 'Username and Password' not allowed."
) )
account = await get_account_by_username_or_email(data.username)
try: if not account or not account.verify_password(data.password):
user = await get_account_by_username_or_email(data.username) raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid credentials."
if not user: )
raise HTTPException(HTTP_401_UNAUTHORIZED, "Invalid credentials.") return _auth_success_response(account.username, account.id)
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
@auth_router.post("/nostr", description="Login via Nostr") @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") @auth_router.post("/usr", description="Login via the User ID")
async def login_usr(data: LoginUsr) -> JSONResponse: async def login_usr(data: LoginUsr) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.user_id_only): if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'User ID' not allowed.") raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
try: detail="Login by 'User ID' not allowed.",
user = await get_user(data.usr) )
if not user: account = await get_account(data.usr)
raise HTTPException(HTTP_401_UNAUTHORIZED, "User ID does not exist.") if not account:
raise HTTPException(
return _auth_success_response(user.username or "", user.id, user.email) status_code=HTTPStatus.UNAUTHORIZED, detail="User ID does not exist."
except HTTPException as exc: )
raise exc return _auth_success_response(account.username, account.id)
except Exception as exc:
logger.debug(exc)
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.") from exc
@auth_router.get("/{provider}", description="SSO Provider") @auth_router.get("/{provider}", description="SSO Provider")
@@ -133,7 +121,8 @@ async def login_with_sso_provider(
provider_sso = _new_sso(provider) provider_sso = _new_sso(provider)
if not provider_sso: if not provider_sso:
raise HTTPException( 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" 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) provider_sso = _new_sso(provider)
if not provider_sso: if not provider_sso:
raise HTTPException( raise HTTPException(
HTTP_401_UNAUTHORIZED, f"Login by '{provider}' not allowed." status_code=HTTPStatus.UNAUTHORIZED,
detail=f"Login by '{provider}' not allowed.",
) )
try: try:
@@ -160,18 +150,18 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons
except HTTPException as exc: except HTTPException as exc:
raise exc raise exc
except ValueError as 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: except Exception as exc:
logger.debug(exc) logger.debug(exc)
raise HTTPException( raise HTTPException(
HTTP_500_INTERNAL_SERVER_ERROR, status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
f"Cannot authenticate user with {provider} Auth.", detail=f"Cannot authenticate user with {provider} Auth.",
) from exc ) from exc
@auth_router.post("/logout") @auth_router.post("/logout")
async def logout() -> JSONResponse: 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("cookie_access_token")
response.delete_cookie("is_lnbits_user_authorized") response.delete_cookie("is_lnbits_user_authorized")
response.delete_cookie("is_access_token_expired") response.delete_cookie("is_access_token_expired")
@@ -184,62 +174,36 @@ async def logout() -> JSONResponse:
async def register(data: CreateUser) -> JSONResponse: async def register(data: CreateUser) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password): if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException( 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: 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: 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): 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): 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: account = Account(
user = await create_user_account( id=urlsafe_short_hash(),
email=data.email, username=data.username, password=data.password email=data.email,
) username=data.username,
return _auth_success_response(user.username, user.id, user.email) )
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") @auth_router.put("/pubkey")
@@ -259,52 +223,68 @@ async def update_pubkey(
raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc
except Exception as exc: except Exception as exc:
logger.debug(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( raise HTTPException(
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user pubkey." status_code=HTTPStatus.BAD_REQUEST, detail="Invalid user ID."
) from exc )
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") @auth_router.put("/reset")
async def reset_password(data: ResetUserPassword) -> JSONResponse: async def reset_password(data: ResetUserPassword) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password): if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException( 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( reset_data_json = decrypt_internal_message(
base64.b64decode(data.reset_key[10:]).decode() base64.b64decode(data.reset_key[10:]).decode()
) )
assert reset_data_json, "Cannot process reset key." assert reset_data_json, "Cannot process reset key."
action, user_id, request_time = json.loads(reset_data_json) action, user_id, request_time = json.loads(reset_data_json)
assert action == "reset", "Expected reset action." assert action == "reset", "Expected reset action."
assert user_id is not None, "Missing user ID." assert user_id is not None, "Missing user ID."
assert request_time is not None, "Missing reset time." assert request_time is not None, "Missing reset time."
user = await get_account(user_id) user = await get_account(user_id)
assert user, "User not found." assert user, "User not found."
update_pwd = UpdateUserPassword( update_pwd = UpdateUserPassword(
user_id=user.id, user_id=user.id,
username=user.username or "", username=user.username or "",
password=data.password, password=data.password,
password_repeat=data.password_repeat, password_repeat=data.password_repeat,
) )
user = await update_user_password(update_pwd, request_time) user = await update_user_password(update_pwd, request_time)
return _auth_success_response( return _auth_success_response(
username=user.username, user_id=user_id, email=user.email 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
@auth_router.put("/update") @auth_router.put("/update")
@@ -312,80 +292,92 @@ async def update(
data: UpdateUser, user: User = Depends(check_user_exists) data: UpdateUser, user: User = Depends(check_user_exists)
) -> 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.")
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( raise HTTPException(
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user." status_code=HTTPStatus.BAD_REQUEST, detail="Invalid user ID."
) from exc )
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") @auth_router.put("/first_install")
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse: async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
if not settings.first_install: if not settings.first_install:
raise HTTPException(HTTP_401_UNAUTHORIZED, "This is not your first install") raise HTTPException(HTTPStatus.UNAUTHORIZED, "This is not your first install")
try: account = await get_account(settings.super_user)
await update_account( if not account:
user_id=settings.super_user, raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
username=data.username, account.username = data.username
user_config=UserConfig(provider="lnbits"), account.extra = account.extra or UserExtra()
) account.extra.provider = "lnbits"
super_user = UpdateUserPassword( account.hash_password(data.password)
user_id=settings.super_user, await update_account(account)
password=data.password, settings.first_install = False
password_repeat=data.password_repeat, return _auth_success_response(username=account.username)
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
async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] = None): async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] = None):
email = userinfo.email email = userinfo.email
if not email or not is_valid_email_address(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" redirect_path = "/wallet"
user_config = UserConfig(**dict(userinfo))
user_config.email_verified = True
account = await get_account_by_email(email) account = await get_account_by_email(email)
if verified_user_id: if verified_user_id:
if account: if account:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Email already used.") raise HTTPException(HTTPStatus.UNAUTHORIZED, "Email already used.")
account = await get_account(verified_user_id) account = await get_account(verified_user_id)
if not account: if not account:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Cannot verify user email.") raise HTTPException(HTTPStatus.UNAUTHORIZED, "Cannot verify user email.")
redirect_path = "/account" redirect_path = "/account"
if 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: else:
if not settings.new_accounts_allowed: if not settings.new_accounts_allowed:
raise HTTPException(HTTP_400_BAD_REQUEST, "Account creation is disabled.") raise HTTPException(HTTPStatus.BAD_REQUEST, "Account creation is disabled.")
user = await create_user_account(email=email, user_config=user_config) account = Account(
id=urlsafe_short_hash(), email=email, extra=UserExtra(email_verified=True)
if not user: )
raise HTTPException(HTTP_401_UNAUTHORIZED, "User not found.") await create_account(account)
return _auth_redirect_response(redirect_path, email) return _auth_redirect_response(redirect_path, email)
+77 -61
View File
@@ -1,7 +1,4 @@
from http import HTTPStatus from http import HTTPStatus
from typing import (
List,
)
from bolt11 import decode as bolt11_decode from bolt11 import decode as bolt11_decode
from fastapi import ( from fastapi import (
@@ -25,6 +22,7 @@ from lnbits.core.extensions.models import (
InstallableExtension, InstallableExtension,
PayToEnableInfo, PayToEnableInfo,
ReleasePaymentInfo, ReleasePaymentInfo,
UserExtension,
UserExtensionInfo, UserExtensionInfo,
) )
from lnbits.core.models import ( from lnbits.core.models import (
@@ -38,6 +36,7 @@ from lnbits.decorators import (
) )
from ..crud import ( from ..crud import (
create_user_extension,
delete_dbversion, delete_dbversion,
drop_extension_db, drop_extension_db,
get_dbversions, get_dbversions,
@@ -46,7 +45,6 @@ from ..crud import (
get_user_extension, get_user_extension,
update_extension_pay_to_enable, update_extension_pay_to_enable,
update_user_extension, update_user_extension,
update_user_extension_extra,
) )
extension_router = APIRouter( extension_router = APIRouter(
@@ -176,18 +174,24 @@ async def api_enable_extension(
assert ext, f"Extension '{ext_id}' is not installed." assert ext, f"Extension '{ext_id}' is not installed."
assert ext.active, f"Extension '{ext_id}' is not activated." 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: 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.") return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
user_ext = await get_user_extension(user.id, ext_id) if not (user_ext.extra and user_ext.extra.payment_hash_to_enable):
if not (user_ext and user_ext.extra and user_ext.extra.payment_hash_to_enable):
raise HTTPException( raise HTTPException(
HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment." HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment."
) )
if user_ext.is_paid: 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( return SimpleStatus(
success=True, message=f"Paid extension '{ext_id}' enabled." 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}'.", f"Invoice generated but not paid for enabeling extension '{ext_id}'.",
) )
user_ext.active = True
user_ext.extra.paid_to_enable = 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_ext)
await update_user_extension(user_id=user.id, extension=ext_id, active=True)
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.") return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
except AssertionError as exc: except AssertionError as exc:
@@ -233,16 +236,15 @@ async def api_disable_extension(
raise HTTPException( raise HTTPException(
HTTPStatus.BAD_REQUEST, f"Extension '{ext_id}' doesn't exist." HTTPStatus.BAD_REQUEST, f"Extension '{ext_id}' doesn't exist."
) )
try: user_ext = await get_user_extension(user.id, ext_id)
logger.info(f"Disabeling extension: {ext_id}.") if not user_ext or not user_ext.active:
await update_user_extension(user_id=user.id, extension=ext_id, active=False) return SimpleStatus(
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.") success=True, message=f"Extension '{ext_id}' already disabled."
except Exception as exc: )
logger.warning(exc) logger.info(f"Disabeling extension: {ext_id}.")
raise HTTPException( user_ext.active = False
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, await update_user_extension(user_ext)
detail=(f"Failed to disable '{ext_id}'."), return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
) from exc
@extension_router.put("/{ext_id}/activate", dependencies=[Depends(check_admin)]) @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)]) @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: try:
extension_releases: List[ExtensionRelease] = ( extension_releases: list[ExtensionRelease] = (
await InstallableExtension.get_extension_releases(ext_id) 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( async def get_pay_to_enable_invoice(
ext_id: str, data: PayToEnableInfo, user: User = Depends(check_user_exists) ext_id: str, data: PayToEnableInfo, user: User = Depends(check_user_exists)
): ):
try: if not data.amount or data.amount <= 0:
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)
raise HTTPException( raise HTTPException(
HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot request invoice." status_code=HTTPStatus.BAD_REQUEST, detail="Amount must be greater than 0."
) from exc )
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( @extension_router.get(
+14 -14
View File
@@ -25,9 +25,11 @@ from ...utils.exchange_rates import allowed_currencies, currencies
from ..crud import ( from ..crud import (
create_account, create_account,
create_wallet, create_wallet,
get_account,
get_dbversions, get_dbversions,
get_installed_extensions, get_installed_extensions,
get_user, get_user,
get_wallet,
) )
generic_router = APIRouter( generic_router = APIRouter(
@@ -136,7 +138,8 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
] ]
# refresh user state. Eg: enabled extensions. # 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( return template_renderer().TemplateResponse(
request, request,
@@ -165,18 +168,16 @@ async def wallet(
wal: Optional[UUID4] = Query(None), wal: Optional[UUID4] = Query(None),
): ):
if wal: if wal:
wallet_id = wal.hex wallet = await get_wallet(wal.hex)
elif len(user.wallets) == 0: elif len(user.wallets) == 0:
wallet = await create_wallet(user_id=user.id) wallet = await create_wallet(user_id=user.id)
user = await get_user(user_id=user.id) or user user.wallets.append(wallet)
wallet_id = wallet.id
elif lnbits_last_active_wallet and user.get_wallet(lnbits_last_active_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: else:
wallet_id = user.wallets[0].id wallet = user.wallets[0]
user_wallet = user.get_wallet(wallet_id) if not wallet or wallet.deleted:
if not user_wallet or user_wallet.deleted:
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(
request, "error.html", {"err": "Wallet not found"}, HTTPStatus.NOT_FOUND request, "error.html", {"err": "Wallet not found"}, HTTPStatus.NOT_FOUND
) )
@@ -186,14 +187,14 @@ async def wallet(
"core/wallet.html", "core/wallet.html",
{ {
"user": user.dict(), "user": user.dict(),
"wallet": user_wallet.dict(), "wallet": wallet.dict(),
"currencies": allowed_currencies(), "currencies": allowed_currencies(),
"service_fee": settings.lnbits_service_fee, "service_fee": settings.lnbits_service_fee,
"service_fee_max": settings.lnbits_service_fee_max, "service_fee_max": settings.lnbits_service_fee_max,
"web_manifest": f"/manifest/{user.id}.webmanifest", "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 return resp
@@ -228,11 +229,10 @@ async def service_worker(request: Request):
@generic_router.get("/manifest/{usr}.webmanifest") @generic_router.get("/manifest/{usr}.webmanifest")
async def manifest(request: Request, usr: str): async def manifest(request: Request, usr: str):
host = urlparse(str(request.url)).netloc host = urlparse(str(request.url)).netloc
account = await get_account(usr)
user = await get_user(usr) if not account:
if not user:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND) raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
user = await get_user(account)
return { return {
"short_name": settings.lnbits_site_title, "short_name": settings.lnbits_site_title,
"name": settings.lnbits_site_title + " Wallet", "name": settings.lnbits_site_title + " Wallet",
+1 -11
View File
@@ -41,17 +41,7 @@ users_router = APIRouter(prefix="/users/api/v1", dependencies=[Depends(check_adm
async def api_get_users( async def api_get_users(
filters: Filters = Depends(parse_filters(AccountFilters)), filters: Filters = Depends(parse_filters(AccountFilters)),
) -> Page[Account]: ) -> Page[Account]:
try: return await get_accounts(filters=filters)
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
@users_router.delete("/user/{user_id}", status_code=HTTPStatus.OK) @users_router.delete("/user/{user_id}", status_code=HTTPStatus.OK)
+2 -5
View File
@@ -65,7 +65,7 @@ class KeyChecker(SecurityBase):
name="X-API-KEY", name="X-API-KEY",
description="Wallet API Key - HEADER", description="Wallet API Key - HEADER",
) )
self.model: APIKey = openapi_model self.model: APIKey = openapi_model # type: ignore
async def __call__(self, request: Request) -> WalletTypeInfo: 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): if not account or not settings.is_user_allowed(account.id):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not allowed.") raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not allowed.")
user = await get_user(account.id) user = await get_user(account)
assert user, "User not found for account."
await _check_user_extension_access(user.id, r["path"]) await _check_user_extension_access(user.id, r["path"])
return user return user
+4 -1
View File
@@ -17,6 +17,7 @@ from lnbits.core.crud import (
create_account, create_account,
create_wallet, create_wallet,
get_account_by_username, get_account_by_username,
get_account,
get_user, get_user,
update_payment_status, update_payment_status,
) )
@@ -148,7 +149,9 @@ def from_super_user(from_user):
@pytest_asyncio.fixture(scope="session") @pytest_asyncio.fixture(scope="session")
async def superuser(): 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 yield user