Merge remote-tracking branch 'arcbtc/FastAPI' into FastAPI

This commit is contained in:
Ben Arc 2021-10-12 18:54:54 +01:00
commit 25616031eb
16 changed files with 1132 additions and 11 deletions

View file

@ -2,6 +2,7 @@ import hashlib
import math import math
from http import HTTPStatus from http import HTTPStatus
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from starlette.exceptions import HTTPException
from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice

View 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\
![new user](https://i.imgur.com/4yZyfJE.png)
2. Fill the user information\
- username
- the generated wallet name, user can create other wallets later on
- email
- set a password
![user information](https://i.imgur.com/40du7W5.png)
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\
![user wallet](https://i.imgur.com/gAyajbx.png)
5. If you need to create more wallets for some user, click "NEW WALLET" at the top\
![multiple wallets](https://i.imgur.com/wovVnim.png)
- select the existing user you wish to add the wallet
- set a wallet name\
![new wallet](https://i.imgur.com/sGwG8dC.png)

View file

@ -0,0 +1,25 @@
import asyncio
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_usermanager")
usermanager_ext: APIRouter = APIRouter(
prefix="/usermanager",
tags=["usermanager"]
#"usermanager", __name__, static_folder="static", template_folder="templates"
)
def usermanager_renderer():
return template_renderer(
[
"lnbits/extensions/usermanager/templates",
]
)
from .views_api import * # noqa
from .views import * # noqa

View file

@ -0,0 +1,6 @@
{
"name": "User Manager",
"short_description": "Generate users and wallets",
"icon": "person_add",
"contributors": ["benarc"]
}

View file

@ -0,0 +1,119 @@
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, CreateUserData
### Users
async def create_usermanager_user(
data: CreateUserData
) -> 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=data.wallet_name)
await db.execute(
"""
INSERT INTO usermanager.users (id, name, admin, email, password)
VALUES (?, ?, ?, ?, ?)
""",
(user.id, data.user_name, data.admin_id, data.email, data.password),
)
await db.execute(
"""
INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey)
VALUES (?, ?, ?, ?, ?, ?)
""",
(wallet.id, data.admin_id, data.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,))

View 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
);
"""
)

View file

@ -0,0 +1,31 @@
from pydantic import BaseModel
from fastapi.param_functions import Query
from sqlite3 import Row
class CreateUserData(BaseModel):
user_name: str = Query(...)
wallet_name: str = Query(...)
admin_id: str = Query(...)
email: str = Query("")
password: str = Query("")
class Users(BaseModel):
id: str
name: str
admin: str
email: str
password: str
class Wallets(BaseModel):
id: str
admin: str
name: str
user: str
adminkey: str
inkey: str
@classmethod
def from_row(cls, row: Row) -> "Wallets":
return cls(**dict(row))

View file

@ -0,0 +1,264 @@
<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: {{ 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/&lt;user_id&gt;</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/&lt;user_id&gt; -H "X-Api-Key: {{
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/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</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/&lt;user_id&gt; -H "X-Api-Key: {{
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&lt;wallet_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</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&lt;wallet_id&gt; -H "X-Api-Key: {{
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": &lt;string&gt;, "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": &lt;string&gt;, "user_name": &lt;string&gt;,
"wallet_name": &lt;string&gt;,"email": &lt;Optional string&gt;
,"password": &lt;Optional string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"id": &lt;string&gt;, "name": &lt;string&gt;, "admin":
&lt;string&gt;, "email": &lt;string&gt;, "password":
&lt;string&gt;}</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": "{{ user.id }}", "wallet_name": &lt;string&gt;,
"user_name": &lt;string&gt;, "email": &lt;Optional string&gt;,
"password": &lt; Optional string&gt;}' -H "X-Api-Key: {{
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": &lt;string&gt;, "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": &lt;string&gt;, "wallet_name": &lt;string&gt;,
"admin_id": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"id": &lt;string&gt;, "admin": &lt;string&gt;, "name":
&lt;string&gt;, "user": &lt;string&gt;, "adminkey": &lt;string&gt;,
"inkey": &lt;string&gt;}</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": &lt;string&gt;, "wallet_name": &lt;string&gt;,
"admin_id": "{{ user.id }}"}' -H "X-Api-Key: {{ 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/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</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/&lt;user_id&gt; -H "X-Api-Key: {{
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/&lt;wallet_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</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/&lt;wallet_id&gt; -H "X-Api-Key: {{
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": &lt;string&gt;}</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": &lt;string&gt;, "extension": &lt;string&gt;, "active":
&lt;integer&gt;}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H
"Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View file

@ -0,0 +1,474 @@
{% 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
})
self.getWallets()
})
.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 %}

View file

@ -0,0 +1,13 @@
from fastapi import FastAPI, Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import usermanager_ext, usermanager_renderer
@usermanager_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return usermanager_renderer().TemplateResponse("usermanager/index.html", {"request": request,"user": user.dict()})

View file

@ -0,0 +1,130 @@
from http import HTTPStatus
from starlette.exceptions import HTTPException
from fastapi import Query
from fastapi.params import Depends
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type
from . import usermanager_ext
from .models import CreateUserData
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.get("/api/v1/users", status_code=HTTPStatus.OK)
async def api_usermanager_users(wallet: WalletTypeInfo = Depends(get_key_type)):
user_id = wallet.wallet.user
return [user.dict() for user in await get_usermanager_users(user_id)]
@usermanager_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
async def api_usermanager_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
user = await get_usermanager_user(user_id)
return user.dict()
@usermanager_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
# @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(data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)):
user = await create_usermanager_user(data)
full = user.dict()
full["wallets"] = [wallet.dict() for wallet in await get_usermanager_users_wallets(user.id)]
return full
@usermanager_ext.delete("/api/v1/users/{user_id}")
async def api_usermanager_users_delete(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
user = await get_usermanager_user(user_id)
if not user:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="User does not exist."
)
await delete_usermanager_user(user_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
###Activate Extension
@usermanager_ext.post("/api/v1/extensions")
async def api_usermanager_activate_extension(extension: str = Query(...), userid: str = Query(...), active: bool = Query(...)):
user = await get_user(userid)
if not user:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="User does not exist."
)
update_user_extension(
user_id=userid, extension=extension, active=active
)
return {"extension": "updated"}
###Wallets
@usermanager_ext.post("/api/v1/wallets")
async def api_usermanager_wallets_create(
wallet: WalletTypeInfo = Depends(get_key_type),
user_id: str = Query(...),
wallet_name: str = Query(...),
admin_id: str = Query(...)
):
user = await create_usermanager_wallet(
user_id, wallet_name, admin_id
)
return user.dict()
@usermanager_ext.get("/api/v1/wallets")
async def api_usermanager_wallets(wallet: WalletTypeInfo = Depends(get_key_type)):
admin_id = wallet.wallet.user
return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)]
@usermanager_ext.get("/api/v1/wallets/{wallet_id}")
async def api_usermanager_wallet_transactions(wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)):
return await get_usermanager_wallet_transactions(wallet_id)
@usermanager_ext.get("/api/v1/wallets/{user_id}")
async def api_usermanager_users_wallets(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
# wallet = await get_usermanager_users_wallets(user_id)
return [s_wallet.dict() for s_wallet in await get_usermanager_users_wallets(user_id)]
@usermanager_ext.delete("/api/v1/wallets/{wallet_id}")
async def api_usermanager_wallets_delete(wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)):
get_wallet = await get_usermanager_wallet(wallet_id)
if not get_wallet:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Wallet does not exist."
)
await delete_usermanager_wallet(wallet_id, get_wallet.user)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)

View file

@ -61,7 +61,7 @@ async def get_withdraw_link(link_id: str, num=0) -> Optional[WithdrawLink]:
# for item in row: # for item in row:
# link.append(item) # link.append(item)
# link.append(num) # link.append(num)
print("GET_LINK", WithdrawLink.from_row(row)) # print("GET_LINK", WithdrawLink.from_row(row))
return WithdrawLink.from_row(row) return WithdrawLink.from_row(row)

View file

@ -3,7 +3,9 @@ from http import HTTPStatus
from datetime import datetime from datetime import datetime
from lnbits.core.services import pay_invoice from lnbits.core.services import pay_invoice
from fastapi.param_functions import Query
from starlette.requests import Request from starlette.requests import Request
from starlette.exceptions import HTTPException
from . import withdraw_ext from . import withdraw_ext
from .crud import get_withdraw_link_by_hash, update_withdraw_link from .crud import get_withdraw_link_by_hash, update_withdraw_link
@ -80,19 +82,17 @@ async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash
# HTTPStatus.OK, # HTTPStatus.OK,
# ) # )
return link.lnurl_response(request).dict() return link.lnurl_response(req=request).dict()
# CALLBACK # CALLBACK
@withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", status_code=HTTPStatus.OK, name="withdraw.api_lnurl_callback") @withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", status_code=HTTPStatus.OK, name="withdraw.api_lnurl_callback")
async def api_lnurl_callback(unique_hash): async def api_lnurl_callback(unique_hash, k1: str = Query(...), pr: str = Query(...)):
link = await get_withdraw_link_by_hash(unique_hash) link = await get_withdraw_link_by_hash(unique_hash)
k1 = request.query_params['k1'] payment_request = pr
payment_request = request.query_params['pr']
now = int(datetime.now().timestamp()) now = int(datetime.now().timestamp())
if not link: if not link:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, status_code=HTTPStatus.NOT_FOUND,

View file

@ -62,7 +62,7 @@ class WithdrawLink(BaseModel):
def lnurl_response(self, req: Request) -> LnurlWithdrawResponse: def lnurl_response(self, req: Request) -> LnurlWithdrawResponse:
url = req.url_for( url = req.url_for(
"withdraw.api_lnurl_callback", unique_hash=self.unique_hash, _external=True name="withdraw.api_lnurl_callback", unique_hash=self.unique_hash
) )
return LnurlWithdrawResponse( return LnurlWithdrawResponse(
callback=url, callback=url,

View file

@ -7,10 +7,10 @@
{% if link.is_spent %} {% if link.is_spent %}
<q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge> <q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge>
{% endif %} {% endif %}
<a href="lightning://{{ link.lnurl }}"> <a href="lightning://{{ lnurl }}">
<q-responsive :ratio="1" class="q-mx-md"> <q-responsive :ratio="1" class="q-mx-md">
<qrcode <qrcode
:value="this.here + '/?lightning={{link.lnurl }}'" :value="this.here + '/?lightning={{lnurl }}'"
:options="{width: 800}" :options="{width: 800}"
class="rounded-borders" class="rounded-borders"
></qrcode> ></qrcode>
@ -18,7 +18,7 @@
</a> </a>
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText('{{ link.lnurl }}')" <q-btn outline color="grey" @click="copyText('{{ lnurl }}')"
>Copy LNURL</q-btn >Copy LNURL</q-btn
> >
</div> </div>

View file

@ -33,7 +33,8 @@ async def display(request: Request, link_id):
) )
# response.status_code = HTTPStatus.NOT_FOUND # response.status_code = HTTPStatus.NOT_FOUND
# return "Withdraw link does not exist." #probably here is where we should return the 404?? # return "Withdraw link does not exist." #probably here is where we should return the 404??
return withdraw_renderer().TemplateResponse("withdraw/display.html", {"request":request,"link":{**link.dict(), "lnurl": link.lnurl(request)}, "unique":True}) print("LINK", link)
return withdraw_renderer().TemplateResponse("withdraw/display.html", {"request":request,"link":link.dict(), "lnurl": link.lnurl(req=request), "unique":True})
@withdraw_ext.get("/img/{link_id}", response_class=StreamingResponse) @withdraw_ext.get("/img/{link_id}", response_class=StreamingResponse)