From de97c0f696f28bab040d5d6d966586524b818f99 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Mon, 11 Oct 2021 11:46:36 +0100 Subject: [PATCH 1/5] lnurlp import execption withdraw not working --- lnbits/extensions/lnurlp/lnurl.py | 1 + lnbits/extensions/withdraw/crud.py | 2 +- lnbits/extensions/withdraw/lnurl.py | 10 +++++----- lnbits/extensions/withdraw/models.py | 2 +- .../withdraw/templates/withdraw/display.html | 6 +++--- lnbits/extensions/withdraw/views.py | 3 ++- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lnbits/extensions/lnurlp/lnurl.py b/lnbits/extensions/lnurlp/lnurl.py index ca17646fe..fc6cc5459 100644 --- a/lnbits/extensions/lnurlp/lnurl.py +++ b/lnbits/extensions/lnurlp/lnurl.py @@ -2,6 +2,7 @@ import hashlib import math from http import HTTPStatus from fastapi import FastAPI, Request +from starlette.exceptions import HTTPException from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore from lnbits.core.services import create_invoice diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py index 183d86298..839e7a400 100644 --- a/lnbits/extensions/withdraw/crud.py +++ b/lnbits/extensions/withdraw/crud.py @@ -61,7 +61,7 @@ async def get_withdraw_link(link_id: str, num=0) -> Optional[WithdrawLink]: # for item in row: # link.append(item) # link.append(num) - print("GET_LINK", WithdrawLink.from_row(row)) + # print("GET_LINK", WithdrawLink.from_row(row)) return WithdrawLink.from_row(row) diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 75339cf72..dadc52e05 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -3,7 +3,9 @@ from http import HTTPStatus from datetime import datetime from lnbits.core.services import pay_invoice +from fastapi.param_functions import Query from starlette.requests import Request +from starlette.exceptions import HTTPException from . import withdraw_ext 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, # ) - return link.lnurl_response(request).dict() + return link.lnurl_response(req=request).dict() # 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) - k1 = request.query_params['k1'] - payment_request = request.query_params['pr'] + payment_request = pr now = int(datetime.now().timestamp()) - if not link: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 55b234e3a..b01ad6544 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -62,7 +62,7 @@ class WithdrawLink(BaseModel): def lnurl_response(self, req: Request) -> LnurlWithdrawResponse: 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( callback=url, diff --git a/lnbits/extensions/withdraw/templates/withdraw/display.html b/lnbits/extensions/withdraw/templates/withdraw/display.html index f4d6ef9d2..245b3ed1a 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/display.html +++ b/lnbits/extensions/withdraw/templates/withdraw/display.html @@ -7,10 +7,10 @@ {% if link.is_spent %} Withdraw is spent. {% endif %} - + @@ -18,7 +18,7 @@
- Copy LNURL
diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index eeacb36e7..8d6eeee20 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -33,7 +33,8 @@ async def display(request: Request, link_id): ) # response.status_code = HTTPStatus.NOT_FOUND # 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) From 706d5843321eb80cb62bb20f62634368e3c669ee Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Mon, 11 Oct 2021 11:52:21 +0100 Subject: [PATCH 2/5] added usermanager extension --- lnbits/extensions/usermanager/README.md | 26 + lnbits/extensions/usermanager/__init__.py | 12 + lnbits/extensions/usermanager/config.json | 6 + lnbits/extensions/usermanager/crud.py | 122 +++++ lnbits/extensions/usermanager/migrations.py | 31 ++ lnbits/extensions/usermanager/models.py | 23 + .../templates/usermanager/_api_docs.html | 259 ++++++++++ .../templates/usermanager/index.html | 473 ++++++++++++++++++ lnbits/extensions/usermanager/views.py | 12 + lnbits/extensions/usermanager/views_api.py | 156 ++++++ 10 files changed, 1120 insertions(+) create mode 100644 lnbits/extensions/usermanager/README.md create mode 100644 lnbits/extensions/usermanager/__init__.py create mode 100644 lnbits/extensions/usermanager/config.json create mode 100644 lnbits/extensions/usermanager/crud.py create mode 100644 lnbits/extensions/usermanager/migrations.py create mode 100644 lnbits/extensions/usermanager/models.py create mode 100644 lnbits/extensions/usermanager/templates/usermanager/_api_docs.html create mode 100644 lnbits/extensions/usermanager/templates/usermanager/index.html create mode 100644 lnbits/extensions/usermanager/views.py create mode 100644 lnbits/extensions/usermanager/views_api.py diff --git a/lnbits/extensions/usermanager/README.md b/lnbits/extensions/usermanager/README.md new file mode 100644 index 000000000..b6f306275 --- /dev/null +++ b/lnbits/extensions/usermanager/README.md @@ -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) diff --git a/lnbits/extensions/usermanager/__init__.py b/lnbits/extensions/usermanager/__init__.py new file mode 100644 index 000000000..53154812d --- /dev/null +++ b/lnbits/extensions/usermanager/__init__.py @@ -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 diff --git a/lnbits/extensions/usermanager/config.json b/lnbits/extensions/usermanager/config.json new file mode 100644 index 000000000..7391ec299 --- /dev/null +++ b/lnbits/extensions/usermanager/config.json @@ -0,0 +1,6 @@ +{ + "name": "User Manager", + "short_description": "Generate users and wallets", + "icon": "person_add", + "contributors": ["benarc"] +} diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py new file mode 100644 index 000000000..a7854ad8f --- /dev/null +++ b/lnbits/extensions/usermanager/crud.py @@ -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,)) diff --git a/lnbits/extensions/usermanager/migrations.py b/lnbits/extensions/usermanager/migrations.py new file mode 100644 index 000000000..62a215752 --- /dev/null +++ b/lnbits/extensions/usermanager/migrations.py @@ -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 + ); + """ + ) diff --git a/lnbits/extensions/usermanager/models.py b/lnbits/extensions/usermanager/models.py new file mode 100644 index 000000000..97eaaea8c --- /dev/null +++ b/lnbits/extensions/usermanager/models.py @@ -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)) diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html new file mode 100644 index 000000000..74640bb80 --- /dev/null +++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html @@ -0,0 +1,259 @@ + + + +
+ User Manager: Make and manager 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 + develpoers stack as the user and wallet manager.
+ + Created by, Ben Arc +

