mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-03-04 01:53:59 +01:00
added usermanager extension
This commit is contained in:
parent
de97c0f696
commit
706d584332
10 changed files with 1120 additions and 0 deletions
26
lnbits/extensions/usermanager/README.md
Normal file
26
lnbits/extensions/usermanager/README.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# User Manager
|
||||
|
||||
## Make and manage users/wallets
|
||||
|
||||
To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets.
|
||||
|
||||
For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the developers stack as the user and wallet manager. Or someone wanting to manage their family's wallets (wife, children, parents, etc...) or you want to host a community Lightning Network node and want to manage wallets for the users.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Click the button "NEW USER" to create a new user\
|
||||

|
||||
2. Fill the user information\
|
||||
- username
|
||||
- the generated wallet name, user can create other wallets later on
|
||||
- email
|
||||
- set a password
|
||||

|
||||
3. After creating your user, it will appear in the **Users** section, and a user's wallet in the **Wallets** section.
|
||||
4. Next you can share the wallet with the corresponding user\
|
||||

|
||||
5. If you need to create more wallets for some user, click "NEW WALLET" at the top\
|
||||

|
||||
- select the existing user you wish to add the wallet
|
||||
- set a wallet name\
|
||||

|
12
lnbits/extensions/usermanager/__init__.py
Normal file
12
lnbits/extensions/usermanager/__init__.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from quart import Blueprint
|
||||
from lnbits.db import Database
|
||||
|
||||
db = Database("ext_usermanager")
|
||||
|
||||
usermanager_ext: Blueprint = Blueprint(
|
||||
"usermanager", __name__, static_folder="static", template_folder="templates"
|
||||
)
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
6
lnbits/extensions/usermanager/config.json
Normal file
6
lnbits/extensions/usermanager/config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "User Manager",
|
||||
"short_description": "Generate users and wallets",
|
||||
"icon": "person_add",
|
||||
"contributors": ["benarc"]
|
||||
}
|
122
lnbits/extensions/usermanager/crud.py
Normal file
122
lnbits/extensions/usermanager/crud.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from typing import Optional, List
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.crud import (
|
||||
create_account,
|
||||
get_user,
|
||||
get_payments,
|
||||
create_wallet,
|
||||
delete_wallet,
|
||||
)
|
||||
|
||||
from . import db
|
||||
from .models import Users, Wallets
|
||||
|
||||
|
||||
### Users
|
||||
|
||||
|
||||
async def create_usermanager_user(
|
||||
user_name: str,
|
||||
wallet_name: str,
|
||||
admin_id: str,
|
||||
email: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
) -> Users:
|
||||
account = await create_account()
|
||||
user = await get_user(account.id)
|
||||
assert user, "Newly created user couldn't be retrieved"
|
||||
|
||||
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO usermanager.users (id, name, admin, email, password)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(user.id, user_name, admin_id, email, password),
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(wallet.id, admin_id, wallet_name, user.id, wallet.adminkey, wallet.inkey),
|
||||
)
|
||||
|
||||
user_created = await get_usermanager_user(user.id)
|
||||
assert user_created, "Newly created user couldn't be retrieved"
|
||||
return user_created
|
||||
|
||||
|
||||
async def get_usermanager_user(user_id: str) -> Optional[Users]:
|
||||
row = await db.fetchone("SELECT * FROM usermanager.users WHERE id = ?", (user_id,))
|
||||
return Users(**row) if row else None
|
||||
|
||||
|
||||
async def get_usermanager_users(user_id: str) -> List[Users]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM usermanager.users WHERE admin = ?", (user_id,)
|
||||
)
|
||||
return [Users(**row) for row in rows]
|
||||
|
||||
|
||||
async def delete_usermanager_user(user_id: str) -> None:
|
||||
wallets = await get_usermanager_wallets(user_id)
|
||||
for wallet in wallets:
|
||||
await delete_wallet(user_id=user_id, wallet_id=wallet.id)
|
||||
|
||||
await db.execute("DELETE FROM usermanager.users WHERE id = ?", (user_id,))
|
||||
await db.execute("""DELETE FROM usermanager.wallets WHERE "user" = ?""", (user_id,))
|
||||
|
||||
|
||||
### Wallets
|
||||
|
||||
|
||||
async def create_usermanager_wallet(
|
||||
user_id: str, wallet_name: str, admin_id: str
|
||||
) -> Wallets:
|
||||
wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name)
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(wallet.id, admin_id, wallet_name, user_id, wallet.adminkey, wallet.inkey),
|
||||
)
|
||||
wallet_created = await get_usermanager_wallet(wallet.id)
|
||||
assert wallet_created, "Newly created wallet couldn't be retrieved"
|
||||
return wallet_created
|
||||
|
||||
|
||||
async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallets]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM usermanager.wallets WHERE id = ?", (wallet_id,)
|
||||
)
|
||||
return Wallets(**row) if row else None
|
||||
|
||||
|
||||
async def get_usermanager_wallets(admin_id: str) -> Optional[Wallets]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM usermanager.wallets WHERE admin = ?", (admin_id,)
|
||||
)
|
||||
return [Wallets(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_usermanager_users_wallets(user_id: str) -> Optional[Wallets]:
|
||||
rows = await db.fetchall(
|
||||
"""SELECT * FROM usermanager.wallets WHERE "user" = ?""", (user_id,)
|
||||
)
|
||||
return [Wallets(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_usermanager_wallet_transactions(wallet_id: str) -> Optional[Payment]:
|
||||
return await get_payments(
|
||||
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
|
||||
)
|
||||
|
||||
|
||||
async def delete_usermanager_wallet(wallet_id: str, user_id: str) -> None:
|
||||
await delete_wallet(user_id=user_id, wallet_id=wallet_id)
|
||||
await db.execute("DELETE FROM usermanager.wallets WHERE id = ?", (wallet_id,))
|
31
lnbits/extensions/usermanager/migrations.py
Normal file
31
lnbits/extensions/usermanager/migrations.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
async def m001_initial(db):
|
||||
"""
|
||||
Initial users table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE usermanager.users (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
admin TEXT NOT NULL,
|
||||
email TEXT,
|
||||
password TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
"""
|
||||
Initial wallets table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE usermanager.wallets (
|
||||
id TEXT PRIMARY KEY,
|
||||
admin TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
"user" TEXT NOT NULL,
|
||||
adminkey TEXT NOT NULL,
|
||||
inkey TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
23
lnbits/extensions/usermanager/models.py
Normal file
23
lnbits/extensions/usermanager/models.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from typing import NamedTuple
|
||||
from sqlite3 import Row
|
||||
|
||||
|
||||
class Users(NamedTuple):
|
||||
id: str
|
||||
name: str
|
||||
admin: str
|
||||
email: str
|
||||
password: str
|
||||
|
||||
|
||||
class Wallets(NamedTuple):
|
||||
id: str
|
||||
admin: str
|
||||
name: str
|
||||
user: str
|
||||
adminkey: str
|
||||
inkey: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Wallets":
|
||||
return cls(**dict(row))
|
|
@ -0,0 +1,259 @@
|
|||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">
|
||||
User Manager: Make and manager users/wallets
|
||||
</h5>
|
||||
<p>
|
||||
To help developers use LNbits to manage their users, the User Manager
|
||||
extension allows the creation and management of users and wallets.
|
||||
<br />For example, a games developer may be developing a game that needs
|
||||
each user to have their own wallet, LNbits can be included in the
|
||||
develpoers stack as the user and wallet manager.<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET users">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/usermanager/api/v1/users</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>JSON list of users</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}usermanager/api/v1/users -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET user">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/usermanager/api/v1/users/<user_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>JSON list of users</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}usermanager/api/v1/users/<user_id> -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET wallets">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/usermanager/api/v1/wallets/<user_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>JSON wallet data</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}usermanager/api/v1/wallets/<user_id> -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET transactions">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/usermanager/api/v1/wallets<wallet_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>JSON a wallets transactions</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}usermanager/api/v1/wallets<wallet_id> -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="POST user + initial wallet"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-green">POST</span>
|
||||
/usermanager/api/v1/users</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code
|
||||
>{"X-Api-Key": <string>, "Content-type":
|
||||
"application/json"}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json) - "admin_id" is a YOUR user ID
|
||||
</h5>
|
||||
<code
|
||||
>{"admin_id": <string>, "user_name": <string>,
|
||||
"wallet_name": <string>,"email": <Optional string>
|
||||
,"password": <Optional string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"id": <string>, "name": <string>, "admin":
|
||||
<string>, "email": <string>, "password":
|
||||
<string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}usermanager/api/v1/users -d '{"admin_id": "{{
|
||||
g.user.id }}", "wallet_name": <string>, "user_name":
|
||||
<string>, "email": <Optional string>, "password": <
|
||||
Optional string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
|
||||
"Content-type: application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="POST wallet">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-green">POST</span>
|
||||
/usermanager/api/v1/wallets</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code
|
||||
>{"X-Api-Key": <string>, "Content-type":
|
||||
"application/json"}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json) - "admin_id" is a YOUR user ID
|
||||
</h5>
|
||||
<code
|
||||
>{"user_id": <string>, "wallet_name": <string>,
|
||||
"admin_id": <string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"id": <string>, "admin": <string>, "name":
|
||||
<string>, "user": <string>, "adminkey": <string>,
|
||||
"inkey": <string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d '{"user_id":
|
||||
<string>, "wallet_name": <string>, "admin_id": "{{
|
||||
g.user.id }}"}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
|
||||
"Content-type: application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="DELETE user and their wallets"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-red">DELETE</span>
|
||||
/usermanager/api/v1/users/<user_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root }}usermanager/api/v1/users/<user_id> -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="DELETE wallet">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-red">DELETE</span>
|
||||
/usermanager/api/v1/wallets/<wallet_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root }}usermanager/api/v1/wallets/<wallet_id>
|
||||
-H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="POST activate extension"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-green">POST</span>
|
||||
/usermanager/api/v1/extensions</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}usermanager/api/v1/extensions -d '{"userid":
|
||||
<string>, "extension": <string>, "active":
|
||||
<integer>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
|
||||
"Content-type: application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
473
lnbits/extensions/usermanager/templates/usermanager/index.html
Normal file
473
lnbits/extensions/usermanager/templates/usermanager/index.html
Normal file
|
@ -0,0 +1,473 @@
|
|||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="primary" @click="userDialog.show = true"
|
||||
>New User</q-btn
|
||||
>
|
||||
<q-btn unelevated color="primary" @click="walletDialog.show = true"
|
||||
>New Wallet
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Users</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportUsersCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="users"
|
||||
row-key="id"
|
||||
:columns="usersTable.columns"
|
||||
:pagination.sync="usersTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteUser(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Wallets</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportWalletsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="wallets"
|
||||
row-key="id"
|
||||
:columns="walletsTable.columns"
|
||||
:pagination.sync="walletsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<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">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="account_balance_wallet"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.walllink"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-tooltip> Link to wallet </q-tooltip>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteWallet(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
{{SITE_TITLE}} User Manager Extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "usermanager/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="userDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="sendUserFormData" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="userDialog.data.usrname"
|
||||
label="Username"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="userDialog.data.walname"
|
||||
label="Initial wallet name"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="userDialog.data.email"
|
||||
label="Email"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="userDialog.data.password"
|
||||
label="Password"
|
||||
></q-input>
|
||||
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="userDialog.data.walname == null"
|
||||
type="submit"
|
||||
>Create User</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="walletDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="sendWalletFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="walletDialog.data.user"
|
||||
:options="userOptions"
|
||||
label="User *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="walletDialog.data.walname"
|
||||
label="Wallet name"
|
||||
></q-input>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="walletDialog.data.walname == null"
|
||||
type="submit"
|
||||
>Create Wallet</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapUserManager = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.walllink = ['../wallet?usr=', obj.user, '&wal=', obj.id].join('')
|
||||
obj._data = _.clone(obj)
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
wallets: [],
|
||||
users: [],
|
||||
|
||||
usersTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Username', field: 'name'},
|
||||
{name: 'email', align: 'left', label: 'Email', field: 'email'},
|
||||
{
|
||||
name: 'password',
|
||||
align: 'left',
|
||||
label: 'Password',
|
||||
field: 'password'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
walletsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'user', align: 'left', label: 'User', field: 'user'},
|
||||
{
|
||||
name: 'adminkey',
|
||||
align: 'left',
|
||||
label: 'Admin Key',
|
||||
field: 'adminkey'
|
||||
},
|
||||
{name: 'inkey', align: 'left', label: 'Invoice Key', field: 'inkey'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
walletDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
userDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userOptions: function () {
|
||||
return this.users.map(function (obj) {
|
||||
console.log(obj.id)
|
||||
return {
|
||||
value: String(obj.id),
|
||||
label: String(obj.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
///////////////Users////////////////////////////
|
||||
|
||||
getUsers: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/usermanager/api/v1/users',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.users = response.data.map(function (obj) {
|
||||
return mapUserManager(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
openUserUpdateDialog: function (linkId) {
|
||||
var link = _.findWhere(this.users, {id: linkId})
|
||||
|
||||
this.userDialog.data = _.clone(link._data)
|
||||
this.userDialog.show = true
|
||||
},
|
||||
sendUserFormData: function () {
|
||||
if (this.userDialog.data.id) {
|
||||
} else {
|
||||
var data = {
|
||||
admin_id: this.g.user.id,
|
||||
user_name: this.userDialog.data.usrname,
|
||||
wallet_name: this.userDialog.data.walname,
|
||||
email: this.userDialog.data.email,
|
||||
password: this.userDialog.data.password
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
this.createUser(data)
|
||||
}
|
||||
},
|
||||
|
||||
createUser: function (data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/usermanager/api/v1/users',
|
||||
this.g.user.wallets[0].inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.users.push(mapUserManager(response.data))
|
||||
self.userDialog.show = false
|
||||
self.userDialog.data = {}
|
||||
data = {}
|
||||
self.getWallets()
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteUser: function (userId) {
|
||||
var self = this
|
||||
|
||||
console.log(userId)
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this User link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/usermanager/api/v1/users/' + userId,
|
||||
self.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.users = _.reject(self.users, function (obj) {
|
||||
return obj.id == userId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
exportUsersCSV: function () {
|
||||
LNbits.utils.exportCSV(this.usersTable.columns, this.users)
|
||||
},
|
||||
|
||||
///////////////Wallets////////////////////////////
|
||||
|
||||
getWallets: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/usermanager/api/v1/wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.wallets = response.data.map(function (obj) {
|
||||
return mapUserManager(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
openWalletUpdateDialog: function (linkId) {
|
||||
var link = _.findWhere(this.users, {id: linkId})
|
||||
|
||||
this.walletDialog.data = _.clone(link._data)
|
||||
this.walletDialog.show = true
|
||||
},
|
||||
sendWalletFormData: function () {
|
||||
if (this.walletDialog.data.id) {
|
||||
} else {
|
||||
var data = {
|
||||
user_id: this.walletDialog.data.user,
|
||||
admin_id: this.g.user.id,
|
||||
wallet_name: this.walletDialog.data.walname
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
this.createWallet(data)
|
||||
}
|
||||
},
|
||||
|
||||
createWallet: function (data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/usermanager/api/v1/wallets',
|
||||
this.g.user.wallets[0].inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.wallets.push(mapUserManager(response.data))
|
||||
self.walletDialog.show = false
|
||||
self.walletDialog.data = {}
|
||||
data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteWallet: function (userId) {
|
||||
var self = this
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this wallet link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/usermanager/api/v1/wallets/' + userId,
|
||||
self.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.wallets = _.reject(self.wallets, function (obj) {
|
||||
return obj.id == userId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportWalletsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.walletsTable.columns, this.wallets)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getUsers()
|
||||
this.getWallets()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
12
lnbits/extensions/usermanager/views.py
Normal file
12
lnbits/extensions/usermanager/views.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from quart import g, render_template
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
|
||||
from . import usermanager_ext
|
||||
|
||||
|
||||
@usermanager_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
async def index():
|
||||
return await render_template("usermanager/index.html", user=g.user)
|
156
lnbits/extensions/usermanager/views_api.py
Normal file
156
lnbits/extensions/usermanager/views_api.py
Normal file
|
@ -0,0 +1,156 @@
|
|||
from quart import g, jsonify
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
|
||||
from . import usermanager_ext
|
||||
from .crud import (
|
||||
create_usermanager_user,
|
||||
get_usermanager_user,
|
||||
get_usermanager_users,
|
||||
get_usermanager_wallet_transactions,
|
||||
delete_usermanager_user,
|
||||
create_usermanager_wallet,
|
||||
get_usermanager_wallet,
|
||||
get_usermanager_wallets,
|
||||
get_usermanager_users_wallets,
|
||||
delete_usermanager_wallet,
|
||||
)
|
||||
from lnbits.core import update_user_extension
|
||||
|
||||
|
||||
### Users
|
||||
|
||||
|
||||
@usermanager_ext.route("/api/v1/users", methods=["GET"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
async def api_usermanager_users():
|
||||
user_id = g.wallet.user
|
||||
return (
|
||||
jsonify([user._asdict() for user in await get_usermanager_users(user_id)]),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
|
||||
@usermanager_ext.route("/api/v1/users/<user_id>", methods=["GET"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
async def api_usermanager_user(user_id):
|
||||
user = await get_usermanager_user(user_id)
|
||||
return (
|
||||
jsonify(user._asdict()),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
|
||||
@usermanager_ext.route("/api/v1/users", methods=["POST"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"user_name": {"type": "string", "empty": False, "required": True},
|
||||
"wallet_name": {"type": "string", "empty": False, "required": True},
|
||||
"admin_id": {"type": "string", "empty": False, "required": True},
|
||||
"email": {"type": "string", "required": False},
|
||||
"password": {"type": "string", "required": False},
|
||||
}
|
||||
)
|
||||
async def api_usermanager_users_create():
|
||||
user = await create_usermanager_user(**g.data)
|
||||
full = user._asdict()
|
||||
full["wallets"] = [wallet._asdict() for wallet in await get_usermanager_users_wallets(user.id)]
|
||||
return jsonify(full), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@usermanager_ext.route("/api/v1/users/<user_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
async def api_usermanager_users_delete(user_id):
|
||||
user = await get_usermanager_user(user_id)
|
||||
if not user:
|
||||
return jsonify({"message": "User does not exist."}), HTTPStatus.NOT_FOUND
|
||||
await delete_usermanager_user(user_id)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
###Activate Extension
|
||||
|
||||
|
||||
@usermanager_ext.route("/api/v1/extensions", methods=["POST"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"extension": {"type": "string", "empty": False, "required": True},
|
||||
"userid": {"type": "string", "empty": False, "required": True},
|
||||
"active": {"type": "boolean", "required": True},
|
||||
}
|
||||
)
|
||||
async def api_usermanager_activate_extension():
|
||||
user = await get_user(g.data["userid"])
|
||||
if not user:
|
||||
return jsonify({"message": "no such user"}), HTTPStatus.NOT_FOUND
|
||||
update_user_extension(
|
||||
user_id=g.data["userid"], extension=g.data["extension"], active=g.data["active"]
|
||||
)
|
||||
return jsonify({"extension": "updated"}), HTTPStatus.CREATED
|
||||
|
||||
|
||||
###Wallets
|
||||
|
||||
|
||||
@usermanager_ext.route("/api/v1/wallets", methods=["POST"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"user_id": {"type": "string", "empty": False, "required": True},
|
||||
"wallet_name": {"type": "string", "empty": False, "required": True},
|
||||
"admin_id": {"type": "string", "empty": False, "required": True},
|
||||
}
|
||||
)
|
||||
async def api_usermanager_wallets_create():
|
||||
user = await create_usermanager_wallet(
|
||||
g.data["user_id"], g.data["wallet_name"], g.data["admin_id"]
|
||||
)
|
||||
return jsonify(user._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@usermanager_ext.route("/api/v1/wallets", methods=["GET"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
async def api_usermanager_wallets():
|
||||
admin_id = g.wallet.user
|
||||
return (
|
||||
jsonify(
|
||||
[wallet._asdict() for wallet in await get_usermanager_wallets(admin_id)]
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
|
||||
@usermanager_ext.route("/api/v1/wallets<wallet_id>", methods=["GET"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
async def api_usermanager_wallet_transactions(wallet_id):
|
||||
return jsonify(await get_usermanager_wallet_transactions(wallet_id)), HTTPStatus.OK
|
||||
|
||||
|
||||
@usermanager_ext.route("/api/v1/wallets/<user_id>", methods=["GET"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
async def api_usermanager_users_wallets(user_id):
|
||||
wallet = await get_usermanager_users_wallets(user_id)
|
||||
return (
|
||||
jsonify(
|
||||
[
|
||||
wallet._asdict()
|
||||
for wallet in await get_usermanager_users_wallets(user_id)
|
||||
]
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
|
||||
@usermanager_ext.route("/api/v1/wallets/<wallet_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
async def api_usermanager_wallets_delete(wallet_id):
|
||||
wallet = await get_usermanager_wallet(wallet_id)
|
||||
if not wallet:
|
||||
return jsonify({"message": "Wallet does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
await delete_usermanager_wallet(wallet_id, wallet.user)
|
||||
return "", HTTPStatus.NO_CONTENT
|
Loading…
Add table
Reference in a new issue