mirror of
https://github.com/lnbits/lnbits.git
synced 2025-10-05 19:22:34 +02:00
feat: improve user admin (#2777)
This commit is contained in:
@@ -13,6 +13,7 @@ from .extensions import (
|
|||||||
get_installed_extensions,
|
get_installed_extensions,
|
||||||
get_user_active_extensions_ids,
|
get_user_active_extensions_ids,
|
||||||
get_user_extension,
|
get_user_extension,
|
||||||
|
get_user_extensions,
|
||||||
update_installed_extension,
|
update_installed_extension,
|
||||||
update_installed_extension_state,
|
update_installed_extension_state,
|
||||||
update_user_extension,
|
update_user_extension,
|
||||||
@@ -98,6 +99,7 @@ __all__ = [
|
|||||||
"update_installed_extension",
|
"update_installed_extension",
|
||||||
"update_installed_extension_state",
|
"update_installed_extension_state",
|
||||||
"update_user_extension",
|
"update_user_extension",
|
||||||
|
"get_user_extensions",
|
||||||
# payments
|
# payments
|
||||||
"DateTrunc",
|
"DateTrunc",
|
||||||
"check_internal",
|
"check_internal",
|
||||||
|
@@ -20,7 +20,9 @@ async def create_account(
|
|||||||
account: Optional[Account] = None,
|
account: Optional[Account] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Account:
|
) -> Account:
|
||||||
if not account:
|
if account:
|
||||||
|
account.validate_fields()
|
||||||
|
else:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
account = Account(id=uuid4().hex, created_at=now, updated_at=now)
|
account = Account(id=uuid4().hex, created_at=now, updated_at=now)
|
||||||
await (conn or db).insert("accounts", account)
|
await (conn or db).insert("accounts", account)
|
||||||
@@ -50,6 +52,8 @@ async def get_accounts(
|
|||||||
accounts.id,
|
accounts.id,
|
||||||
accounts.username,
|
accounts.username,
|
||||||
accounts.email,
|
accounts.email,
|
||||||
|
accounts.pubkey,
|
||||||
|
wallets.id as wallet_id,
|
||||||
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,
|
||||||
|
@@ -28,6 +28,7 @@ from .users import (
|
|||||||
CreateUser,
|
CreateUser,
|
||||||
LoginUsernamePassword,
|
LoginUsernamePassword,
|
||||||
LoginUsr,
|
LoginUsr,
|
||||||
|
RegisterUser,
|
||||||
ResetUserPassword,
|
ResetUserPassword,
|
||||||
UpdateSuperuserPassword,
|
UpdateSuperuserPassword,
|
||||||
UpdateUser,
|
UpdateUser,
|
||||||
@@ -70,6 +71,7 @@ __all__ = [
|
|||||||
"AccountOverview",
|
"AccountOverview",
|
||||||
"CreateTopup",
|
"CreateTopup",
|
||||||
"CreateUser",
|
"CreateUser",
|
||||||
|
"RegisterUser",
|
||||||
"LoginUsernamePassword",
|
"LoginUsernamePassword",
|
||||||
"LoginUsr",
|
"LoginUsr",
|
||||||
"ResetUserPassword",
|
"ResetUserPassword",
|
||||||
|
@@ -2,12 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
|
from lnbits.helpers import is_valid_email_address, is_valid_pubkey, is_valid_username
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .wallets import Wallet
|
from .wallets import Wallet
|
||||||
@@ -36,13 +38,13 @@ class Account(BaseModel):
|
|||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
@property
|
is_super_user: bool = Field(default=False, no_database=True)
|
||||||
def is_super_user(self) -> bool:
|
is_admin: bool = Field(default=False, no_database=True)
|
||||||
return self.id == settings.super_user
|
|
||||||
|
|
||||||
@property
|
def __init__(self, **data):
|
||||||
def is_admin(self) -> bool:
|
super().__init__(**data)
|
||||||
return self.id in settings.lnbits_admin_users or self.is_super_user
|
self.is_super_user = settings.is_super_user(self.id)
|
||||||
|
self.is_admin = settings.is_admin_user(self.id)
|
||||||
|
|
||||||
def hash_password(self, password: str) -> str:
|
def hash_password(self, password: str) -> str:
|
||||||
"""sets and returns the hashed password"""
|
"""sets and returns the hashed password"""
|
||||||
@@ -57,6 +59,17 @@ class Account(BaseModel):
|
|||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
return pwd_context.verify(password, self.password_hash)
|
return pwd_context.verify(password, self.password_hash)
|
||||||
|
|
||||||
|
def validate_fields(self):
|
||||||
|
if self.username and not is_valid_username(self.username):
|
||||||
|
raise ValueError("Invalid username.")
|
||||||
|
if self.email and not is_valid_email_address(self.email):
|
||||||
|
raise ValueError("Invalid email.")
|
||||||
|
if self.pubkey and not is_valid_pubkey(self.pubkey):
|
||||||
|
raise ValueError("Invalid pubkey.")
|
||||||
|
user_uuid4 = UUID(hex=self.id, version=4)
|
||||||
|
if user_uuid4.hex != self.id:
|
||||||
|
raise ValueError("User ID is not valid UUID4 hex string.")
|
||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
transaction_count: Optional[int] = 0
|
transaction_count: Optional[int] = 0
|
||||||
@@ -66,7 +79,7 @@ class AccountOverview(Account):
|
|||||||
|
|
||||||
|
|
||||||
class AccountFilters(FilterModel):
|
class AccountFilters(FilterModel):
|
||||||
__search_fields__ = ["id", "email", "username"]
|
__search_fields__ = ["user", "email", "username", "pubkey", "wallet_id"]
|
||||||
__sort_fields__ = [
|
__sort_fields__ = [
|
||||||
"balance_msat",
|
"balance_msat",
|
||||||
"email",
|
"email",
|
||||||
@@ -76,12 +89,11 @@ class AccountFilters(FilterModel):
|
|||||||
"last_payment",
|
"last_payment",
|
||||||
]
|
]
|
||||||
|
|
||||||
id: str
|
|
||||||
last_payment: Optional[datetime] = None
|
|
||||||
transaction_count: Optional[int] = None
|
|
||||||
wallet_count: Optional[int] = None
|
|
||||||
username: Optional[str] = None
|
|
||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
|
user: Optional[str] = None
|
||||||
|
username: Optional[str] = None
|
||||||
|
pubkey: Optional[str] = None
|
||||||
|
wallet_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
@@ -117,13 +129,24 @@ class User(BaseModel):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class CreateUser(BaseModel):
|
class RegisterUser(BaseModel):
|
||||||
email: Optional[str] = Query(default=None)
|
email: Optional[str] = Query(default=None)
|
||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateUser(BaseModel):
|
||||||
|
id: Optional[str] = Query(default=None)
|
||||||
|
email: Optional[str] = Query(default=None)
|
||||||
|
username: Optional[str] = Query(default=None, min_length=2, max_length=20)
|
||||||
|
password: Optional[str] = Query(default=None, min_length=8, max_length=50)
|
||||||
|
password_repeat: Optional[str] = Query(default=None, min_length=8, max_length=50)
|
||||||
|
pubkey: str = Query(default=None, max_length=64)
|
||||||
|
extensions: Optional[list[str]] = None
|
||||||
|
extra: Optional[UserExtra] = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateUser(BaseModel):
|
class UpdateUser(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
email: Optional[str] = Query(default=None)
|
email: Optional[str] = Query(default=None)
|
||||||
|
@@ -20,7 +20,14 @@ from .settings import (
|
|||||||
check_webpush_settings,
|
check_webpush_settings,
|
||||||
update_cached_settings,
|
update_cached_settings,
|
||||||
)
|
)
|
||||||
from .users import check_admin_settings, create_user_account, init_admin_settings
|
from .users import (
|
||||||
|
check_admin_settings,
|
||||||
|
create_user_account,
|
||||||
|
create_user_account_no_ckeck,
|
||||||
|
init_admin_settings,
|
||||||
|
update_user_account,
|
||||||
|
update_user_extensions,
|
||||||
|
)
|
||||||
from .websockets import websocket_manager, websocket_updater
|
from .websockets import websocket_manager, websocket_updater
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -48,7 +55,10 @@ __all__ = [
|
|||||||
# users
|
# users
|
||||||
"check_admin_settings",
|
"check_admin_settings",
|
||||||
"create_user_account",
|
"create_user_account",
|
||||||
|
"create_user_account_no_ckeck",
|
||||||
"init_admin_settings",
|
"init_admin_settings",
|
||||||
|
"update_user_account",
|
||||||
|
"update_user_extensions",
|
||||||
# websockets
|
# websockets
|
||||||
"websocket_manager",
|
"websocket_manager",
|
||||||
"websocket_updater",
|
"websocket_updater",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import UUID, uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -15,13 +15,16 @@ from lnbits.settings import (
|
|||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_account,
|
create_account,
|
||||||
create_admin_settings,
|
create_admin_settings,
|
||||||
|
create_user_extension,
|
||||||
create_wallet,
|
create_wallet,
|
||||||
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,
|
||||||
get_super_settings,
|
get_super_settings,
|
||||||
|
get_user_extensions,
|
||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
|
update_account,
|
||||||
update_super_user,
|
update_super_user,
|
||||||
update_user_extension,
|
update_user_extension,
|
||||||
)
|
)
|
||||||
@@ -39,7 +42,16 @@ async def create_user_account(
|
|||||||
) -> User:
|
) -> User:
|
||||||
if not settings.new_accounts_allowed:
|
if not settings.new_accounts_allowed:
|
||||||
raise ValueError("Account creation is disabled.")
|
raise ValueError("Account creation is disabled.")
|
||||||
|
|
||||||
|
return await create_user_account_no_ckeck(account, wallet_name)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_user_account_no_ckeck(
|
||||||
|
account: Optional[Account] = None, wallet_name: Optional[str] = None
|
||||||
|
) -> User:
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
|
account.validate_fields()
|
||||||
if account.username and await get_account_by_username(account.username):
|
if account.username and await get_account_by_username(account.username):
|
||||||
raise ValueError("Username already exists.")
|
raise ValueError("Username already exists.")
|
||||||
|
|
||||||
@@ -49,10 +61,7 @@ async def create_user_account(
|
|||||||
if account.pubkey and await get_account_by_pubkey(account.pubkey):
|
if account.pubkey and await get_account_by_pubkey(account.pubkey):
|
||||||
raise ValueError("Pubkey already exists.")
|
raise ValueError("Pubkey already exists.")
|
||||||
|
|
||||||
if account.id:
|
if not account.id:
|
||||||
user_uuid4 = UUID(hex=account.id, version=4)
|
|
||||||
assert user_uuid4.hex == account.id, "User ID is not valid UUID4 hex string"
|
|
||||||
else:
|
|
||||||
account.id = uuid4().hex
|
account.id = uuid4().hex
|
||||||
|
|
||||||
account = await create_account(account)
|
account = await create_account(account)
|
||||||
@@ -71,6 +80,58 @@ async def create_user_account(
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def update_user_account(account: Account) -> Account:
|
||||||
|
account.validate_fields()
|
||||||
|
|
||||||
|
existing_account = await get_account(account.id)
|
||||||
|
if not existing_account:
|
||||||
|
raise ValueError("User does not exist.")
|
||||||
|
|
||||||
|
account.password_hash = existing_account.password_hash
|
||||||
|
|
||||||
|
if existing_account.username and not account.username:
|
||||||
|
raise ValueError("Cannot remove username.")
|
||||||
|
|
||||||
|
if account.username:
|
||||||
|
existing_account = await get_account_by_username(account.username)
|
||||||
|
if existing_account and existing_account.id != account.id:
|
||||||
|
raise ValueError("Username already exists.")
|
||||||
|
elif existing_account.username:
|
||||||
|
raise ValueError("Cannot remove username.")
|
||||||
|
|
||||||
|
if account.email:
|
||||||
|
existing_account = await get_account_by_email(account.email)
|
||||||
|
if existing_account and existing_account.id != account.id:
|
||||||
|
raise ValueError("Email already exists.")
|
||||||
|
|
||||||
|
if account.pubkey:
|
||||||
|
existing_account = await get_account_by_pubkey(account.pubkey)
|
||||||
|
if existing_account and existing_account.id != account.id:
|
||||||
|
raise ValueError("Pubkey already exists.")
|
||||||
|
|
||||||
|
return await update_account(account)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_user_extensions(user_id: str, extensions: list[str]):
|
||||||
|
user_extensions = await get_user_extensions(user_id)
|
||||||
|
for user_ext in user_extensions:
|
||||||
|
if user_ext.active:
|
||||||
|
if user_ext.extension not in extensions:
|
||||||
|
user_ext.active = False
|
||||||
|
await update_user_extension(user_ext)
|
||||||
|
else:
|
||||||
|
if user_ext.extension in extensions:
|
||||||
|
user_ext.active = True
|
||||||
|
await update_user_extension(user_ext)
|
||||||
|
|
||||||
|
user_extension_ids = [ue.extension for ue in user_extensions]
|
||||||
|
for ext in extensions:
|
||||||
|
if ext in user_extension_ids:
|
||||||
|
continue
|
||||||
|
user_extension = UserExtension(user=user_id, extension=ext, active=True)
|
||||||
|
await create_user_extension(user_extension)
|
||||||
|
|
||||||
|
|
||||||
async def check_admin_settings():
|
async def check_admin_settings():
|
||||||
if settings.super_user:
|
if settings.super_user:
|
||||||
settings.super_user = to_valid_user_id(settings.super_user).hex
|
settings.super_user = to_valid_user_id(settings.super_user).hex
|
||||||
|
@@ -101,11 +101,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
<payment-list
|
<q-card
|
||||||
:update="updatePayments"
|
:style="
|
||||||
:wallet="this.g.wallet"
|
$q.screen.lt.md
|
||||||
:mobile-simple="mobileSimple"
|
? {
|
||||||
/>
|
background: $q.screen.lt.md ? 'none !important' : '',
|
||||||
|
boxShadow: $q.screen.lt.md ? 'none !important' : '',
|
||||||
|
marginTop: $q.screen.lt.md ? '0px !important' : ''
|
||||||
|
}
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<q-card-section>
|
||||||
|
<payment-list
|
||||||
|
:update="updatePayments"
|
||||||
|
:wallet="this.g.wallet"
|
||||||
|
:mobile-simple="mobileSimple"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
{% if HIDE_API %}
|
{% if HIDE_API %}
|
||||||
<div class="col-12 col-md-4 q-gutter-y-md">
|
<div class="col-12 col-md-4 q-gutter-y-md">
|
||||||
|
@@ -1,23 +0,0 @@
|
|||||||
<q-dialog v-model="createUserDialog.show">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<p>Create User</p>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-form @submit="createUser">
|
|
||||||
<lnbits-dynamic-fields
|
|
||||||
:options="createUserDialog.fields"
|
|
||||||
v-model="createUserDialog.data"
|
|
||||||
></lnbits-dynamic-fields>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn v-close-popup unelevated color="primary" type="submit"
|
|
||||||
>Create</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
@@ -1,22 +1,43 @@
|
|||||||
<q-dialog v-model="createWalletDialog.show">
|
<q-dialog v-model="createWalletDialog.show" position="top">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||||
<p>Create Wallet</p>
|
<strong>Create Wallet</strong>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-form @submit="createWallet">
|
<div class="row q-mt-lg">
|
||||||
<lnbits-dynamic-fields
|
<div class="col">
|
||||||
:options="createWalletDialog.fields"
|
<q-input
|
||||||
v-model="createUserDialog.data"
|
v-model="createWalletDialog.data.name"
|
||||||
></lnbits-dynamic-fields>
|
:label='$t("name_your_wallet")'
|
||||||
<div class="row q-mt-lg">
|
filled
|
||||||
<q-btn v-close-popup unelevated color="primary" type="submit"
|
dense
|
||||||
>Create</q-btn
|
class="q-mb-md"
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
>
|
||||||
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<div class="col">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="createWalletDialog.data.currency"
|
||||||
|
:options="{{ currencies | safe }}"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
@click="createWallet()"
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>Create</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
187
lnbits/core/templates/users/_manageUser.html
Normal file
187
lnbits/core/templates/users/_manageUser.html
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<div class="row q-mb-lg">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
icon="arrow_back_ios"
|
||||||
|
@click="backToUsersPage()"
|
||||||
|
:label="$t('back')"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-if="activeUser.data.id"
|
||||||
|
@click="updateUser()"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('update_account')"
|
||||||
|
class="q-ml-md"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
@click="createUser()"
|
||||||
|
:label="$t('create_account')"
|
||||||
|
color="primary"
|
||||||
|
class="float-right"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-card v-if="activeUser.show" class="q-pa-md">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">
|
||||||
|
<span v-if="activeUser.data.id" v-text="$t('update_account')"></span>
|
||||||
|
<span v-else v-text="$t('create_account')"></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
v-if="activeUser.data.id"
|
||||||
|
v-model="activeUser.data.id"
|
||||||
|
:label="$t('user_id')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
:type="activeUser.data.showUserId ? 'text': 'password'"
|
||||||
|
class="q-mb-md"
|
||||||
|
><q-btn
|
||||||
|
@click="activeUser.data.showUserId = !activeUser.data.showUserId"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
:icon="activeUser.data.showUserId ? 'visibility_off' : 'visibility'"
|
||||||
|
color="grey"
|
||||||
|
></q-btn>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
v-model="activeUser.data.username"
|
||||||
|
:label="$t('username')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-toggle
|
||||||
|
size="xs"
|
||||||
|
v-if="!activeUser.data.id"
|
||||||
|
color="secondary"
|
||||||
|
:label="$t('set_password')"
|
||||||
|
v-model="activeUser.setPassword"
|
||||||
|
>
|
||||||
|
<q-tooltip>Toggle Admin</q-tooltip>
|
||||||
|
</q-toggle>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-if="activeUser.setPassword"
|
||||||
|
v-model="activeUser.data.password"
|
||||||
|
:type="activeUser.data.showPassword ? 'text': 'password'"
|
||||||
|
autocomplete="off"
|
||||||
|
:label="$t('password')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
@click="activeUser.data.showPassword = !activeUser.data.showPassword"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
:icon="activeUser.data.showPassword ? 'visibility_off' : 'visibility'"
|
||||||
|
color="grey"
|
||||||
|
></q-btn>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="activeUser.setPassword"
|
||||||
|
v-model="activeUser.data.password_repeat"
|
||||||
|
:type="activeUser.data.showPassword ? 'text': 'password'"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
:label="$t('password_repeat')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="q-mb-md"
|
||||||
|
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
@click="activeUser.data.showPassword = !activeUser.data.showPassword"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
:icon="activeUser.data.showPassword ? 'visibility_off' : 'visibility'"
|
||||||
|
color="grey"
|
||||||
|
></q-btn>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="activeUser.data.pubkey"
|
||||||
|
:label="$t('pubkey')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
v-model="activeUser.data.email"
|
||||||
|
:label="$t('email')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-if="activeUser.data.extra">
|
||||||
|
<q-input
|
||||||
|
v-model="activeUser.data.extra.first_name"
|
||||||
|
:label="$t('first_name')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
v-model="activeUser.data.extra.last_name"
|
||||||
|
:label="$t('last_name')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
v-model="activeUser.data.extra.provider"
|
||||||
|
:label="$t('auth_provider')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
v-model="activeUser.data.extra.picture"
|
||||||
|
:label="$t('picture')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="activeUser.data.extensions"
|
||||||
|
multiple
|
||||||
|
label="User extensions"
|
||||||
|
:options="g.extensions"
|
||||||
|
></q-select>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-if="activeUser.data.id">
|
||||||
|
<q-btn
|
||||||
|
@click="resetPassword(activeUser.data.id)"
|
||||||
|
:disable="activeUser.data.is_super_user"
|
||||||
|
:label="$t('reset_password')"
|
||||||
|
icon="refresh"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<q-tooltip>Generate and copy password reset url</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
@click="deleteUser(activeUser.data.id)"
|
||||||
|
:disable="activeUser.data.is_super_user"
|
||||||
|
:label="$t('delete')"
|
||||||
|
icon="delete"
|
||||||
|
color="negative"
|
||||||
|
class="float-right"
|
||||||
|
>
|
||||||
|
<q-tooltip>Delete User</q-tooltip></q-btn
|
||||||
|
>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
@@ -1,11 +1,37 @@
|
|||||||
<q-dialog v-model="walletDialog.show">
|
<div v-if="paymentPage.show">
|
||||||
<q-card class="q-pa-lg" style="width: 700px; max-width: 80vw">
|
<div class="row q-mb-lg">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
icon="arrow_back_ios"
|
||||||
|
@click="paymentPage.show = false"
|
||||||
|
:label="$t('back')"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-card class="q-pa-md">
|
||||||
|
<q-card-section>
|
||||||
|
<payment-list :wallet="paymentsWallet" />
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="activeWallet.show">
|
||||||
|
<div class="row q-mb-lg">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
icon="arrow_back_ios"
|
||||||
|
@click="backToUsersPage()"
|
||||||
|
:label="$t('back')"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
@click="createWalletDialog.show = true"
|
||||||
|
:label="$t('create_new_wallet')"
|
||||||
|
color="primary"
|
||||||
|
class="q-ml-md"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-card class="q-pa-md">
|
||||||
<h2 class="text-h6 q-mb-md">Wallets</h2>
|
<h2 class="text-h6 q-mb-md">Wallets</h2>
|
||||||
<q-dialog v-model="paymentDialog.show">
|
|
||||||
<q-card class="q-pa-lg" style="width: 700px; max-width: 80vw">
|
|
||||||
<payment-list :wallet="activeWallet" />
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<q-table :rows="wallets" :columns="walletTable.columns">
|
<q-table :rows="wallets" :columns="walletTable.columns">
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
@@ -22,6 +48,13 @@
|
|||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td auto-width>
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
:label="$t('topup')"
|
||||||
|
@click="showTopupDialog(props.row.id)"
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
class="q-mr-md"
|
||||||
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
icon="menu"
|
icon="menu"
|
||||||
@@ -31,23 +64,7 @@
|
|||||||
>
|
>
|
||||||
<q-tooltip>Show Payments</q-tooltip>
|
<q-tooltip>Show Payments</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
|
||||||
v-if="!props.row.deleted"
|
|
||||||
round
|
|
||||||
icon="content_copy"
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
class="q-ml-xs"
|
|
||||||
@click="copyText(props.row.id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy Wallet ID</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<lnbits-update-balance
|
|
||||||
v-if="!props.row.deleted"
|
|
||||||
:wallet_id="props.row.id"
|
|
||||||
:callback="topupCallback"
|
|
||||||
class="q-ml-xs"
|
|
||||||
></lnbits-update-balance>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
v-if="!props.row.deleted"
|
v-if="!props.row.deleted"
|
||||||
@@ -70,17 +87,7 @@
|
|||||||
>
|
>
|
||||||
<q-tooltip>Copy Invoice Key</q-tooltip>
|
<q-tooltip>Copy Invoice Key</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
v-if="props.row.deleted"
|
|
||||||
icon="toggle_off"
|
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
class="q-ml-xs"
|
|
||||||
@click="undeleteUserWallet(props.row.user, props.row.id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Undelete Wallet</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
icon="delete"
|
icon="delete"
|
||||||
@@ -92,21 +99,51 @@
|
|||||||
<q-tooltip>Delete Wallet</q-tooltip>
|
<q-tooltip>Delete Wallet</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td auto-width v-text="props.row.name"></q-td>
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
icon="link"
|
||||||
|
size="sm"
|
||||||
|
flat
|
||||||
|
class="cursor-pointer q-mr-xs"
|
||||||
|
@click="copyWalletLink(props.row.id)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Copy Wallet Link</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<span v-text="props.row.name"></span>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
v-if="props.row.deleted"
|
||||||
|
icon="toggle_off"
|
||||||
|
size="sm"
|
||||||
|
color="secondary"
|
||||||
|
class="q-ml-xs"
|
||||||
|
@click="undeleteUserWallet(props.row.user, props.row.id)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Undelete Wallet</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
icon="content_copy"
|
||||||
|
size="sm"
|
||||||
|
flat
|
||||||
|
class="cursor-pointer q-mr-xs"
|
||||||
|
@click="copyText(props.row.id)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Copy Wallet ID</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-text="props.row.id"
|
||||||
|
:class="props.row.deleted ? 'text-strike' : ''"
|
||||||
|
></span>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
<q-td auto-width v-text="props.row.currency"></q-td>
|
<q-td auto-width v-text="props.row.currency"></q-td>
|
||||||
<q-td auto-width v-text="formatSat(props.row.balance_msat)"></q-td>
|
<q-td auto-width v-text="formatSat(props.row.balance_msat)"></q-td>
|
||||||
<q-td auto-width v-text="props.row.deleted"></q-td>
|
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
:label="$t('close')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</div>
|
@@ -1,28 +1,25 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block page %} {% include "users/_walletDialog.html" %} {% include
|
%} {% block page %}{% include "users/_topupDialog.html" %}
|
||||||
"users/_topupDialog.html" %} {% include "users/_createUserDialog.html" %} {%
|
|
||||||
include "users/_createWalletDialog.html" %}
|
|
||||||
|
|
||||||
<h3 class="text-subtitle q-my-none" v-text="$t('users')"></h3>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
<div class="row q-col-gutter-md justify-center">
|
||||||
<div class="col q-gutter-y-md" style="width: 300px">
|
<div class="col">
|
||||||
<div style="width: 100%; max-width: 2000px">
|
{% include "users/_manageWallet.html" %}
|
||||||
<canvas ref="chart1"></canvas>
|
<div v-if="activeUser.show" class="row">
|
||||||
|
<div class="col-12 col-md-6">{%include "users/_manageUser.html" %}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else-if="activeWallet.show">
|
||||||
</div>
|
{%include "users/_createWalletDialog.html" %}
|
||||||
<div class="row q-col-gutter-md justify-center">
|
</div>
|
||||||
<div class="col q-gutter-y-md">
|
<div v-else>
|
||||||
<q-card>
|
<q-btn
|
||||||
<q-card-section>
|
@click="showAccountPage()"
|
||||||
<div class="row items-center no-wrap q-mb-sm">
|
:label="$t('create_account')"
|
||||||
<q-btn :label="$t('topup')" @click="topupDialog.show = true">
|
color="primary"
|
||||||
<q-tooltip
|
class="q-mb-md"
|
||||||
>{%raw%}{{ $t('add_funds_tooltip') }}{%endraw%}</q-tooltip
|
>
|
||||||
>
|
</q-btn>
|
||||||
</q-btn>
|
|
||||||
</div>
|
<q-card class="q-pa-md">
|
||||||
<q-table
|
<q-table
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:rows="users"
|
:rows="users"
|
||||||
@@ -36,94 +33,118 @@ include "users/_createWalletDialog.html" %}
|
|||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
v-for="col in props.cols"
|
<q-input
|
||||||
v-text="col.label"
|
v-if="['user', 'username', 'email', 'pubkey', 'wallet_id'].includes(col.name)"
|
||||||
:key="col.name"
|
v-model="searchData[col.name]"
|
||||||
:props="props"
|
@keydown.enter="searchUserBy(col.name)"
|
||||||
></q-th>
|
dense
|
||||||
|
type="text"
|
||||||
|
filled
|
||||||
|
:label="col.label"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon
|
||||||
|
name="search"
|
||||||
|
@click="searchUserBy(col.name)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<span v-else v-text="col.label"></span>
|
||||||
|
</q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr auto-width :props="props">
|
<q-tr auto-width :props="props">
|
||||||
<q-td>
|
<q-td>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
@click="showAccountPage(props.row.id)"
|
||||||
round
|
round
|
||||||
icon="list"
|
icon="edit"
|
||||||
size="sm"
|
size="sm"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@click="fetchWallets(props.row.id)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Show Wallets</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="content_copy"
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
class="q-ml-xs"
|
class="q-ml-xs"
|
||||||
@click="copyText(props.row.id)"
|
|
||||||
>
|
>
|
||||||
<q-tooltip>Copy User ID</q-tooltip>
|
<q-tooltip>
|
||||||
|
<span v-text="$t('update_account')"></span>
|
||||||
|
</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
</q-td>
|
||||||
round
|
<q-td>
|
||||||
|
<q-toggle
|
||||||
|
size="xs"
|
||||||
v-if="!props.row.is_super_user"
|
v-if="!props.row.is_super_user"
|
||||||
icon="build"
|
color="secondary"
|
||||||
size="sm"
|
v-model="props.row.is_admin"
|
||||||
:color="props.row.is_admin ? 'primary' : 'grey'"
|
@update:model-value="toggleAdmin(props.row.id)"
|
||||||
class="q-ml-xs"
|
|
||||||
@click="toggleAdmin(props.row.id)"
|
|
||||||
>
|
>
|
||||||
<q-tooltip>Toggle Admin</q-tooltip>
|
<q-tooltip>Toggle Admin</q-tooltip>
|
||||||
</q-btn>
|
</q-toggle>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
v-if="props.row.is_super_user"
|
v-if="props.row.is_super_user"
|
||||||
icon="build"
|
icon="verified"
|
||||||
size="sm"
|
size="sm"
|
||||||
color="positive"
|
color="secondary"
|
||||||
class="q-ml-xs"
|
class="q-ml-xs"
|
||||||
>
|
>
|
||||||
<q-tooltip>Super User</q-tooltip>
|
<q-tooltip>Super User</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
icon="list"
|
||||||
icon="refresh"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@click="resetPassword(props.row.id)"
|
:label="props.row.wallet_count"
|
||||||
|
@click="fetchWallets(props.row.id)"
|
||||||
>
|
>
|
||||||
<q-tooltip>Generate and copy password reset url</q-tooltip>
|
<q-tooltip>Show Wallets</q-tooltip>
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="delete"
|
|
||||||
size="sm"
|
|
||||||
color="negative"
|
|
||||||
class="q-ml-xs"
|
|
||||||
@click="deleteUser(props.row.id, props)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Delete User</q-tooltip>
|
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td
|
|
||||||
auto-width
|
<q-td>
|
||||||
v-text="formatSat(props.row.balance_msat)"
|
<q-btn
|
||||||
></q-td>
|
icon="content_copy"
|
||||||
<q-td auto-width v-text="props.row.wallet_count"></q-td>
|
size="sm"
|
||||||
<q-td auto-width v-text="props.row.transaction_count"></q-td>
|
flat
|
||||||
<q-td auto-width v-text="props.row.username"></q-td>
|
class="cursor-pointer q-mr-xs"
|
||||||
<q-td auto-width v-text="props.row.email"></q-td>
|
@click="copyText(props.row.id)"
|
||||||
<q-td
|
>
|
||||||
auto-width
|
<q-tooltip>Copy User ID</q-tooltip>
|
||||||
v-text="formatDate(props.row.last_payment)"
|
</q-btn>
|
||||||
></q-td>
|
<span v-text="shortify(props.row.id)"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-text="props.row.username"></q-td>
|
||||||
|
|
||||||
|
<q-td v-text="props.row.email"></q-td>
|
||||||
|
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
v-if="props.row.pubkey"
|
||||||
|
icon="content_copy"
|
||||||
|
size="sm"
|
||||||
|
flat
|
||||||
|
class="cursor-pointer q-mr-xs"
|
||||||
|
@click="copyText(props.row.pubkey)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Copy Public Key</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<span v-text="shortify(props.row.pubkey)"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-text="formatSat(props.row.balance_msat)"></q-td>
|
||||||
|
|
||||||
|
<q-td v-text="props.row.transaction_count"></q-td>
|
||||||
|
|
||||||
|
<q-td v-text="formatDate(props.row.last_payment)"></q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
</q-card-section>
|
</q-card>
|
||||||
</q-card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -35,9 +35,9 @@ from ..crud import (
|
|||||||
from ..models import (
|
from ..models import (
|
||||||
AccessTokenPayload,
|
AccessTokenPayload,
|
||||||
Account,
|
Account,
|
||||||
CreateUser,
|
|
||||||
LoginUsernamePassword,
|
LoginUsernamePassword,
|
||||||
LoginUsr,
|
LoginUsr,
|
||||||
|
RegisterUser,
|
||||||
ResetUserPassword,
|
ResetUserPassword,
|
||||||
UpdateSuperuserPassword,
|
UpdateSuperuserPassword,
|
||||||
UpdateUser,
|
UpdateUser,
|
||||||
@@ -145,7 +145,7 @@ async def logout() -> JSONResponse:
|
|||||||
|
|
||||||
|
|
||||||
@auth_router.post("/register")
|
@auth_router.post("/register")
|
||||||
async def register(data: CreateUser) -> JSONResponse:
|
async def register(data: RegisterUser) -> 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(
|
||||||
HTTPStatus.UNAUTHORIZED,
|
HTTPStatus.UNAUTHORIZED,
|
||||||
|
@@ -2,39 +2,59 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
import shortuuid
|
||||||
|
from fastapi import APIRouter, Body, Depends
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
from lnbits.core.crud import (
|
||||||
|
create_wallet,
|
||||||
delete_account,
|
delete_account,
|
||||||
delete_wallet,
|
delete_wallet,
|
||||||
force_delete_wallet,
|
force_delete_wallet,
|
||||||
get_accounts,
|
get_accounts,
|
||||||
|
get_user,
|
||||||
get_wallet,
|
get_wallet,
|
||||||
get_wallets,
|
get_wallets,
|
||||||
update_admin_settings,
|
update_admin_settings,
|
||||||
|
update_wallet,
|
||||||
)
|
)
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccountFilters,
|
AccountFilters,
|
||||||
AccountOverview,
|
AccountOverview,
|
||||||
CreateTopup,
|
CreateTopup,
|
||||||
|
CreateUser,
|
||||||
|
SimpleStatus,
|
||||||
User,
|
User,
|
||||||
|
UserExtra,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from lnbits.core.services import update_wallet_balance
|
from lnbits.core.models.users import Account
|
||||||
|
from lnbits.core.services import (
|
||||||
|
create_user_account_no_ckeck,
|
||||||
|
update_user_account,
|
||||||
|
update_user_extensions,
|
||||||
|
update_wallet_balance,
|
||||||
|
)
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
||||||
from lnbits.helpers import encrypt_internal_message, generate_filter_params_openapi
|
from lnbits.helpers import (
|
||||||
|
encrypt_internal_message,
|
||||||
|
generate_filter_params_openapi,
|
||||||
|
)
|
||||||
from lnbits.settings import EditableSettings, settings
|
from lnbits.settings import EditableSettings, settings
|
||||||
|
from lnbits.utils.exchange_rates import allowed_currencies
|
||||||
|
|
||||||
users_router = APIRouter(prefix="/users/api/v1", dependencies=[Depends(check_admin)])
|
users_router = APIRouter(
|
||||||
|
prefix="/users/api/v1", dependencies=[Depends(check_admin)], tags=["Users"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@users_router.get(
|
@users_router.get(
|
||||||
"/user",
|
"/user",
|
||||||
name="get accounts",
|
name="Get accounts",
|
||||||
summary="Get paginated list of accounts",
|
summary="Get paginated list of accounts",
|
||||||
openapi_extra=generate_filter_params_openapi(AccountFilters),
|
openapi_extra=generate_filter_params_openapi(AccountFilters),
|
||||||
)
|
)
|
||||||
@@ -44,10 +64,80 @@ async def api_get_users(
|
|||||||
return await get_accounts(filters=filters)
|
return await get_accounts(filters=filters)
|
||||||
|
|
||||||
|
|
||||||
@users_router.delete("/user/{user_id}", status_code=HTTPStatus.OK)
|
@users_router.get(
|
||||||
|
"/user/{user_id}",
|
||||||
|
name="Get user",
|
||||||
|
summary="Get user by Id",
|
||||||
|
)
|
||||||
|
async def api_get_user(user_id: str) -> User:
|
||||||
|
user = await get_user(user_id)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@users_router.post("/user", name="Create user")
|
||||||
|
async def api_create_user(data: CreateUser) -> CreateUser:
|
||||||
|
if not data.username and data.password:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST, "Username required when password provided."
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.password != data.password_repeat:
|
||||||
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Passwords do not match.")
|
||||||
|
|
||||||
|
if not data.password:
|
||||||
|
random_password = shortuuid.uuid()
|
||||||
|
data.password = random_password
|
||||||
|
data.password_repeat = random_password
|
||||||
|
data.extra = data.extra or UserExtra()
|
||||||
|
data.extra.provider = data.extra.provider or "lnbits"
|
||||||
|
|
||||||
|
account = Account(
|
||||||
|
id=uuid4().hex,
|
||||||
|
username=data.username,
|
||||||
|
email=data.email,
|
||||||
|
pubkey=data.pubkey,
|
||||||
|
extra=data.extra,
|
||||||
|
)
|
||||||
|
account.validate_fields()
|
||||||
|
account.hash_password(data.password)
|
||||||
|
user = await create_user_account_no_ckeck(account)
|
||||||
|
data.id = user.id
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@users_router.put("/user/{user_id}", name="Update user")
|
||||||
|
async def api_update_user(user_id: str, data: CreateUser) -> CreateUser:
|
||||||
|
if user_id != data.id:
|
||||||
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
|
||||||
|
|
||||||
|
if data.password or data.password_repeat:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
|
||||||
|
)
|
||||||
|
|
||||||
|
account = Account(
|
||||||
|
id=user_id,
|
||||||
|
username=data.username,
|
||||||
|
email=data.email,
|
||||||
|
pubkey=data.pubkey,
|
||||||
|
extra=data.extra or UserExtra(),
|
||||||
|
)
|
||||||
|
await update_user_account(account)
|
||||||
|
|
||||||
|
await update_user_extensions(user_id, data.extensions or [])
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@users_router.delete(
|
||||||
|
"/user/{user_id}",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
name="Delete user by Id",
|
||||||
|
)
|
||||||
async def api_users_delete_user(
|
async def api_users_delete_user(
|
||||||
user_id: str, user: User = Depends(check_admin)
|
user_id: str, user: User = Depends(check_admin)
|
||||||
) -> None:
|
) -> SimpleStatus:
|
||||||
wallets = await get_wallets(user_id)
|
wallets = await get_wallets(user_id)
|
||||||
if len(wallets) > 0:
|
if len(wallets) > 0:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -67,10 +157,13 @@ async def api_users_delete_user(
|
|||||||
detail="Only super_user can delete admin user.",
|
detail="Only super_user can delete admin user.",
|
||||||
)
|
)
|
||||||
await delete_account(user_id)
|
await delete_account(user_id)
|
||||||
|
return SimpleStatus(success=True, message="User deleted.")
|
||||||
|
|
||||||
|
|
||||||
@users_router.put(
|
@users_router.put(
|
||||||
"/user/{user_id}/reset_password", dependencies=[Depends(check_super_user)]
|
"/user/{user_id}/reset_password",
|
||||||
|
dependencies=[Depends(check_super_user)],
|
||||||
|
name="Reset user password",
|
||||||
)
|
)
|
||||||
async def api_users_reset_password(user_id: str) -> str:
|
async def api_users_reset_password(user_id: str) -> str:
|
||||||
if user_id == settings.super_user:
|
if user_id == settings.super_user:
|
||||||
@@ -87,28 +180,54 @@ 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("/user/{user_id}/admin", dependencies=[Depends(check_super_user)])
|
@users_router.get(
|
||||||
async def api_users_toggle_admin(user_id: str) -> None:
|
"/user/{user_id}/admin",
|
||||||
|
dependencies=[Depends(check_super_user)],
|
||||||
|
name="Give or revoke admin permsisions to a user",
|
||||||
|
)
|
||||||
|
async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
||||||
if user_id == settings.super_user:
|
if user_id == settings.super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="Cannot change super user.",
|
detail="Cannot change super user.",
|
||||||
)
|
)
|
||||||
if user_id in settings.lnbits_admin_users:
|
|
||||||
|
if settings.is_admin_user(user_id):
|
||||||
settings.lnbits_admin_users.remove(user_id)
|
settings.lnbits_admin_users.remove(user_id)
|
||||||
else:
|
else:
|
||||||
settings.lnbits_admin_users.append(user_id)
|
settings.lnbits_admin_users.append(user_id)
|
||||||
update_settings = EditableSettings(lnbits_admin_users=settings.lnbits_admin_users)
|
update_settings = EditableSettings(lnbits_admin_users=settings.lnbits_admin_users)
|
||||||
await update_admin_settings(update_settings)
|
await update_admin_settings(update_settings)
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True, message=f"User admin: '{settings.is_admin_user(user_id)}'."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@users_router.get("/user/{user_id}/wallet")
|
@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)
|
return await get_wallets(user_id)
|
||||||
|
|
||||||
|
|
||||||
@users_router.get("/user/{user_id}/wallet/{wallet}/undelete")
|
@users_router.post("/user/{user_id}/wallet", name="Create a new wallet for user")
|
||||||
async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> None:
|
async def api_users_create_user_wallet(
|
||||||
|
user_id: str, name: Optional[str] = Body(None), currency: Optional[str] = Body(None)
|
||||||
|
):
|
||||||
|
if currency and currency not in allowed_currencies():
|
||||||
|
raise ValueError(f"Currency '{currency}' not allowed.")
|
||||||
|
|
||||||
|
wallet = await create_wallet(user_id=user_id, wallet_name=name)
|
||||||
|
|
||||||
|
if currency:
|
||||||
|
wallet.currency = currency
|
||||||
|
await update_wallet(wallet)
|
||||||
|
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
|
@users_router.put(
|
||||||
|
"/user/{user_id}/wallet/{wallet}/undelete", name="Reactivate deleted wallet"
|
||||||
|
)
|
||||||
|
async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
|
||||||
wal = await get_wallet(wallet)
|
wal = await get_wallet(wallet)
|
||||||
if not wal:
|
if not wal:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -123,10 +242,18 @@ async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> None:
|
|||||||
)
|
)
|
||||||
if wal.deleted:
|
if wal.deleted:
|
||||||
await delete_wallet(user_id=user_id, wallet_id=wallet, deleted=False)
|
await delete_wallet(user_id=user_id, wallet_id=wallet, deleted=False)
|
||||||
|
return SimpleStatus(success=True, message="Wallet undeleted.")
|
||||||
|
|
||||||
|
return SimpleStatus(success=True, message="Wallet is already active.")
|
||||||
|
|
||||||
|
|
||||||
@users_router.delete("/user/{user_id}/wallet/{wallet}")
|
@users_router.delete(
|
||||||
async def api_users_delete_user_wallet(user_id: str, wallet: str) -> None:
|
"/user/{user_id}/wallet/{wallet}",
|
||||||
|
name="Delete wallet by id",
|
||||||
|
summary="First time it is called it does a soft delete (only sets a flag)."
|
||||||
|
"The second time it is called will delete the entry from the DB",
|
||||||
|
)
|
||||||
|
async def api_users_delete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
|
||||||
wal = await get_wallet(wallet)
|
wal = await get_wallet(wallet)
|
||||||
if not wal:
|
if not wal:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -136,18 +263,20 @@ async def api_users_delete_user_wallet(user_id: str, wallet: str) -> None:
|
|||||||
if wal.deleted:
|
if wal.deleted:
|
||||||
await force_delete_wallet(wallet)
|
await force_delete_wallet(wallet)
|
||||||
await delete_wallet(user_id=user_id, wallet_id=wallet)
|
await delete_wallet(user_id=user_id, wallet_id=wallet)
|
||||||
|
return SimpleStatus(success=True, message="Wallet deleted.")
|
||||||
|
|
||||||
|
|
||||||
@users_router.put(
|
@users_router.put(
|
||||||
"/topup",
|
"/topup",
|
||||||
name="Topup",
|
name="Topup",
|
||||||
|
summary="Update balance for a particular wallet.",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
dependencies=[Depends(check_super_user)],
|
dependencies=[Depends(check_super_user)],
|
||||||
)
|
)
|
||||||
async def api_topup_balance(data: CreateTopup) -> dict[str, str]:
|
async def api_topup_balance(data: CreateTopup) -> SimpleStatus:
|
||||||
await get_wallet(data.id)
|
await get_wallet(data.id)
|
||||||
if settings.lnbits_backend_wallet_class == "VoidWallet":
|
if settings.lnbits_backend_wallet_class == "VoidWallet":
|
||||||
raise Exception("VoidWallet active")
|
raise Exception("VoidWallet active")
|
||||||
|
|
||||||
await update_wallet_balance(wallet_id=data.id, amount=int(data.amount))
|
await update_wallet_balance(wallet_id=data.id, amount=int(data.amount))
|
||||||
return {"status": "Success"}
|
return SimpleStatus(success=True, message="Balance updated.")
|
||||||
|
@@ -647,7 +647,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
|||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
if key not in model.__fields__:
|
if key not in model.__fields__:
|
||||||
logger.warning(f"Converting {key} to model `{model}`.")
|
# Somethimes an SQL JOIN will create additional column
|
||||||
continue
|
continue
|
||||||
type_ = model.__fields__[key].type_
|
type_ = model.__fields__[key].type_
|
||||||
outertype_ = model.__fields__[key].outer_type_
|
outertype_ = model.__fields__[key].outer_type_
|
||||||
@@ -678,4 +678,6 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
|||||||
_dict[key] = value
|
_dict[key] = value
|
||||||
continue
|
continue
|
||||||
_model = model.construct(**_dict)
|
_model = model.construct(**_dict)
|
||||||
|
if isinstance(_model, BaseModel):
|
||||||
|
_model.__init__(**_dict) # type: ignore
|
||||||
return _model
|
return _model
|
||||||
|
@@ -186,6 +186,16 @@ def is_valid_username(username: str) -> bool:
|
|||||||
return re.fullmatch(username_regex, username) is not None
|
return re.fullmatch(username_regex, username) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_pubkey(pubkey: str) -> bool:
|
||||||
|
if len(pubkey) != 64:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
int(pubkey, 16)
|
||||||
|
return True
|
||||||
|
except Exception as _:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def create_access_token(data: dict):
|
def create_access_token(data: dict):
|
||||||
expire = datetime.now(timezone.utc) + timedelta(
|
expire = datetime.now(timezone.utc) + timedelta(
|
||||||
minutes=settings.auth_token_expire_minutes
|
minutes=settings.auth_token_expire_minutes
|
||||||
|
@@ -673,8 +673,11 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
|||||||
or user_id == self.super_user
|
or user_id == self.super_user
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_super_user(self, user_id: str) -> bool:
|
||||||
|
return user_id == self.super_user
|
||||||
|
|
||||||
def is_admin_user(self, user_id: str) -> bool:
|
def is_admin_user(self, user_id: str) -> bool:
|
||||||
return user_id in self.lnbits_admin_users or user_id == self.super_user
|
return self.is_super_user(user_id) or user_id in self.lnbits_admin_users
|
||||||
|
|
||||||
def is_admin_extension(self, ext_id: str) -> bool:
|
def is_admin_extension(self, ext_id: str) -> bool:
|
||||||
return ext_id in self.lnbits_admin_extensions
|
return ext_id in self.lnbits_admin_extensions
|
||||||
|
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -122,6 +122,7 @@ window.localisation.en = {
|
|||||||
pay_to_enable: 'Pay To Enable',
|
pay_to_enable: 'Pay To Enable',
|
||||||
enable_extension_details: 'Enable extension for current user',
|
enable_extension_details: 'Enable extension for current user',
|
||||||
disable: 'Disable',
|
disable: 'Disable',
|
||||||
|
delete: 'Delete',
|
||||||
installed: 'Installed',
|
installed: 'Installed',
|
||||||
activated: 'Activated',
|
activated: 'Activated',
|
||||||
deactivated: 'Deactivated',
|
deactivated: 'Deactivated',
|
||||||
|
@@ -3,60 +3,35 @@ window.app = Vue.createApp({
|
|||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
isSuperUser: false,
|
paymentsWallet: {},
|
||||||
activeWallet: {},
|
|
||||||
wallet: {},
|
wallet: {},
|
||||||
cancel: {},
|
cancel: {},
|
||||||
users: [],
|
users: [],
|
||||||
wallets: [],
|
wallets: [],
|
||||||
paymentDialog: {
|
searchData: {
|
||||||
|
user: '',
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
pubkey: ''
|
||||||
|
},
|
||||||
|
paymentPage: {
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
walletDialog: {
|
activeWallet: {
|
||||||
|
userId: null,
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
topupDialog: {
|
topupDialog: {
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
createUserDialog: {
|
activeUser: {
|
||||||
data: {},
|
data: null,
|
||||||
fields: [
|
showUserId: false,
|
||||||
{
|
|
||||||
description: 'Username',
|
|
||||||
name: 'username'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'Email',
|
|
||||||
name: 'email'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'password',
|
|
||||||
description: 'Password',
|
|
||||||
name: 'password'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
|
|
||||||
createWalletDialog: {
|
createWalletDialog: {
|
||||||
data: {},
|
data: {},
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
type: 'str',
|
|
||||||
description: 'Wallet Name',
|
|
||||||
name: 'name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
values: ['', 'EUR', 'USD'],
|
|
||||||
description: 'Currency',
|
|
||||||
name: 'currency'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'str',
|
|
||||||
description: 'Balance',
|
|
||||||
name: 'balance'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
walletTable: {
|
walletTable: {
|
||||||
@@ -67,6 +42,12 @@ window.app = Vue.createApp({
|
|||||||
label: 'Name',
|
label: 'Name',
|
||||||
field: 'name'
|
field: 'name'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Wallet Id',
|
||||||
|
field: 'id'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'currency',
|
name: 'currency',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@@ -78,17 +59,65 @@ window.app = Vue.createApp({
|
|||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Balance',
|
label: 'Balance',
|
||||||
field: 'balance_msat'
|
field: 'balance_msat'
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'deleted',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Deleted',
|
|
||||||
field: 'deleted'
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'name',
|
||||||
|
rowsPerPage: 10,
|
||||||
|
page: 1,
|
||||||
|
descending: true,
|
||||||
|
rowsNumber: 10
|
||||||
|
},
|
||||||
|
search: null,
|
||||||
|
hideEmpty: true,
|
||||||
|
loading: false
|
||||||
},
|
},
|
||||||
usersTable: {
|
usersTable: {
|
||||||
columns: [
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'admin',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Admin',
|
||||||
|
field: 'admin',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wallet_id',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Wallets',
|
||||||
|
field: 'wallet_id',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'user',
|
||||||
|
align: 'left',
|
||||||
|
label: 'User Id',
|
||||||
|
field: 'user',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'username',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Username',
|
||||||
|
field: 'username',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Email',
|
||||||
|
field: 'email',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pubkey',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Public Key',
|
||||||
|
field: 'pubkey',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'balance_msat',
|
name: 'balance_msat',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@@ -96,34 +125,15 @@ window.app = Vue.createApp({
|
|||||||
field: 'balance_msat',
|
field: 'balance_msat',
|
||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'wallet_count',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Wallet Count',
|
|
||||||
field: 'wallet_count',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'transaction_count',
|
name: 'transaction_count',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Transaction Count',
|
label: 'Payments',
|
||||||
field: 'transaction_count',
|
field: 'transaction_count',
|
||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'username',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Username',
|
|
||||||
field: 'username',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'email',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Email',
|
|
||||||
field: 'email',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'last_payment',
|
name: 'last_payment',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@@ -160,50 +170,7 @@ window.app = Vue.createApp({
|
|||||||
created() {
|
created() {
|
||||||
this.fetchUsers()
|
this.fetchUsers()
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
this.chart1 = new Chart(this.$refs.chart1.getContext('2d'), {
|
|
||||||
type: 'bubble',
|
|
||||||
options: {
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'linear',
|
|
||||||
beginAtZero: true,
|
|
||||||
title: {
|
|
||||||
text: 'Transaction count'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
type: 'linear',
|
|
||||||
beginAtZero: true,
|
|
||||||
title: {
|
|
||||||
text: 'User balance in million sats'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
label: function (tooltipItem, data) {
|
|
||||||
const dataset = data.datasets[tooltipItem.datasetIndex]
|
|
||||||
const dataPoint = dataset.data[tooltipItem.index]
|
|
||||||
return dataPoint.customLabel || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
padding: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: 'Wallet balance vs transaction count',
|
|
||||||
backgroundColor: 'rgb(255, 99, 132)',
|
|
||||||
data: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
formatDate: function (value) {
|
formatDate: function (value) {
|
||||||
return LNbits.utils.formatDate(value)
|
return LNbits.utils.formatDate(value)
|
||||||
@@ -211,17 +178,24 @@ window.app = Vue.createApp({
|
|||||||
formatSat: function (value) {
|
formatSat: function (value) {
|
||||||
return LNbits.utils.formatSat(Math.floor(value / 1000))
|
return LNbits.utils.formatSat(Math.floor(value / 1000))
|
||||||
},
|
},
|
||||||
|
backToUsersPage() {
|
||||||
|
this.activeUser.show = false
|
||||||
|
this.paymentPage.show = false
|
||||||
|
this.activeWallet.show = false
|
||||||
|
this.fetchUsers()
|
||||||
|
},
|
||||||
resetPassword(user_id) {
|
resetPassword(user_id) {
|
||||||
return LNbits.api
|
return LNbits.api
|
||||||
.request('PUT', `/users/api/v1/user/${user_id}/reset_password`)
|
.request('PUT', `/users/api/v1/user/${user_id}/reset_password`)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.$q.notify({
|
LNbits.utils
|
||||||
type: 'positive',
|
.confirmDialog(
|
||||||
message: 'generated key for password reset',
|
'A reset key has been generated. Click OK to copy the rest key to your clipboard.'
|
||||||
icon: null
|
)
|
||||||
})
|
.onOk(() => {
|
||||||
const url = window.location.origin + '?reset_key=' + res.data
|
const url = window.location.origin + '?reset_key=' + res.data
|
||||||
this.copyText(url)
|
this.copyText(url)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
@@ -229,33 +203,66 @@ window.app = Vue.createApp({
|
|||||||
},
|
},
|
||||||
createUser() {
|
createUser() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('POST', '/users/api/v1/user', null, this.createUserDialog.data)
|
.request('POST', '/users/api/v1/user', null, this.activeUser.data)
|
||||||
.then(() => {
|
.then(resp => {
|
||||||
this.fetchUsers()
|
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Success! User created!',
|
message: 'User created!',
|
||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.activeUser.setPassword = true
|
||||||
|
this.activeUser.data = resp.data
|
||||||
|
this.fetchUsers()
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
createWallet(user_id) {
|
updateUser() {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'PUT',
|
||||||
|
`/users/api/v1/user/${this.activeUser.data.id}`,
|
||||||
|
null,
|
||||||
|
this.activeUser.data
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'User updated!',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
this.activeUser.data = null
|
||||||
|
this.activeUser.show = false
|
||||||
|
this.fetchUsers()
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
createWallet() {
|
||||||
|
const userId = this.activeWallet.userId
|
||||||
|
if (!userId) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'No user selected!',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'POST',
|
'POST',
|
||||||
`/users/api/v1/user/${user_id}/wallet`,
|
`/users/api/v1/user/${userId}/wallet`,
|
||||||
null,
|
null,
|
||||||
this.createWalletDialog.data
|
this.createWalletDialog.data
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchUsers()
|
this.fetchWallets(userId)
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Success! User created!',
|
message: 'Wallet created!'
|
||||||
icon: null
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
@@ -272,9 +279,11 @@ window.app = Vue.createApp({
|
|||||||
this.fetchUsers()
|
this.fetchUsers()
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Success! User deleted!',
|
message: 'User deleted!',
|
||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
|
this.activeUser.data = null
|
||||||
|
this.activeUser.show = false
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
@@ -284,14 +293,14 @@ window.app = Vue.createApp({
|
|||||||
undeleteUserWallet(user_id, wallet) {
|
undeleteUserWallet(user_id, wallet) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'GET',
|
'PUT',
|
||||||
`/users/api/v1/user/${user_id}/wallet/${wallet}/undelete`
|
`/users/api/v1/user/${user_id}/wallet/${wallet}/undelete`
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchWallets(user_id)
|
this.fetchWallets(user_id)
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Success! Undeleted user wallet!',
|
message: 'Undeleted user wallet!',
|
||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -310,7 +319,7 @@ window.app = Vue.createApp({
|
|||||||
this.fetchWallets(user_id)
|
this.fetchWallets(user_id)
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Success! User wallet deleted!',
|
message: 'User wallet deleted!',
|
||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -319,38 +328,11 @@ window.app = Vue.createApp({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateChart(users) {
|
copyWalletLink(walletId) {
|
||||||
const filtered = users.filter(user => {
|
const url = `${window.location.origin}/wallet?usr=${this.activeWallet.userId}&wal=${walletId}`
|
||||||
if (
|
this.copyText(url)
|
||||||
user.balance_msat === null ||
|
|
||||||
user.balance_msat === 0 ||
|
|
||||||
user.wallet_count === 0
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = filtered.map(user => {
|
|
||||||
const labelUsername = `${user.username ? 'User: ' + user.username + '. ' : ''}`
|
|
||||||
const userBalanceSats = Math.floor(
|
|
||||||
user.balance_msat / 1000
|
|
||||||
).toLocaleString()
|
|
||||||
return {
|
|
||||||
x: user.transaction_count,
|
|
||||||
y: user.balance_msat / 1000000000,
|
|
||||||
r: 4,
|
|
||||||
customLabel:
|
|
||||||
labelUsername +
|
|
||||||
'Balance: ' +
|
|
||||||
userBalanceSats +
|
|
||||||
' sats. Tx count: ' +
|
|
||||||
user.transaction_count
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.chart1.data.datasets[0].data = data
|
|
||||||
this.chart1.update()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchUsers(props) {
|
fetchUsers(props) {
|
||||||
const params = LNbits.utils.prepareFilterQuery(this.usersTable, props)
|
const params = LNbits.utils.prepareFilterQuery(this.usersTable, props)
|
||||||
LNbits.api
|
LNbits.api
|
||||||
@@ -359,38 +341,32 @@ window.app = Vue.createApp({
|
|||||||
this.usersTable.loading = false
|
this.usersTable.loading = false
|
||||||
this.usersTable.pagination.rowsNumber = res.data.total
|
this.usersTable.pagination.rowsNumber = res.data.total
|
||||||
this.users = res.data.data
|
this.users = res.data.data
|
||||||
this.updateChart(this.users)
|
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchWallets(user_id) {
|
fetchWallets(userId) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', `/users/api/v1/user/${user_id}/wallet`)
|
.request('GET', `/users/api/v1/user/${userId}/wallet`)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.wallets = res.data
|
this.wallets = res.data
|
||||||
this.walletDialog.show = this.wallets.length > 0
|
this.activeWallet.userId = userId
|
||||||
if (!this.walletDialog.show) {
|
this.activeWallet.show = true
|
||||||
this.fetchUsers()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
showPayments(wallet_id) {
|
|
||||||
this.activeWallet = this.wallets.find(wallet => wallet.id === wallet_id)
|
toggleAdmin(userId) {
|
||||||
this.paymentDialog.show = true
|
|
||||||
},
|
|
||||||
toggleAdmin(user_id) {
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', `/users/api/v1/user/${user_id}/admin`)
|
.request('GET', `/users/api/v1/user/${userId}/admin`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchUsers()
|
this.fetchUsers()
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Success! Toggled admin!',
|
message: 'Toggled admin!',
|
||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -401,6 +377,40 @@ window.app = Vue.createApp({
|
|||||||
exportUsers() {
|
exportUsers() {
|
||||||
console.log('export users')
|
console.log('export users')
|
||||||
},
|
},
|
||||||
|
async showAccountPage(user_id) {
|
||||||
|
this.activeUser.showPassword = false
|
||||||
|
this.activeUser.showUserId = false
|
||||||
|
this.activeUser.setPassword = false
|
||||||
|
if (!user_id) {
|
||||||
|
this.activeUser.data = {extra: {}}
|
||||||
|
this.activeUser.show = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/users/api/v1/user/${user_id}`
|
||||||
|
)
|
||||||
|
this.activeUser.data = data
|
||||||
|
|
||||||
|
this.activeUser.show = true
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to get user!'
|
||||||
|
})
|
||||||
|
this.activeUser.show = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showTopupDialog(walletId) {
|
||||||
|
this.wallet.id = walletId
|
||||||
|
this.topupDialog.show = true
|
||||||
|
},
|
||||||
|
showPayments(wallet_id) {
|
||||||
|
this.paymentsWallet = this.wallets.find(wallet => wallet.id === wallet_id)
|
||||||
|
this.paymentPage.show = true
|
||||||
|
},
|
||||||
topupCallback(res) {
|
topupCallback(res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
this.wallets.forEach(wallet => {
|
this.wallets.forEach(wallet => {
|
||||||
@@ -422,14 +432,31 @@ window.app = Vue.createApp({
|
|||||||
.then(_ => {
|
.then(_ => {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: `Success! Added ${this.wallet.amount} to ${this.wallet.id}`,
|
message: `Added ${this.wallet.amount} to ${this.wallet.id}`,
|
||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
this.wallet = {}
|
this.wallet = {}
|
||||||
|
this.fetchWallets(this.activeWallet.userId)
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
searchUserBy(fieldName) {
|
||||||
|
const fieldValue = this.searchData[fieldName]
|
||||||
|
this.usersTable.filter = {}
|
||||||
|
if (fieldValue) {
|
||||||
|
this.usersTable.filter[fieldName] = fieldValue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchUsers()
|
||||||
|
},
|
||||||
|
shortify(value) {
|
||||||
|
valueLength = (value || '').length
|
||||||
|
if (valueLength <= 10) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return `${value.substring(0, 5)}...${value.substring(valueLength - 5, valueLength)}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -700,7 +700,7 @@ window.app = Vue.createApp({
|
|||||||
LNbits.events.onInvoicePaid(this.g.wallet, payment => {
|
LNbits.events.onInvoicePaid(this.g.wallet, payment => {
|
||||||
this.onPaymentReceived(payment.payment_hash)
|
this.onPaymentReceived(payment.payment_hash)
|
||||||
})
|
})
|
||||||
eventReactionWebocket(wallet.id)
|
eventReactionWebocket(wallet.inkey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -550,285 +550,266 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="payment-list">
|
<template id="payment-list">
|
||||||
<q-card
|
<div class="row items-center no-wrap q-mb-sm">
|
||||||
|
<div class="col">
|
||||||
|
<h5 class="text-subtitle1 q-my-none" :v-text="$t('transactions')"></h5>
|
||||||
|
</div>
|
||||||
|
<div class="gt-sm col-auto">
|
||||||
|
<q-btn-dropdown
|
||||||
|
outline
|
||||||
|
persistent
|
||||||
|
class="q-mr-sm"
|
||||||
|
color="grey"
|
||||||
|
:label="$t('export_csv')"
|
||||||
|
split
|
||||||
|
@click="exportCSV(false)"
|
||||||
|
>
|
||||||
|
<q-list>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
@keydown.enter="addFilterTag"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="exportTagName"
|
||||||
|
type="text"
|
||||||
|
label="Payment Tags"
|
||||||
|
class="q-pa-sm"
|
||||||
|
>
|
||||||
|
<q-btn @click="addFilterTag" dense flat icon="add"></q-btn>
|
||||||
|
</q-input>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item v-if="exportPaymentTagList.length">
|
||||||
|
<q-item-section>
|
||||||
|
<div>
|
||||||
|
<q-chip
|
||||||
|
v-for="tag in exportPaymentTagList"
|
||||||
|
:key="tag"
|
||||||
|
removable
|
||||||
|
@remove="removeExportTag(tag)"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
:label="tag"
|
||||||
|
></q-chip>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
@click="exportCSV(true)"
|
||||||
|
label="Export to CSV with details"
|
||||||
|
></q-btn>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
<payment-chart :wallet="wallet" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-input
|
||||||
:style="
|
:style="
|
||||||
$q.screen.lt.md
|
$q.screen.lt.md
|
||||||
? {
|
? {
|
||||||
background: $q.screen.lt.md ? 'none !important' : '',
|
display: mobileSimple ? 'none !important' : ''
|
||||||
boxShadow: $q.screen.lt.md ? 'none !important' : '',
|
|
||||||
marginTop: $q.screen.lt.md ? '0px !important' : ''
|
|
||||||
}
|
}
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
v-model="paymentsTable.search"
|
||||||
|
debounce="300"
|
||||||
|
:placeholder="$t('search_by_tag_memo_amount')"
|
||||||
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
<q-card-section>
|
</q-input>
|
||||||
<div class="row items-center no-wrap q-mb-sm">
|
<q-table
|
||||||
<div class="col">
|
dense
|
||||||
<h5
|
flat
|
||||||
class="text-subtitle1 q-my-none"
|
:rows="paymentsOmitter"
|
||||||
:v-text="$t('transactions')"
|
:row-key="paymentTableRowKey"
|
||||||
></h5>
|
:columns="paymentsTable.columns"
|
||||||
</div>
|
:no-data-label="$t('no_transactions')"
|
||||||
<div class="gt-sm col-auto">
|
:filter="paymentsTable.search"
|
||||||
<q-btn-dropdown
|
:loading="paymentsTable.loading"
|
||||||
outline
|
:hide-header="mobileSimple"
|
||||||
persistent
|
:hide-bottom="mobileSimple"
|
||||||
class="q-mr-sm"
|
v-model:pagination="paymentsTable.pagination"
|
||||||
color="grey"
|
@request="fetchPayments"
|
||||||
:label="$t('export_csv')"
|
>
|
||||||
split
|
<template v-slot:header="props">
|
||||||
@click="exportCSV(false)"
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
v-text="col.label"
|
||||||
|
></q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width class="text-center">
|
||||||
|
<q-icon
|
||||||
|
v-if="props.row.isPaid"
|
||||||
|
size="14px"
|
||||||
|
:name="props.row.isOut ? 'call_made' : 'call_received'"
|
||||||
|
:color="props.row.isOut ? 'pink' : 'green'"
|
||||||
|
@click="props.expand = !props.expand"
|
||||||
|
></q-icon>
|
||||||
|
<q-icon
|
||||||
|
v-else-if="props.row.isFailed"
|
||||||
|
name="warning"
|
||||||
|
color="yellow"
|
||||||
|
@click="props.expand = !props.expand"
|
||||||
>
|
>
|
||||||
<q-list>
|
<q-tooltip><span>failed</span></q-tooltip>
|
||||||
<q-item>
|
</q-icon>
|
||||||
<q-item-section>
|
<q-icon
|
||||||
<q-input
|
v-else
|
||||||
@keydown.enter="addFilterTag"
|
name="settings_ethernet"
|
||||||
filled
|
color="grey"
|
||||||
dense
|
@click="props.expand = !props.expand"
|
||||||
v-model="exportTagName"
|
>
|
||||||
type="text"
|
<q-tooltip><span v-text="$t('pending')"></span></q-tooltip>
|
||||||
label="Payment Tags"
|
</q-icon>
|
||||||
class="q-pa-sm"
|
</q-td>
|
||||||
>
|
<q-td
|
||||||
<q-btn @click="addFilterTag" dense flat icon="add"></q-btn>
|
key="time"
|
||||||
</q-input>
|
:props="props"
|
||||||
</q-item-section>
|
style="white-space: normal; word-break: break-all"
|
||||||
</q-item>
|
>
|
||||||
<q-item v-if="exportPaymentTagList.length">
|
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
|
||||||
<q-item-section>
|
<a
|
||||||
<div>
|
v-text="'#' + props.row.tag"
|
||||||
<q-chip
|
class="inherit"
|
||||||
v-for="tag in exportPaymentTagList"
|
:href="['/', props.row.tag].join('')"
|
||||||
:key="tag"
|
></a>
|
||||||
removable
|
</q-badge>
|
||||||
@remove="removeExportTag(tag)"
|
<span v-text="props.row.memo"></span>
|
||||||
color="primary"
|
<br />
|
||||||
text-color="white"
|
|
||||||
:label="tag"
|
|
||||||
></q-chip>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item>
|
<i>
|
||||||
<q-item-section>
|
<span v-text="props.row.dateFrom"></span>
|
||||||
|
<q-tooltip><span v-text="props.row.date"></span></q-tooltip>
|
||||||
|
</i>
|
||||||
|
</q-td>
|
||||||
|
<q-td
|
||||||
|
auto-width
|
||||||
|
key="amount"
|
||||||
|
v-if="denomination != 'sats'"
|
||||||
|
:props="props"
|
||||||
|
class="col1"
|
||||||
|
v-text="parseFloat(String(props.row.fsat).replaceAll(',', '')) / 100"
|
||||||
|
>
|
||||||
|
</q-td>
|
||||||
|
<q-td class="col2" auto-width key="amount" v-else :props="props">
|
||||||
|
<span v-text="props.row.fsat"></span>
|
||||||
|
<br />
|
||||||
|
<i v-if="props.row.extra.wallet_fiat_currency">
|
||||||
|
<span
|
||||||
|
v-text="
|
||||||
|
formatCurrency(
|
||||||
|
props.row.extra.wallet_fiat_amount,
|
||||||
|
props.row.extra.wallet_fiat_currency
|
||||||
|
)
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
<br />
|
||||||
|
</i>
|
||||||
|
<i v-if="props.row.extra.fiat_currency">
|
||||||
|
<span
|
||||||
|
v-text="
|
||||||
|
formatCurrency(
|
||||||
|
props.row.extra.fiat_amount,
|
||||||
|
props.row.extra.fiat_currency
|
||||||
|
)
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
</i>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-dialog v-model="props.expand" :props="props" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<div class="text-center q-mb-lg">
|
||||||
|
<div v-if="props.row.isIn && props.row.isPending">
|
||||||
|
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
||||||
|
<span v-text="$t('invoice_waiting')"></span>
|
||||||
|
<lnbits-payment-details
|
||||||
|
:payment="props.row"
|
||||||
|
></lnbits-payment-details>
|
||||||
|
<div v-if="props.row.bolt11" class="text-center q-mb-lg">
|
||||||
|
<a :href="'lightning:' + props.row.bolt11">
|
||||||
|
<lnbits-qrcode
|
||||||
|
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-close-popup
|
|
||||||
outline
|
outline
|
||||||
color="grey"
|
color="grey"
|
||||||
@click="exportCSV(true)"
|
@click="copyText(props.row.bolt11)"
|
||||||
label="Export to CSV with details"
|
:label="$t('copy_invoice')"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="q-ml-auto"
|
||||||
|
:label="$t('close')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-btn-dropdown>
|
|
||||||
<payment-chart :wallet="wallet" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-input
|
|
||||||
:style="
|
|
||||||
$q.screen.lt.md
|
|
||||||
? {
|
|
||||||
display: mobileSimple ? 'none !important' : ''
|
|
||||||
}
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
clearable
|
|
||||||
v-model="paymentsTable.search"
|
|
||||||
debounce="300"
|
|
||||||
:placeholder="$t('search_by_tag_memo_amount')"
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:rows="paymentsOmitter"
|
|
||||||
:row-key="paymentTableRowKey"
|
|
||||||
:columns="paymentsTable.columns"
|
|
||||||
:no-data-label="$t('no_transactions')"
|
|
||||||
:filter="paymentsTable.search"
|
|
||||||
:loading="paymentsTable.loading"
|
|
||||||
:hide-header="mobileSimple"
|
|
||||||
:hide-bottom="mobileSimple"
|
|
||||||
v-model:pagination="paymentsTable.pagination"
|
|
||||||
@request="fetchPayments"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th
|
|
||||||
v-for="col in props.cols"
|
|
||||||
:key="col.name"
|
|
||||||
:props="props"
|
|
||||||
v-text="col.label"
|
|
||||||
></q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width class="text-center">
|
|
||||||
<q-icon
|
|
||||||
v-if="props.row.isPaid"
|
|
||||||
size="14px"
|
|
||||||
:name="props.row.isOut ? 'call_made' : 'call_received'"
|
|
||||||
:color="props.row.isOut ? 'pink' : 'green'"
|
|
||||||
@click="props.expand = !props.expand"
|
|
||||||
></q-icon>
|
|
||||||
<q-icon
|
|
||||||
v-else-if="props.row.isFailed"
|
|
||||||
name="warning"
|
|
||||||
color="yellow"
|
|
||||||
@click="props.expand = !props.expand"
|
|
||||||
>
|
|
||||||
<q-tooltip><span>failed</span></q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
<q-icon
|
|
||||||
v-else
|
|
||||||
name="settings_ethernet"
|
|
||||||
color="grey"
|
|
||||||
@click="props.expand = !props.expand"
|
|
||||||
>
|
|
||||||
<q-tooltip><span v-text="$t('pending')"></span></q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</q-td>
|
|
||||||
<q-td
|
|
||||||
key="time"
|
|
||||||
:props="props"
|
|
||||||
style="white-space: normal; word-break: break-all"
|
|
||||||
>
|
|
||||||
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
|
|
||||||
<a
|
|
||||||
v-text="'#' + props.row.tag"
|
|
||||||
class="inherit"
|
|
||||||
:href="['/', props.row.tag].join('')"
|
|
||||||
></a>
|
|
||||||
</q-badge>
|
|
||||||
<span v-text="props.row.memo"></span>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<i>
|
|
||||||
<span v-text="props.row.dateFrom"></span>
|
|
||||||
<q-tooltip><span v-text="props.row.date"></span></q-tooltip>
|
|
||||||
</i>
|
|
||||||
</q-td>
|
|
||||||
<q-td
|
|
||||||
auto-width
|
|
||||||
key="amount"
|
|
||||||
v-if="denomination != 'sats'"
|
|
||||||
:props="props"
|
|
||||||
class="col1"
|
|
||||||
v-text="
|
|
||||||
parseFloat(String(props.row.fsat).replaceAll(',', '')) / 100
|
|
||||||
"
|
|
||||||
>
|
|
||||||
</q-td>
|
|
||||||
<q-td class="col2" auto-width key="amount" v-else :props="props">
|
|
||||||
<span v-text="props.row.fsat"></span>
|
|
||||||
<br />
|
|
||||||
<i v-if="props.row.extra.wallet_fiat_currency">
|
|
||||||
<span
|
|
||||||
v-text="
|
|
||||||
formatCurrency(
|
|
||||||
props.row.extra.wallet_fiat_amount,
|
|
||||||
props.row.extra.wallet_fiat_currency
|
|
||||||
)
|
|
||||||
"
|
|
||||||
></span>
|
|
||||||
<br />
|
|
||||||
</i>
|
|
||||||
<i v-if="props.row.extra.fiat_currency">
|
|
||||||
<span
|
|
||||||
v-text="
|
|
||||||
formatCurrency(
|
|
||||||
props.row.extra.fiat_amount,
|
|
||||||
props.row.extra.fiat_currency
|
|
||||||
)
|
|
||||||
"
|
|
||||||
></span>
|
|
||||||
</i>
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-dialog v-model="props.expand" :props="props" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<div class="text-center q-mb-lg">
|
|
||||||
<div v-if="props.row.isIn && props.row.isPending">
|
|
||||||
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
|
||||||
<span v-text="$t('invoice_waiting')"></span>
|
|
||||||
<lnbits-payment-details
|
|
||||||
:payment="props.row"
|
|
||||||
></lnbits-payment-details>
|
|
||||||
<div v-if="props.row.bolt11" class="text-center q-mb-lg">
|
|
||||||
<a :href="'lightning:' + props.row.bolt11">
|
|
||||||
<lnbits-qrcode
|
|
||||||
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
|
||||||
></lnbits-qrcode>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText(props.row.bolt11)"
|
|
||||||
:label="$t('copy_invoice')"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
:label="$t('close')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="props.row.isOut && props.row.isPending">
|
|
||||||
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
|
||||||
<span v-text="$t('outgoing_payment_pending')"></span>
|
|
||||||
<lnbits-payment-details
|
|
||||||
:payment="props.row"
|
|
||||||
></lnbits-payment-details>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="props.row.isPaid && props.row.isIn">
|
|
||||||
<q-icon
|
|
||||||
size="18px"
|
|
||||||
:name="'call_received'"
|
|
||||||
:color="'green'"
|
|
||||||
></q-icon>
|
|
||||||
<span v-text="$t('payment_received')"></span>
|
|
||||||
<lnbits-payment-details
|
|
||||||
:payment="props.row"
|
|
||||||
></lnbits-payment-details>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="props.row.isPaid && props.row.isOut">
|
|
||||||
<q-icon
|
|
||||||
size="18px"
|
|
||||||
:name="'call_made'"
|
|
||||||
:color="'pink'"
|
|
||||||
></q-icon>
|
|
||||||
<span v-text="$t('payment_sent')"></span>
|
|
||||||
<lnbits-payment-details
|
|
||||||
:payment="props.row"
|
|
||||||
></lnbits-payment-details>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="props.row.isFailed">
|
|
||||||
<q-icon name="warning" color="yellow"></q-icon>
|
|
||||||
<span>Payment failed</span>
|
|
||||||
<lnbits-payment-details
|
|
||||||
:payment="props.row"
|
|
||||||
></lnbits-payment-details>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</div>
|
||||||
</q-dialog>
|
<div v-else-if="props.row.isOut && props.row.isPending">
|
||||||
</q-tr>
|
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
||||||
</template>
|
<span v-text="$t('outgoing_payment_pending')"></span>
|
||||||
</q-table>
|
<lnbits-payment-details
|
||||||
</q-card-section>
|
:payment="props.row"
|
||||||
</q-card>
|
></lnbits-payment-details>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="props.row.isPaid && props.row.isIn">
|
||||||
|
<q-icon
|
||||||
|
size="18px"
|
||||||
|
:name="'call_received'"
|
||||||
|
:color="'green'"
|
||||||
|
></q-icon>
|
||||||
|
<span v-text="$t('payment_received')"></span>
|
||||||
|
<lnbits-payment-details
|
||||||
|
:payment="props.row"
|
||||||
|
></lnbits-payment-details>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="props.row.isPaid && props.row.isOut">
|
||||||
|
<q-icon
|
||||||
|
size="18px"
|
||||||
|
:name="'call_made'"
|
||||||
|
:color="'pink'"
|
||||||
|
></q-icon>
|
||||||
|
<span v-text="$t('payment_sent')"></span>
|
||||||
|
<lnbits-payment-details
|
||||||
|
:payment="props.row"
|
||||||
|
></lnbits-payment-details>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="props.row.isFailed">
|
||||||
|
<q-icon name="warning" color="yellow"></q-icon>
|
||||||
|
<span>Payment failed</span>
|
||||||
|
<lnbits-payment-details
|
||||||
|
:payment="props.row"
|
||||||
|
></lnbits-payment-details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="lnbits-extension-rating">
|
<template id="lnbits-extension-rating">
|
||||||
|
Reference in New Issue
Block a user