From 0668fd79b69ad1f21db9acaddf45141ee21396d7 Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 28 Jan 2023 21:01:01 +0000 Subject: [PATCH] Removed usermanager --- lnbits/extensions/usermanager/README.md | 26 - lnbits/extensions/usermanager/__init__.py | 25 - lnbits/extensions/usermanager/config.json | 6 - lnbits/extensions/usermanager/crud.py | 119 ----- lnbits/extensions/usermanager/migrations.py | 31 -- lnbits/extensions/usermanager/models.py | 40 -- .../usermanager/static/image/usermanager.png | Bin 23993 -> 0 bytes .../templates/usermanager/_api_docs.html | 277 ---------- .../templates/usermanager/index.html | 474 ------------------ lnbits/extensions/usermanager/views.py | 14 - lnbits/extensions/usermanager/views_api.py | 135 ----- tests/data/mock_data.zip | Bin 44350 -> 43615 bytes 12 files changed, 1147 deletions(-) delete mode 100644 lnbits/extensions/usermanager/README.md delete mode 100644 lnbits/extensions/usermanager/__init__.py delete mode 100644 lnbits/extensions/usermanager/config.json delete mode 100644 lnbits/extensions/usermanager/crud.py delete mode 100644 lnbits/extensions/usermanager/migrations.py delete mode 100644 lnbits/extensions/usermanager/models.py delete mode 100644 lnbits/extensions/usermanager/static/image/usermanager.png delete mode 100644 lnbits/extensions/usermanager/templates/usermanager/_api_docs.html delete mode 100644 lnbits/extensions/usermanager/templates/usermanager/index.html delete mode 100644 lnbits/extensions/usermanager/views.py delete mode 100644 lnbits/extensions/usermanager/views_api.py diff --git a/lnbits/extensions/usermanager/README.md b/lnbits/extensions/usermanager/README.md deleted file mode 100644 index b6f30627..00000000 --- a/lnbits/extensions/usermanager/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# 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 deleted file mode 100644 index 8d8ff557..00000000 --- a/lnbits/extensions/usermanager/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from fastapi import APIRouter -from fastapi.staticfiles import StaticFiles - -from lnbits.db import Database -from lnbits.helpers import template_renderer - -db = Database("ext_usermanager") - -usermanager_ext: APIRouter = APIRouter(prefix="/usermanager", tags=["usermanager"]) - -usermanager_static_files = [ - { - "path": "/usermanager/static", - "app": StaticFiles(directory="lnbits/extensions/usermanager/static"), - "name": "usermanager_static", - } -] - - -def usermanager_renderer(): - return template_renderer(["lnbits/extensions/usermanager/templates"]) - - -from .views import * # noqa -from .views_api import * # noqa diff --git a/lnbits/extensions/usermanager/config.json b/lnbits/extensions/usermanager/config.json deleted file mode 100644 index 0b38a083..00000000 --- a/lnbits/extensions/usermanager/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "User Manager", - "short_description": "Generate users and wallets", - "tile": "/usermanager/static/image/usermanager.png", - "contributors": ["benarc"] -} diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py deleted file mode 100644 index 959914f2..00000000 --- a/lnbits/extensions/usermanager/crud.py +++ /dev/null @@ -1,119 +0,0 @@ -from typing import List, Optional - -from lnbits.core.crud import ( - create_account, - create_wallet, - delete_wallet, - get_payments, - get_user, -) -from lnbits.core.models import Payment - -from . import db -from .models import CreateUserData, User, Wallet - - -async def create_usermanager_user(data: CreateUserData) -> User: - 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[User]: - row = await db.fetchone("SELECT * FROM usermanager.users WHERE id = ?", (user_id,)) - return User(**row) if row else None - - -async def get_usermanager_users(user_id: str) -> List[User]: - rows = await db.fetchall( - "SELECT * FROM usermanager.users WHERE admin = ?", (user_id,) - ) - - return [User(**row) for row in rows] - - -async def delete_usermanager_user(user_id: str, delete_core: bool = True) -> None: - if delete_core: - 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,)) - - -async def create_usermanager_wallet( - user_id: str, wallet_name: str, admin_id: str -) -> Wallet: - 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[Wallet]: - row = await db.fetchone( - "SELECT * FROM usermanager.wallets WHERE id = ?", (wallet_id,) - ) - return Wallet(**row) if row else None - - -async def get_usermanager_wallets(admin_id: str) -> List[Wallet]: - rows = await db.fetchall( - "SELECT * FROM usermanager.wallets WHERE admin = ?", (admin_id,) - ) - return [Wallet(**row) for row in rows] - - -async def get_usermanager_users_wallets(user_id: str) -> List[Wallet]: - rows = await db.fetchall( - """SELECT * FROM usermanager.wallets WHERE "user" = ?""", (user_id,) - ) - return [Wallet(**row) for row in rows] - - -async def get_usermanager_wallet_transactions(wallet_id: str) -> List[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 deleted file mode 100644 index 62a21575..00000000 --- a/lnbits/extensions/usermanager/migrations.py +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 05122cc8..00000000 --- a/lnbits/extensions/usermanager/models.py +++ /dev/null @@ -1,40 +0,0 @@ -from sqlite3 import Row -from typing import Optional - -from fastapi.param_functions import Query -from pydantic import BaseModel - - -class CreateUserData(BaseModel): - user_name: str = Query(...) - wallet_name: str = Query(...) - admin_id: str = Query(...) - email: str = Query("") - password: str = Query("") - - -class CreateUserWallet(BaseModel): - user_id: str = Query(...) - wallet_name: str = Query(...) - admin_id: str = Query(...) - - -class User(BaseModel): - id: str - name: str - admin: str - email: Optional[str] = None - password: Optional[str] = None - - -class Wallet(BaseModel): - id: str - admin: str - name: str - user: str - adminkey: str - inkey: str - - @classmethod - def from_row(cls, row: Row) -> "Wallet": - return cls(**dict(row)) diff --git a/lnbits/extensions/usermanager/static/image/usermanager.png b/lnbits/extensions/usermanager/static/image/usermanager.png deleted file mode 100644 index a38b2b3e07c332d3da5e78c845c2df83fa6591ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23993 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 Dn^C=h diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html deleted file mode 100644 index 55e9f5b3..00000000 --- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html +++ /dev/null @@ -1,277 +0,0 @@ - - - -
- 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 - developers stack as the user and wallet manager.
- - Created by, - Ben Arc -

