big fx part2
This commit is contained in:
+69
-198
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user