+
+
+
+ + + + + GET + /usermanager/api/v1/users +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON list of users +
Curl example
+ curl -X GET {{ request.url_root }}usermanager/api/v1/users -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /usermanager/api/v1/users/<user_id> +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON list of users +
Curl example
+ curl -X GET {{ request.url_root }}usermanager/api/v1/users/<user_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /usermanager/api/v1/wallets/<user_id> +
Headers
+ {"X-Api-Key": <string>} +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON wallet data +
Curl example
+ curl -X GET {{ request.url_root }}usermanager/api/v1/wallets/<user_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /usermanager/api/v1/wallets<wallet_id> +
Headers
+ {"X-Api-Key": <string>} +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON a wallets transactions +
Curl example
+ curl -X GET {{ request.url_root }}usermanager/api/v1/wallets<wallet_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + POST + /usermanager/api/v1/users +
Headers
+ {"X-Api-Key": <string>, "Content-type": + "application/json"} +
+ Body (application/json) - "admin_id" is a YOUR user ID +
+ {"admin_id": <string>, "user_name": <string>, + "wallet_name": <string>,"email": <Optional string> + ,"password": <Optional string>} +
+ Returns 201 CREATED (application/json) +
+ {"id": <string>, "name": <string>, "admin": + <string>, "email": <string>, "password": + <string>} +
Curl example
+ 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" + +
+
+
+ + + + POST + /usermanager/api/v1/wallets +
Headers
+ {"X-Api-Key": <string>, "Content-type": + "application/json"} +
+ Body (application/json) - "admin_id" is a YOUR user ID +
+ {"user_id": <string>, "wallet_name": <string>, + "admin_id": <string>} +
+ Returns 201 CREATED (application/json) +
+ {"id": <string>, "admin": <string>, "name": + <string>, "user": <string>, "adminkey": <string>, + "inkey": <string>} +
Curl example
+ 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" + +
+
+
+ + + + DELETE + /usermanager/api/v1/users/<user_id> +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X DELETE {{ request.url_root }}usermanager/api/v1/users/<user_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + DELETE + /usermanager/api/v1/wallets/<wallet_id> +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X DELETE {{ request.url_root }}usermanager/api/v1/wallets/<wallet_id> + -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + POST + /usermanager/api/v1/extensions +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ 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" + +
+
+
+
diff --git a/lnbits/extensions/usermanager/templates/usermanager/index.html b/lnbits/extensions/usermanager/templates/usermanager/index.html new file mode 100644 index 000000000..446ee51d5 --- /dev/null +++ b/lnbits/extensions/usermanager/templates/usermanager/index.html @@ -0,0 +1,473 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + New User + New Wallet + + + + + + +
+
+
Users
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + +
+
+
Wallets
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ +
+ + +
+ {{SITE_TITLE}} User Manager Extension +
+
+ + + {% include "usermanager/_api_docs.html" %} + +
+
+ + + + + + + + + + Create User + Cancel + + + + + + + + + + + Create Wallet + Cancel + + + +
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/usermanager/views.py b/lnbits/extensions/usermanager/views.py new file mode 100644 index 000000000..df6949c64 --- /dev/null +++ b/lnbits/extensions/usermanager/views.py @@ -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) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py new file mode 100644 index 000000000..d3bba6ad3 --- /dev/null +++ b/lnbits/extensions/usermanager/views_api.py @@ -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/", 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/", 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", 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/", 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/", 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 From e5b22ead0c5ec2debcd1612d3aad9a1c608015d2 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Tue, 12 Oct 2021 10:38:09 +0100 Subject: [PATCH 3/5] initial work --- lnbits/extensions/usermanager/__init__.py | 19 ++++- lnbits/extensions/usermanager/models.py | 14 +++- lnbits/extensions/usermanager/views.py | 19 +++-- lnbits/extensions/usermanager/views_api.py | 95 ++++++++++------------ 4 files changed, 81 insertions(+), 66 deletions(-) diff --git a/lnbits/extensions/usermanager/__init__.py b/lnbits/extensions/usermanager/__init__.py index 53154812d..f421a15c2 100644 --- a/lnbits/extensions/usermanager/__init__.py +++ b/lnbits/extensions/usermanager/__init__.py @@ -1,12 +1,25 @@ -from quart import Blueprint +import asyncio + +from fastapi import APIRouter + from lnbits.db import Database +from lnbits.helpers import template_renderer db = Database("ext_usermanager") -usermanager_ext: Blueprint = Blueprint( - "usermanager", __name__, static_folder="static", template_folder="templates" +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 diff --git a/lnbits/extensions/usermanager/models.py b/lnbits/extensions/usermanager/models.py index 97eaaea8c..7e6a95950 100644 --- a/lnbits/extensions/usermanager/models.py +++ b/lnbits/extensions/usermanager/models.py @@ -1,8 +1,16 @@ -from typing import NamedTuple +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(None) + password: str = Query(None) -class Users(NamedTuple): + +class Users(BaseModel): id: str name: str admin: str @@ -10,7 +18,7 @@ class Users(NamedTuple): password: str -class Wallets(NamedTuple): +class Wallets(BaseModel): id: str admin: str name: str diff --git a/lnbits/extensions/usermanager/views.py b/lnbits/extensions/usermanager/views.py index df6949c64..d58a98265 100644 --- a/lnbits/extensions/usermanager/views.py +++ b/lnbits/extensions/usermanager/views.py @@ -1,12 +1,13 @@ -from quart import g, render_template +from fastapi import FastAPI, Request +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates +from starlette.responses import HTMLResponse -from lnbits.decorators import check_user_exists, validate_uuids +from lnbits.core.models import User +from lnbits.decorators import check_user_exists -from . import usermanager_ext +from . import usermanager_ext, usermanager_renderer - -@usermanager_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(): - return await render_template("usermanager/index.html", user=g.user) +@usermanager_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return await render_template("usermanager/index.html", user=user.dict()) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index d3bba6ad3..4e41c8aaa 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -1,10 +1,15 @@ -from quart import g, jsonify 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 api_check_wallet_key, api_validate_post_request +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, @@ -23,74 +28,62 @@ 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.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.route("/api/v1/users/", methods=["GET"]) -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_user(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 ( - jsonify(user._asdict()), - HTTPStatus.OK, - ) + return user.dict() -@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.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.route("/api/v1/users/", methods=["DELETE"]) -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_users_delete(user_id): +@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: - return jsonify({"message": "User does not exist."}), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="User does not exist." + ) await delete_usermanager_user(user_id) - return "", HTTPStatus.NO_CONTENT + raise HTTPException(status_code=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"]) +@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: - return jsonify({"message": "no such user"}), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="User does not exist." + ) update_user_extension( - user_id=g.data["userid"], extension=g.data["extension"], active=g.data["active"] + user_id=userid, extension=extension, active=active ) - return jsonify({"extension": "updated"}), HTTPStatus.CREATED + return {"extension": "updated"} ###Wallets From f4dfc1b68978f1e52e2dbdc234850febcd337db7 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Tue, 12 Oct 2021 10:57:15 +0100 Subject: [PATCH 4/5] usermanager views_api --- lnbits/extensions/usermanager/views_api.py | 79 ++++++++-------------- 1 file changed, 30 insertions(+), 49 deletions(-) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index 4e41c8aaa..e8a78bf19 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -5,7 +5,6 @@ from fastapi import Query from fastapi.params import Depends from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import WalletTypeInfo, get_key_type from . import usermanager_ext @@ -89,61 +88,43 @@ async def api_usermanager_activate_extension(extension: str = Query(...), userid ###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(): +@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( - g.data["user_id"], g.data["wallet_name"], g.data["admin_id"] + user_id, wallet_name, admin_id ) - return jsonify(user._asdict()), HTTPStatus.CREATED + return user.dict() -@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.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.route("/api/v1/wallets", 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.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.route("/api/v1/wallets/", 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.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.route("/api/v1/wallets/", 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 +@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) From 04462f337ffa2fc0f43b103e0ef848b926102c6a Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Tue, 12 Oct 2021 17:04:49 +0100 Subject: [PATCH 5/5] usermanager working --- lnbits/extensions/usermanager/crud.py | 15 +++--- lnbits/extensions/usermanager/models.py | 4 +- .../templates/usermanager/_api_docs.html | 53 ++++++++++--------- .../templates/usermanager/index.html | 1 + lnbits/extensions/usermanager/views.py | 2 +- lnbits/extensions/usermanager/views_api.py | 2 +- 6 files changed, 40 insertions(+), 37 deletions(-) diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py index a7854ad8f..e1c8aebdb 100644 --- a/lnbits/extensions/usermanager/crud.py +++ b/lnbits/extensions/usermanager/crud.py @@ -10,31 +10,27 @@ from lnbits.core.crud import ( ) from . import db -from .models import Users, Wallets +from .models import Users, Wallets, CreateUserData ### Users async def create_usermanager_user( - user_name: str, - wallet_name: str, - admin_id: str, - email: Optional[str] = None, - password: Optional[str] = None, + 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=wallet_name) + 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, user_name, admin_id, email, password), + (user.id, data.user_name, data.admin_id, data.email, data.password), ) await db.execute( @@ -42,7 +38,7 @@ async def create_usermanager_user( INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey) VALUES (?, ?, ?, ?, ?, ?) """, - (wallet.id, admin_id, wallet_name, user.id, wallet.adminkey, wallet.inkey), + (wallet.id, data.admin_id, data.wallet_name, user.id, wallet.adminkey, wallet.inkey), ) user_created = await get_usermanager_user(user.id) @@ -59,6 +55,7 @@ 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] diff --git a/lnbits/extensions/usermanager/models.py b/lnbits/extensions/usermanager/models.py index 7e6a95950..005ed8afb 100644 --- a/lnbits/extensions/usermanager/models.py +++ b/lnbits/extensions/usermanager/models.py @@ -6,8 +6,8 @@ class CreateUserData(BaseModel): user_name: str = Query(...) wallet_name: str = Query(...) admin_id: str = Query(...) - email: str = Query(None) - password: str = Query(None) + email: str = Query("") + password: str = Query("") class Users(BaseModel): diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html index 74640bb80..1944416b4 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html +++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html @@ -42,8 +42,8 @@ JSON list of users
Curl example
curl -X GET {{ request.url_root }}usermanager/api/v1/users -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root }}usermanager/api/v1/users -H + "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -62,8 +62,9 @@ JSON list of users
Curl example
curl -X GET {{ request.url_root }}usermanager/api/v1/users/<user_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root + }}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -84,8 +85,9 @@ JSON wallet data
Curl example
curl -X GET {{ request.url_root }}usermanager/api/v1/wallets/<user_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root + }}usermanager/api/v1/wallets/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -106,8 +108,9 @@ JSON a wallets transactions
Curl example
curl -X GET {{ request.url_root }}usermanager/api/v1/wallets<wallet_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root + }}usermanager/api/v1/wallets<wallet_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -147,11 +150,11 @@ >
Curl example
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" + >curl -X POST {{ request.url_root }}usermanager/api/v1/users -d + '{"admin_id": "{{ user.id }}", "wallet_name": <string>, + "user_name": <string>, "email": <Optional string>, + "password": < Optional string>}' -H "X-Api-Key: {{ + user.wallets[0].inkey }}" -H "Content-type: application/json" @@ -185,10 +188,10 @@ >
Curl example
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" + >curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d + '{"user_id": <string>, "wallet_name": <string>, + "admin_id": "{{ user.id }}"}' -H "X-Api-Key: {{ user.wallets[0].inkey + }}" -H "Content-type: application/json" @@ -209,8 +212,9 @@ {"X-Api-Key": <string>}
Curl example
curl -X DELETE {{ request.url_root }}usermanager/api/v1/users/<user_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X DELETE {{ request.url_root + }}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -226,8 +230,9 @@ {"X-Api-Key": <string>}
Curl example
curl -X DELETE {{ request.url_root }}usermanager/api/v1/wallets/<wallet_id> - -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X DELETE {{ request.url_root + }}usermanager/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -248,9 +253,9 @@ {"X-Api-Key": <string>}
Curl example
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 + >curl -X POST {{ request.url_root }}usermanager/api/v1/extensions -d + '{"userid": <string>, "extension": <string>, "active": + <integer>}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type: application/json" diff --git a/lnbits/extensions/usermanager/templates/usermanager/index.html b/lnbits/extensions/usermanager/templates/usermanager/index.html index 446ee51d5..6fbe9686d 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/index.html +++ b/lnbits/extensions/usermanager/templates/usermanager/index.html @@ -368,6 +368,7 @@ self.users = _.reject(self.users, function (obj) { return obj.id == userId }) + self.getWallets() }) .catch(function (error) { LNbits.utils.notifyApiError(error) diff --git a/lnbits/extensions/usermanager/views.py b/lnbits/extensions/usermanager/views.py index d58a98265..395e0c0ba 100644 --- a/lnbits/extensions/usermanager/views.py +++ b/lnbits/extensions/usermanager/views.py @@ -10,4 +10,4 @@ from . import usermanager_ext, usermanager_renderer @usermanager_ext.get("/", response_class=HTMLResponse) async def index(request: Request, user: User = Depends(check_user_exists)): - return await render_template("usermanager/index.html", user=user.dict()) + return usermanager_renderer().TemplateResponse("usermanager/index.html", {"request": request,"user": user.dict()}) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index e8a78bf19..caa513c82 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -50,7 +50,7 @@ async def api_usermanager_user(user_id, wallet: WalletTypeInfo = Depends(get_key # } # ) async def api_usermanager_users_create(data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)): - user = await create_usermanager_user(**data) + 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