-
-
-
- - - - - - GET - /usermanager/api/v1/users -
Body (application/json)
-
- Returns 200 OK (application/json) -
- JSON list of users -
Curl example
- curl -X GET {{ request.base_url }}usermanager/api/v1/users -H - "X-Api-Key: {{ user.wallets[0].adminkey }}" - -
-
-
- - - - GET - /usermanager/api/v1/users/<user_id> -
Body (application/json)
- -
- Returns 200 OK (application/json) -
- {"id": <string>, "name": <string>, "admin": - <string>, "email": <string>, "password": - <string>} - -
Curl example
- curl -X GET {{ request.base_url - }}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{ - user.wallets[0].inkey }}" - -
-
-
- - - - GET - /usermanager/api/v1/wallets -
Headers
- {"X-Api-Key": <string>} -
Body (application/json)
-
- Returns 200 OK (application/json) -
- JSON wallet data -
Curl example
- curl -X GET {{ request.base_url }}usermanager/api/v1/wallets -H - "X-Api-Key: {{ user.wallets[0].adminkey }}" - -
-
-
- - - - GET - /usermanager/api/v1/transactions/<wallet_id> -
Headers
- {"X-Api-Key": <string>} -
Body (application/json)
-
- Returns 200 OK (application/json) -
- JSON a wallets transactions -
Curl example
- curl -X GET {{ request.base_url - }}usermanager/api/v1/transactions/<wallet_id> -H "X-Api-Key: {{ - 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.base_url }}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" - -
-
-
- - - - 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.base_url }}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" - -
-
-
- - - - DELETE - /usermanager/api/v1/users/<user_id> -
Headers
- {"X-Api-Key": <string>} -
Curl example
- curl -X DELETE {{ request.base_url - }}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
- - - - DELETE - /usermanager/api/v1/wallets/<wallet_id> -
Headers
- {"X-Api-Key": <string>} -
Curl example
- curl -X DELETE {{ request.base_url - }}usermanager/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
- - - - POST - /usermanager/api/v1/extensions -
Headers
- {"X-Api-Key": <string>} -
Curl example
- curl -X POST {{ request.base_url - }}usermanager/api/v1/extensions?extension=withdraw&userid=user_id&active=true - -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type: - application/json" - -
- Returns 200 OK (application/json) -
- {"extension": "updated"} -
-
-
-
diff --git a/lnbits/extensions/usermanager/templates/usermanager/index.html b/lnbits/extensions/usermanager/templates/usermanager/index.html deleted file mode 100644 index da11ad44..00000000 --- a/lnbits/extensions/usermanager/templates/usermanager/index.html +++ /dev/null @@ -1,474 +0,0 @@ -{% 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 deleted file mode 100644 index 115cbfa6..00000000 --- a/lnbits/extensions/usermanager/views.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import Depends, Request -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()} - ) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py deleted file mode 100644 index 27c6f3cb..00000000 --- a/lnbits/extensions/usermanager/views_api.py +++ /dev/null @@ -1,135 +0,0 @@ -from http import HTTPStatus - -from fastapi import Depends, Query -from starlette.exceptions import HTTPException - -from lnbits.core import update_user_extension -from lnbits.core.crud import get_user -from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key - -from . import usermanager_ext -from .crud import ( - create_usermanager_user, - create_usermanager_wallet, - delete_usermanager_user, - delete_usermanager_wallet, - get_usermanager_user, - get_usermanager_users, - get_usermanager_users_wallets, - get_usermanager_wallet, - get_usermanager_wallet_transactions, - get_usermanager_wallets, -) -from .models import CreateUserData, CreateUserWallet - - -@usermanager_ext.get("/api/v1/users", status_code=HTTPStatus.OK) -async def api_usermanager_users( - wallet: WalletTypeInfo = Depends(require_admin_key), -): - 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, - dependencies=[Depends(get_key_type)], -) -async def api_usermanager_user(user_id): - user = await get_usermanager_user(user_id) - return user.dict() if user else None - - -@usermanager_ext.post( - "/api/v1/users", - status_code=HTTPStatus.CREATED, - dependencies=[Depends(get_key_type)], -) -async def api_usermanager_users_create(data: CreateUserData): - 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}", dependencies=[Depends(require_admin_key)] -) -async def api_usermanager_users_delete( - user_id, - delete_core: bool = Query(True), -): - 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, delete_core) - return "", 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." - ) - await update_user_extension(user_id=userid, extension=extension, active=active) - return {"extension": "updated"} - - -# Wallets - - -@usermanager_ext.post("/api/v1/wallets", dependencies=[Depends(get_key_type)]) -async def api_usermanager_wallets_create(data: CreateUserWallet): - user = await create_usermanager_wallet( - user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id - ) - return user.dict() - - -@usermanager_ext.get("/api/v1/wallets") -async def api_usermanager_wallets( - wallet: WalletTypeInfo = Depends(require_admin_key), -): - admin_id = wallet.wallet.user - return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)] - - -@usermanager_ext.get( - "/api/v1/transactions/{wallet_id}", dependencies=[Depends(get_key_type)] -) -async def api_usermanager_wallet_transactions(wallet_id): - return await get_usermanager_wallet_transactions(wallet_id) - - -@usermanager_ext.get( - "/api/v1/wallets/{user_id}", dependencies=[Depends(require_admin_key)] -) -async def api_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}", dependencies=[Depends(require_admin_key)] -) -async def api_usermanager_wallets_delete(wallet_id): - 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) - return "", HTTPStatus.NO_CONTENT diff --git a/tests/data/mock_data.zip b/tests/data/mock_data.zip index 154c4528a6d15cc2789e74ed5fab32666f1dc3c5..f1dfd6c3832ebe283862d0d4252ce01329c133be 100644 GIT binary patch delta 87 zcmdmYi|PIqrVX;4o2PfPDNLT;EyBwH1xp%*I~gXg>NcBfuu@=h<{BxMZ_^kUCNG$y jIN3j47%X^tjTF=0nUkNd@#NEB&|sL%&%m&47Dzh)fnOd+ delta 764 zcmcbAg=ya{rVX;4rkmIP2oZf_`&*TXfq@}_fkB)>fg!b`B)+sbwJ0|+FEKr}NUyjs zC$l8gI5dQpfjw#A-K3<+3s;EMzrA@n@3Mmk+k?VMW&xTlb462=r}9qK_>`*mjj6Li zDNxXv({T&8NUv?OW|p3bUueX(J>34mW@{=1rdjwcaA~qw6R`CE<(WOUb`x#g@AW+Y zy82wgvb8BjvL|Ogow)mm!HMM4N-ZIqwz;mGIxX2=a!Ew1P0jB$VgLQKwrU5Nzp9VV z3NPQ6_rab43?d$Hidi@7?6Qw9@A|ko*Xp%9+j>ureVV!b?yIe9<6e6%&(&pL``h|j zKHI5RTl=f7C4N`PDeYhT{Ox0QZO))xw#TmLx~E50Uw*kpt>>x7`rZ4gr@ealZdcDA zG4)@Mcg?X^`Lk=^_V}A-(Z0<*$Gqx2{Oj9qe%hvgK5g=!J^SZW+>eyYiItHEy=>BZ z`tqKP&(FO-&%fk#@YBM2-S9wDuG1^siww>PhaJCv+QfFgkNEff^M1u;-mXdcm;36c z$S=QpQ#W6KdhO5Lm8oY})T>2;eZ;_UpuRLEr@eQ>MGvO_swfM`1G%$)TuC=f-#4xA ztwp_DvePmx?pH?rT>d9#Xs?P9yQvg*X5qH^KRBP~Ox)TbYZU*#UKwr_Z^?R@dwRc; zHr)7cuL2is)BDz&C1G~R;EVFR8;*~-rw5%{`MUZ}pUL^Eg(jaDpDlaYeeM0kGFv(A zr!OJ`v?^4X-UXj8Q#!ch{36|!%Ek?kPXy=r;K~V$Q