feat: basic account activate/deactivate
This commit is contained in:
@@ -61,7 +61,9 @@ from .users import (
|
|||||||
get_user,
|
get_user,
|
||||||
get_user_access_control_lists,
|
get_user_access_control_lists,
|
||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
|
is_account_activated,
|
||||||
update_account,
|
update_account,
|
||||||
|
update_account_activation,
|
||||||
)
|
)
|
||||||
from .wallets import (
|
from .wallets import (
|
||||||
create_wallet,
|
create_wallet,
|
||||||
@@ -144,11 +146,13 @@ __all__ = [
|
|||||||
"get_wallets",
|
"get_wallets",
|
||||||
"get_webpush_subscription",
|
"get_webpush_subscription",
|
||||||
"get_webpush_subscriptions_for_user",
|
"get_webpush_subscriptions_for_user",
|
||||||
|
"is_account_activated",
|
||||||
"is_internal_status_success",
|
"is_internal_status_success",
|
||||||
"mark_webhook_sent",
|
"mark_webhook_sent",
|
||||||
"remove_deleted_wallets",
|
"remove_deleted_wallets",
|
||||||
"reset_core_settings",
|
"reset_core_settings",
|
||||||
"update_account",
|
"update_account",
|
||||||
|
"update_account_activation",
|
||||||
"update_admin_settings",
|
"update_admin_settings",
|
||||||
"update_installed_extension",
|
"update_installed_extension",
|
||||||
"update_installed_extension_state",
|
"update_installed_extension_state",
|
||||||
|
|||||||
+83
-22
@@ -36,6 +36,23 @@ async def update_account(account: Account, conn: Connection | None = None) -> Ac
|
|||||||
return account
|
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:
|
async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE from accounts WHERE id = :user",
|
"DELETE from accounts WHERE id = :user",
|
||||||
@@ -69,6 +86,7 @@ async def get_accounts(
|
|||||||
accounts.email,
|
accounts.email,
|
||||||
accounts.pubkey,
|
accounts.pubkey,
|
||||||
accounts.external_id,
|
accounts.external_id,
|
||||||
|
accounts.activated,
|
||||||
SUM(COALESCE((
|
SUM(COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)) as balance_msat,
|
), 0)) as balance_msat,
|
||||||
@@ -93,16 +111,33 @@ async def get_accounts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
|
async def get_account(
|
||||||
|
user_id: str, activated: bool | None = True, conn: Connection | None = None
|
||||||
|
) -> Account | None:
|
||||||
if len(user_id) == 0:
|
if len(user_id) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE id = :id",
|
f"""
|
||||||
{"id": user_id},
|
SELECT * FROM accounts
|
||||||
|
WHERE id = :id {_activated_clause(activated)}
|
||||||
|
""", # noqa: S608
|
||||||
|
{"id": user_id, "activated": activated},
|
||||||
Account,
|
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(
|
async def delete_accounts_no_wallets(
|
||||||
time_delta: int,
|
time_delta: int,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
@@ -124,55 +159,72 @@ async def delete_accounts_no_wallets(
|
|||||||
|
|
||||||
|
|
||||||
async def get_account_by_username(
|
async def get_account_by_username(
|
||||||
username: str, conn: Connection | None = None
|
username: str, activated: bool | None = True, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
if len(username) == 0:
|
if len(username) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(username) = :username",
|
f"""
|
||||||
{"username": username.lower()},
|
SELECT * FROM accounts
|
||||||
|
WHERE LOWER(username) = :username {_activated_clause(activated)}
|
||||||
|
""", # noqa: S608
|
||||||
|
{"username": username.lower(), "activated": activated},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_pubkey(
|
async def get_account_by_pubkey(
|
||||||
pubkey: str, conn: Connection | None = None
|
pubkey: str, activated: bool | None = True, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
f"""
|
||||||
{"pubkey": pubkey.lower()},
|
SELECT * FROM accounts
|
||||||
|
WHERE LOWER(pubkey) = :pubkey {_activated_clause(activated)}
|
||||||
|
""", # noqa: S608
|
||||||
|
{"pubkey": pubkey.lower(), "activated": activated},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_email(
|
async def get_account_by_email(
|
||||||
email: str, conn: Connection | None = None
|
email: str, activated: bool | None = True, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
if len(email) == 0:
|
if len(email) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(email) = :email",
|
f"""
|
||||||
{"email": email.lower()},
|
SELECT * FROM accounts
|
||||||
|
WHERE LOWER(email) = :email {_activated_clause(activated)}
|
||||||
|
""", # noqa: S608
|
||||||
|
{"email": email.lower(), "activated": activated},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_username_or_email(
|
async def get_account_by_username_or_email(
|
||||||
username_or_email: str, conn: Connection | None = None
|
username_or_email: str,
|
||||||
|
activated: bool | None = True,
|
||||||
|
conn: Connection | None = None,
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
f"""
|
||||||
SELECT * FROM accounts
|
SELECT * FROM accounts
|
||||||
WHERE LOWER(email) = :value or LOWER(username) = :value
|
WHERE (LOWER(email) = :value or LOWER(username) = :value)
|
||||||
""",
|
{_activated_clause(activated)}
|
||||||
{"value": username_or_email.lower()},
|
""", # noqa: S608
|
||||||
|
{"value": username_or_email.lower(), "activated": activated},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
async def get_user(
|
||||||
|
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:
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
account = await get_account(user_id, conn=conn)
|
account = await get_account(user_id, activated=activated, conn=conn)
|
||||||
if not account:
|
if not account:
|
||||||
return None
|
return None
|
||||||
return await get_user_from_account(account, conn=conn)
|
return await get_user_from_account(account, conn=conn)
|
||||||
@@ -216,12 +268,21 @@ async def update_user_access_control_list(
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_access_control_lists(
|
async def get_user_access_control_lists(
|
||||||
user_id: str, conn: Connection | None = None
|
user_id: str, activated: bool | None = True, conn: Connection | None = None
|
||||||
) -> UserAcls:
|
) -> UserAcls:
|
||||||
user_acls = await (conn or db).fetchone(
|
user_acls = await (conn or db).fetchone(
|
||||||
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
f"""
|
||||||
{"id": user_id},
|
SELECT id, access_control_list FROM accounts
|
||||||
|
WHERE id = :user_id {_activated_clause(activated)}
|
||||||
|
""", # noqa: S608
|
||||||
|
{"user_id": user_id, "activated": activated},
|
||||||
UserAcls,
|
UserAcls,
|
||||||
)
|
)
|
||||||
|
|
||||||
return user_acls or UserAcls(id=user_id)
|
return user_acls or UserAcls(id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _activated_clause(activated: bool | None) -> str:
|
||||||
|
if activated is None:
|
||||||
|
return ""
|
||||||
|
return "AND activated = :activated"
|
||||||
|
|||||||
@@ -856,3 +856,11 @@ async def m043_add_ui_customization_to_accounts(db: Connection):
|
|||||||
Used for server side persistence of UI customization settings.
|
Used for server side persistence of UI customization settings.
|
||||||
"""
|
"""
|
||||||
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
async def m044_add_activated_to_accounts(db: Connection):
|
||||||
|
"""
|
||||||
|
Adds activated column to accounts.
|
||||||
|
Used for account activation status.
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ class Account(AccountId):
|
|||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
|
activated: bool
|
||||||
transaction_count: int | None = 0
|
transaction_count: int | None = 0
|
||||||
wallet_count: int | None = 0
|
wallet_count: int | None = 0
|
||||||
balance_msat: int | None = 0
|
balance_msat: int | None = 0
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ from ..crud import (
|
|||||||
get_account_by_username,
|
get_account_by_username,
|
||||||
get_account_by_username_or_email,
|
get_account_by_username_or_email,
|
||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
|
is_account_activated,
|
||||||
update_account,
|
update_account,
|
||||||
)
|
)
|
||||||
from ..models import (
|
from ..models import (
|
||||||
@@ -83,9 +84,13 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.FORBIDDEN, "Login by 'Username and Password' not allowed."
|
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):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
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)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from lnbits.core.crud import (
|
|||||||
get_user,
|
get_user,
|
||||||
get_wallet,
|
get_wallet,
|
||||||
get_wallets,
|
get_wallets,
|
||||||
|
is_account_activated,
|
||||||
|
update_account_activation,
|
||||||
update_admin_settings,
|
update_admin_settings,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
)
|
)
|
||||||
@@ -197,7 +199,7 @@ async def api_users_reset_password(user_id: str) -> str:
|
|||||||
return f"reset_key_{reset_key_b64}"
|
return f"reset_key_{reset_key_b64}"
|
||||||
|
|
||||||
|
|
||||||
@users_router.get(
|
@users_router.put(
|
||||||
"/user/{user_id}/admin",
|
"/user/{user_id}/admin",
|
||||||
dependencies=[Depends(check_super_user)],
|
dependencies=[Depends(check_super_user)],
|
||||||
name="Give or revoke admin permsisions to a user",
|
name="Give or revoke admin permsisions to a user",
|
||||||
@@ -220,6 +222,42 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@users_router.put(
|
||||||
|
"/user/{user_id}/activate",
|
||||||
|
name="Activate or deactivate a user",
|
||||||
|
)
|
||||||
|
async def api_users_toggle_activated(
|
||||||
|
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 == account.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Users cannot deactivate themselves.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings.is_admin_user(user_id):
|
||||||
|
settings.lnbits_admin_users.remove(user_id)
|
||||||
|
|
||||||
|
is_activated = await is_account_activated(user_id)
|
||||||
|
|
||||||
|
if is_activated is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="User not found.",
|
||||||
|
)
|
||||||
|
await update_account_activation(user_id, not is_activated)
|
||||||
|
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True,
|
||||||
|
message=f"User {'activated' if not is_activated else 'deactivated'}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
||||||
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
||||||
return await get_wallets(user_id, deleted=None)
|
return await get_wallets(user_id, deleted=None)
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ window.localisation.en = {
|
|||||||
installed: 'Installed',
|
installed: 'Installed',
|
||||||
activated: 'Activated',
|
activated: 'Activated',
|
||||||
deactivated: 'Deactivated',
|
deactivated: 'Deactivated',
|
||||||
|
activate: 'Activate',
|
||||||
|
deactivate: 'Deactivate',
|
||||||
release_notes: 'Release Notes',
|
release_notes: 'Release Notes',
|
||||||
activate_extension_details: 'Make extension available/unavailable for users',
|
activate_extension_details: 'Make extension available/unavailable for users',
|
||||||
featured: 'Featured',
|
featured: 'Featured',
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ window.PageUsers = {
|
|||||||
usersTable: {
|
usersTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'admin',
|
name: 'activated',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Admin',
|
label: this.$t('activated'),
|
||||||
field: 'admin',
|
field: 'activated',
|
||||||
sortable: false
|
sortable: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -401,7 +401,7 @@ window.PageUsers = {
|
|||||||
|
|
||||||
toggleAdmin(userId) {
|
toggleAdmin(userId) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', `/users/api/v1/user/${userId}/admin`)
|
.request('PUT', `/users/api/v1/user/${userId}/admin`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchUsers()
|
this.fetchUsers()
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -412,6 +412,19 @@ window.PageUsers = {
|
|||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
|
toggleUserActivated(userId) {
|
||||||
|
LNbits.api
|
||||||
|
.request('PUT', `/users/api/v1/user/${userId}/activate`)
|
||||||
|
.then(() => {
|
||||||
|
this.fetchUsers()
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Toggled user activation!',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
},
|
||||||
async showAccountPage(user_id) {
|
async showAccountPage(user_id) {
|
||||||
this.activeUser.showPassword = false
|
this.activeUser.showPassword = false
|
||||||
this.activeUser.showUserId = false
|
this.activeUser.showUserId = false
|
||||||
|
|||||||
@@ -605,10 +605,18 @@
|
|||||||
size="xs"
|
size="xs"
|
||||||
v-if="!props.row.is_super_user"
|
v-if="!props.row.is_super_user"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
v-model="props.row.is_admin"
|
v-model="props.row.activated"
|
||||||
@update:model-value="toggleAdmin(props.row.id)"
|
@update:model-value="toggleUserActivated(props.row.id)"
|
||||||
>
|
>
|
||||||
<q-tooltip>Toggle Admin</q-tooltip>
|
<q-tooltip
|
||||||
|
><span
|
||||||
|
v-text="
|
||||||
|
props.row.activated
|
||||||
|
? $t('deactivate')
|
||||||
|
: $t('activate')
|
||||||
|
"
|
||||||
|
></span
|
||||||
|
></q-tooltip>
|
||||||
</q-toggle>
|
</q-toggle>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
|
|||||||
Reference in New Issue
Block a user