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