feat: basic account activate/deactivate
This commit is contained in:
@@ -61,7 +61,9 @@ from .users import (
|
||||
get_user,
|
||||
get_user_access_control_lists,
|
||||
get_user_from_account,
|
||||
is_account_activated,
|
||||
update_account,
|
||||
update_account_activation,
|
||||
)
|
||||
from .wallets import (
|
||||
create_wallet,
|
||||
@@ -144,11 +146,13 @@ __all__ = [
|
||||
"get_wallets",
|
||||
"get_webpush_subscription",
|
||||
"get_webpush_subscriptions_for_user",
|
||||
"is_account_activated",
|
||||
"is_internal_status_success",
|
||||
"mark_webhook_sent",
|
||||
"remove_deleted_wallets",
|
||||
"reset_core_settings",
|
||||
"update_account",
|
||||
"update_account_activation",
|
||||
"update_admin_settings",
|
||||
"update_installed_extension",
|
||||
"update_installed_extension_state",
|
||||
|
||||
+66
-59
@@ -4,12 +4,10 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||
from lnbits.core.crud.wallets import clear_wallet_cache, create_wallet, get_wallets
|
||||
from lnbits.core.crud.wallets import create_wallet, get_wallets
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import UserAcls
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
from lnbits.helpers import sha256s
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
from ..models import (
|
||||
Account,
|
||||
@@ -38,12 +36,28 @@ async def update_account(account: Account, conn: Connection | None = None) -> Ac
|
||||
return account
|
||||
|
||||
|
||||
async def update_account_activation(
|
||||
user_id: str, activated: bool, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
UPDATE accounts
|
||||
SET activated = :activated, updated_at = {db.timestamp_placeholder('now')}
|
||||
WHERE id = :user_id
|
||||
""", # noqa: S608
|
||||
{
|
||||
"activated": activated,
|
||||
"now": int(time()),
|
||||
"user_id": user_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
||||
await (conn or db).execute(
|
||||
"DELETE from accounts WHERE id = :user",
|
||||
{"user": user_id},
|
||||
)
|
||||
await clear_user_id_cache(user_id)
|
||||
|
||||
|
||||
async def get_accounts(
|
||||
@@ -98,21 +112,32 @@ async def get_accounts(
|
||||
|
||||
|
||||
async def get_account(
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
user_id: str, activated: bool | None = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(user_id) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
f"""
|
||||
SELECT * FROM accounts
|
||||
WHERE id = :id AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"id": user_id, "activated": active_only},
|
||||
WHERE id = :id {_activated_clause(activated)}
|
||||
""", # noqa: S608
|
||||
{"id": user_id, "activated": activated},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def is_account_activated(
|
||||
user_id: str, conn: Connection | None = None
|
||||
) -> bool | None:
|
||||
result = await (conn or db).execute(
|
||||
"SELECT activated FROM accounts WHERE id = :id",
|
||||
{"id": user_id},
|
||||
)
|
||||
row = result.mappings().first()
|
||||
return row.get("activated", False) if row is not None else None
|
||||
|
||||
|
||||
async def delete_accounts_no_wallets(
|
||||
time_delta: int,
|
||||
conn: Connection | None = None,
|
||||
@@ -134,79 +159,72 @@ async def delete_accounts_no_wallets(
|
||||
|
||||
|
||||
async def get_account_by_username(
|
||||
username: str, active_only: bool = True, conn: Connection | None = None
|
||||
username: str, activated: bool | None = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(username) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
f"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(username) = :username
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"username": username.lower(), "activated": active_only},
|
||||
WHERE LOWER(username) = :username {_activated_clause(activated)}
|
||||
""", # noqa: S608
|
||||
{"username": username.lower(), "activated": activated},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_pubkey(
|
||||
pubkey: str, active_only: bool = True, conn: Connection | None = None
|
||||
pubkey: str, activated: bool | None = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
f"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(pubkey) = :pubkey
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"pubkey": pubkey.lower(), "activated": active_only},
|
||||
WHERE LOWER(pubkey) = :pubkey {_activated_clause(activated)}
|
||||
""", # noqa: S608
|
||||
{"pubkey": pubkey.lower(), "activated": activated},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_email(
|
||||
email: str, active_only: bool = True, conn: Connection | None = None
|
||||
email: str, activated: bool | None = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(email) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
f"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(email) = :email
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"email": email.lower(), "activated": active_only},
|
||||
WHERE LOWER(email) = :email {_activated_clause(activated)}
|
||||
""", # noqa: S608
|
||||
{"email": email.lower(), "activated": activated},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_username_or_email(
|
||||
username_or_email: str,
|
||||
active_only: bool = True,
|
||||
activated: bool | None = True,
|
||||
conn: Connection | None = None,
|
||||
) -> Account | None:
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
f"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
(LOWER(email) = :value or LOWER(username) = :value)
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"value": username_or_email.lower(), "activated": active_only},
|
||||
WHERE (LOWER(email) = :value or LOWER(username) = :value)
|
||||
{_activated_clause(activated)}
|
||||
""", # noqa: S608
|
||||
{"value": username_or_email.lower(), "activated": activated},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_user(
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
user_id: str, activated: bool | None = True, conn: Connection | None = None
|
||||
) -> User | None:
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
account = await get_account(user_id, active_only, conn=conn)
|
||||
account = await get_account(user_id, activated=activated, conn=conn)
|
||||
if not account:
|
||||
return None
|
||||
return await get_user_from_account(account, conn=conn)
|
||||
@@ -225,7 +243,6 @@ async def get_user_from_account(
|
||||
|
||||
return User(
|
||||
id=account.id,
|
||||
activated=account.activated,
|
||||
email=account.email,
|
||||
username=account.username,
|
||||
pubkey=account.pubkey,
|
||||
@@ -251,31 +268,21 @@ async def update_user_access_control_list(
|
||||
|
||||
|
||||
async def get_user_access_control_lists(
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
user_id: str, activated: bool | None = True, conn: Connection | None = None
|
||||
) -> UserAcls:
|
||||
user_acls = await (conn or db).fetchone(
|
||||
"""
|
||||
f"""
|
||||
SELECT id, access_control_list FROM accounts
|
||||
WHERE id = :user_id AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"user_id": user_id, "activated": active_only},
|
||||
WHERE id = :user_id {_activated_clause(activated)}
|
||||
""", # noqa: S608
|
||||
{"user_id": user_id, "activated": activated},
|
||||
UserAcls,
|
||||
)
|
||||
|
||||
return user_acls or UserAcls(id=user_id)
|
||||
|
||||
|
||||
async def clear_user_id_cache(user_id: str):
|
||||
user = await get_user(user_id, active_only=True)
|
||||
if user:
|
||||
clear_user_cache(user)
|
||||
|
||||
|
||||
def clear_user_cache(user: User):
|
||||
user_cache_key: str | None = cache.pop(
|
||||
f"auth:user:cache_key:{sha256s(user.id)}", None
|
||||
)
|
||||
if user_cache_key:
|
||||
cache.pop(user_cache_key)
|
||||
for wallet in user.wallets:
|
||||
clear_wallet_cache(wallet)
|
||||
def _activated_clause(activated: bool | None) -> str:
|
||||
if activated is None:
|
||||
return ""
|
||||
return "AND activated = :activated"
|
||||
|
||||
@@ -181,7 +181,6 @@ class AccountId(BaseModel):
|
||||
|
||||
|
||||
class Account(AccountId):
|
||||
activated: bool = True
|
||||
external_id: str | None = None # for external account linking
|
||||
username: str | None = None
|
||||
password_hash: str | None = None
|
||||
@@ -242,7 +241,7 @@ class Account(AccountId):
|
||||
|
||||
|
||||
class AccountOverview(Account):
|
||||
activated: bool = True
|
||||
activated: bool
|
||||
transaction_count: int | None = 0
|
||||
wallet_count: int | None = 0
|
||||
balance_msat: int | None = 0
|
||||
@@ -278,7 +277,6 @@ class AccountFilters(FilterModel):
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
activated: bool = True
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
email: str | None = None
|
||||
|
||||
@@ -51,6 +51,7 @@ from ..crud import (
|
||||
get_account_by_username,
|
||||
get_account_by_username_or_email,
|
||||
get_user_from_account,
|
||||
is_account_activated,
|
||||
update_account,
|
||||
)
|
||||
from ..models import (
|
||||
@@ -83,10 +84,13 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "Login by 'Username and Password' not allowed."
|
||||
)
|
||||
account = await get_account_by_username_or_email(data.username)
|
||||
account = await get_account_by_username_or_email(data.username, activated=None)
|
||||
if not account or not account.verify_password(data.password):
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||
|
||||
is_activated = await is_account_activated(account.id)
|
||||
if is_activated is False:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Account is not activated.")
|
||||
return _auth_success_response(account.username, account.id, account.email)
|
||||
|
||||
|
||||
@@ -95,7 +99,7 @@ async def nostr_login(request: Request) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
||||
event = _nostr_nip98_event(request)
|
||||
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
|
||||
account = await get_account_by_pubkey(event["pubkey"])
|
||||
if not account:
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
@@ -103,8 +107,6 @@ async def nostr_login(request: Request) -> JSONResponse:
|
||||
extra=UserExtra(provider="nostr"),
|
||||
)
|
||||
await create_user_account(account)
|
||||
if not account.activated:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
|
||||
return _auth_success_response(account.username or "", account.id, account.email)
|
||||
|
||||
|
||||
@@ -360,7 +362,7 @@ async def register(data: RegisterUser) -> JSONResponse:
|
||||
if not is_valid_username(data.username):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
||||
|
||||
if await get_account_by_username(data.username, active_only=False):
|
||||
if await get_account_by_username(data.username):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||
|
||||
if data.email and not is_valid_email_address(data.email):
|
||||
@@ -530,7 +532,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||
|
||||
redirect_path = "/wallet"
|
||||
account = await get_account_by_email(email, active_only=False)
|
||||
account = await get_account_by_email(email)
|
||||
|
||||
if verified_user_id:
|
||||
if account:
|
||||
|
||||
@@ -17,10 +17,11 @@ from lnbits.core.crud import (
|
||||
get_user,
|
||||
get_wallet,
|
||||
get_wallets,
|
||||
is_account_activated,
|
||||
update_account_activation,
|
||||
update_admin_settings,
|
||||
update_wallet,
|
||||
)
|
||||
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
|
||||
from lnbits.core.crud.wallets import delete_wallet_by_id
|
||||
from lnbits.core.models import (
|
||||
AccountFilters,
|
||||
@@ -74,7 +75,7 @@ async def api_get_users(
|
||||
summary="Get user by Id",
|
||||
)
|
||||
async def api_get_user(user_id: str) -> User:
|
||||
user = await get_user(user_id, active_only=False)
|
||||
user = await get_user(user_id)
|
||||
if not user:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||
return user
|
||||
@@ -226,35 +227,34 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
||||
name="Activate or deactivate a user",
|
||||
)
|
||||
async def api_users_toggle_activated(
|
||||
user_id: str, admin_account: Account = Depends(check_admin)
|
||||
user_id: str, account: Account = Depends(check_admin)
|
||||
) -> SimpleStatus:
|
||||
if user_id == settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Cannot deactivate super user.",
|
||||
)
|
||||
if user_id == admin_account.id:
|
||||
if user_id == account.id:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="You cannot deactivate yourself.",
|
||||
detail="Users cannot deactivate themselves.",
|
||||
)
|
||||
|
||||
if settings.is_admin_user(user_id):
|
||||
settings.lnbits_admin_users.remove(user_id)
|
||||
|
||||
user_account = await get_account(user_id, active_only=False)
|
||||
if not user_account:
|
||||
is_activated = await is_account_activated(user_id)
|
||||
|
||||
if is_activated is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="User not found.",
|
||||
)
|
||||
user_account.activated = not user_account.activated
|
||||
await update_account(user_account)
|
||||
await clear_user_id_cache(user_id)
|
||||
await update_account_activation(user_id, not is_activated)
|
||||
|
||||
return SimpleStatus(
|
||||
success=True,
|
||||
message=f"User {'activated' if user_account.activated else 'deactivated'}.",
|
||||
message=f"User {'activated' if not is_activated else 'deactivated'}.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -415,11 +415,11 @@ window.PageUsers = {
|
||||
toggleUserActivated(userId) {
|
||||
LNbits.api
|
||||
.request('PUT', `/users/api/v1/user/${userId}/activate`)
|
||||
.then(res => {
|
||||
.then(() => {
|
||||
this.fetchUsers()
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: res.data.message,
|
||||
message: 'Toggled user activation!',
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user