diff --git a/lws-web/Makefile b/lws-web/Makefile index 0b0ed5a..d23247b 100644 --- a/lws-web/Makefile +++ b/lws-web/Makefile @@ -7,4 +7,7 @@ run: .venv/bin/poetry run start clean: - rm -rf .venv poetry.lock \ No newline at end of file + rm -rf .venv poetry.lock + +shell: + .venv/bin/python3 \ No newline at end of file diff --git a/lws-web/lws/app.py b/lws-web/lws/app.py index 9bfd66c..550d095 100644 --- a/lws-web/lws/app.py +++ b/lws-web/lws/app.py @@ -1,30 +1,25 @@ -from os import environ as env -from secrets import token_urlsafe - -import asyncio +import requests import monero.address -from dotenv import load_dotenv -from quart import Quart, render_template, redirect, request, flash +from quart import Quart, render_template, redirect, request, flash, jsonify from quart_auth import ( - AuthUser, AuthManager, current_user, login_required, login_user, logout_user, Unauthorized + AuthUser, AuthManager, login_required, login_user, current_user, Unauthorized ) from quart_bcrypt import Bcrypt from quart_session import Session -from lws.models import Admin, Wallet - -load_dotenv() +from lws.models import User, Wallet +from lws import config app = Quart(__name__) -app.config["TEMPLATES_AUTO_RELOAD"] = True -app.config["DEBUG"] = 1 == env.get("DEBUG", 1) -app.config["QUART_ENV"] = env.get("QUART_ENV", "development") -app.config["SECRET_KEY"] = env.get("SECRET_KEY", token_urlsafe(12)) -app.config["SESSION_URI"] = env.get("SESSION_URI", "redis://127.0.0.1:6379") -app.config["SESSION_TYPE"] = "redis" -app.config["SESSION_PROTECTION"] = True -app.config["QUART_AUTH_DURATION"] = 60 * 60 # 1 hour +app.config["TEMPLATES_AUTO_RELOAD"] = config.TEMPLATES_AUTO_RELOAD +app.config["DEBUG"] = config.DEBUG +app.config["QUART_ENV"] = config.QUART_ENV +app.config["SECRET_KEY"] = config.SECRET_KEY +app.config["SESSION_URI"] = config.SESSION_URI +app.config["SESSION_TYPE"] = config.SESSION_TYPE +app.config["SESSION_PROTECTION"] = config.SESSION_PROTECTION +app.config["QUART_AUTH_DURATION"] = config.QUART_AUTH_DURATION Session(app) AuthManager(app) bcrypt = Bcrypt(app) @@ -33,15 +28,28 @@ bcrypt = Bcrypt(app) @app.route("/") @login_required async def index(): - admin_exists = Admin.select().first() + admin_exists = User.select().first() if not admin_exists: return redirect("/setup") return await render_template("index.html") +@app.route("/debug") +async def debug(): + admin = User.get(1) + data = { + "auth": admin.view_key, + "params": { + "height": 2836540, + "addresses": ["46pSfwbyukuduh13pqUo7R6S5W8Uk2EnqcKuPg4T9KaoHVSFQ5Qb33nBEN6xVxpeKG1TgYoxo4GxhJm2JFYN1sHJBEH1MwY"] + } + } + r = requests.post("http://127.0.0.1:8081/rescan", json=data) + return jsonify(r.json()) + @app.route("/login", methods=["GET", "POST"]) async def login(): - if not Admin.select().first(): + if not User.select().first(): await flash("must setup first") return redirect("/setup") form = await request.form @@ -54,7 +62,7 @@ async def login(): if not password: await flash("must provide a password") return redirect("/login") - user = Admin.select().where(Admin.username == username).first() + user = User.select().where(User.username == username).first() if not user: await flash("this user does not exist") return redirect("/login") @@ -69,7 +77,7 @@ async def login(): @app.route("/setup", methods=["GET", "POST"]) async def setup(): - if Admin.select().first(): + if User.select().first(): await flash("Setup already completed") return redirect("/") form = await request.form @@ -101,7 +109,7 @@ async def setup(): await flash("Invalid view key provided for address") return redirect("/setup") pw_hash = bcrypt.generate_password_hash(password).decode("utf-8") - admin = Admin.create( + admin = User.create( username=username, password=pw_hash, address=address, @@ -111,7 +119,69 @@ async def setup(): return redirect("/") return await render_template("setup.html") -# bcrypt.check_password_hash(pw_hash, "hunter2") # returns True + +@app.route("/wallet/add", methods=["GET", "POST"]) +async def wallet_add(): + form = await request.form + if form: + name = form.get("name", "") + description = form.get("description", "") + address = form.get("address", "") + view_key = form.get("view_key", "") + restore_height = form.get("restore_height", 0) + valid_view_key = False + if not address: + await flash("must provide an LWS admin address") + return redirect("/wallet/add") + if not view_key: + await flash("must provide an LWS admin view_key") + return redirect("/wallet/add") + try: + _a = monero.address.Address(address) + valid_view_key = _a.check_private_view_key(view_key) + except ValueError: + await flash("Invalid Monero address") + return redirect("/wallet/add") + if not valid_view_key: + await flash("Invalid view key provided for address") + return redirect("/wallet/add") + wallet = Wallet.create( + name=name, + description=description, + address=address, + view_key=view_key, + restore_height=restore_height, + user=User.get(current_user.auth_id) + ) + if not name: + wallet.name = f"wallet-{id}" + wallet.add_wallet_lws() + await flash("wallet added") + return redirect(f"/wallet/{wallet.id}") + return await render_template("wallet/add.html") + + +@app.route("/wallet/") +@login_required +async def wallet_show(id): + wallet = Wallet.select().where(Wallet.id == id).first() + if not wallet: + await flash("wallet does not exist") + return redirect("/") + return await render_template( + "wallet/show.html", + wallet=wallet + ) + +@app.route("/wallet//rescan") +@login_required +async def wallet_rescan(id): + wallet = Wallet.select().where(Wallet.id == id).first() + if not wallet: + await flash("wallet does not exist") + return redirect("/") + wallet.rescan() + return redirect(f"/wallet/{id}") # / - redirect to /setup if user not setup, to /login if not authenticated # /setup - first time setup user account, encrypted session @@ -121,45 +191,17 @@ async def setup(): # /wallet/:id/remove - remove a wallet from LWS # /wallet/:id/resync - resync wallet +# get_address_info +# get_address_txs +# get_random_outs +# get_unspent_outs +# import_request +# submit_raw_tx @app.errorhandler(Unauthorized) async def redirect_to_login(*_): return redirect("/login") - -# @app.get("/replay") -# async def replay(): -# data = list() -# messages = Message.select().order_by(Message.datestamp.asc()).limit(100) -# for m in messages: -# data.append({ -# "message": m.message, -# "datestamp": m.datestamp -# }) -# return jsonify(data) - - -# @app.websocket("/ws") -# async def ws() -> None: -# try: -# task = asyncio.ensure_future(_receive()) -# async for message in broker.subscribe(): -# await websocket.send(message) -# finally: -# task.cancel() -# await task - - -# async def _receive() -> None: -# while True: -# message = await websocket.receive() -# if len(message) > 120: -# print("too long, skipping") -# break -# await broker.publish(message) - - - def run() -> None: app.run() \ No newline at end of file diff --git a/lws-web/lws/config.py b/lws-web/lws/config.py new file mode 100644 index 0000000..84bb3f0 --- /dev/null +++ b/lws-web/lws/config.py @@ -0,0 +1,18 @@ +from os import environ as env +from secrets import token_urlsafe +from dotenv import load_dotenv + +load_dotenv() + +TEMPLATES_AUTO_RELOAD = True +DEBUG = 1 == env.get("DEBUG", 1) +QUART_ENV = env.get("QUART_ENV", "development") +SECRET_KEY = env.get("SECRET_KEY", token_urlsafe(12)) +SESSION_URI = env.get("SESSION_URI", "redis://127.0.0.1:6379") +SESSION_TYPE = "redis" +SESSION_PROTECTION = True +QUART_AUTH_DURATION = 60 * 60 # 1 hour + +# LWS +LWS_URL = env.get("LWS_URL", "http://127.0.0.1:8080") +LWS_ADMIN_URL = env.get("LWS_ADMIN_URL", "http://127.0.0.1:8081") \ No newline at end of file diff --git a/lws-web/lws/helpers.py b/lws-web/lws/helpers.py new file mode 100644 index 0000000..b513e56 --- /dev/null +++ b/lws-web/lws/helpers.py @@ -0,0 +1,3 @@ +import requests + + diff --git a/lws-web/lws/models.py b/lws-web/lws/models.py index 46f0827..0563aea 100644 --- a/lws-web/lws/models.py +++ b/lws-web/lws/models.py @@ -1,13 +1,16 @@ from datetime import datetime +import requests from peewee import * from playhouse.sqliteq import SqliteQueueDatabase +from lws import config + db = SqliteQueueDatabase('data/lws-web.db') -class Admin(Model): +class User(Model): username = CharField() password = CharField() address = CharField() @@ -19,15 +22,99 @@ class Admin(Model): class Wallet(Model): - name = CharField() - description = TextField() - address = CharField() - view_key = CharField() + name = CharField(unique=True) + description = TextField(default="") + address = CharField(unique=True) + view_key = CharField(unique=True) restore_height = IntegerField() + added = BooleanField(default=False) date = DateTimeField(default=datetime.utcnow) + date_added = DateTimeField(null=True) + user = ForeignKeyField(User, backref="wallets") + + def check_wallet_lws(self): + endpoint = f"{config.LWS_ADMIN_URL}/list_accounts" + data = { + "auth": self.user.view_key, + "params": {} + } + try: + req = requests.post(endpoint, json=data, timeout=5) + req.raise_for_status() + if req.ok: + res = req.json() + for _status in res: + for _wallet in res[_status]: + if _wallet["address"] == self.address: + self.added = True + self.save() + return True + return False + return False + except Exception as e: + print(f"Failed to list wallets: {e}") + return False + def add_wallet_lws(self): + endpoint = f"{config.LWS_ADMIN_URL}/add_account" + data = { + "auth": self.user.view_key, + "params": { + "address": self.address, + "key": self.view_key + } + } + try: + req = requests.post(endpoint, json=data, timeout=5) + req.raise_for_status() + if req.ok: + self.added = True + self.date_added = datetime.utcnow() + self.save() + return True + return False + except Exception as e: + print(f"Failed to add wallet {self.address}: {e}") + return False + + def get_wallet_info(self): + endpoint = f"{config.LWS_URL}/get_address_info" + data = { + "address": self.address, + "view_key": self.view_key + } + try: + req = requests.post(endpoint, json=data, timeout=5) + req.raise_for_status() + if req.ok: + return req.json() + return {} + except Exception as e: + print(f"Failed to get wallet info {self.address}: {e}") + return False + + def rescan(self): + endpoint = f"{config.LWS_ADMIN_URL}/rescan" + data = { + "auth": self.user.view_key, + "params": { + "height": self.restore_height, + "addresses": [self.address] + } + } + try: + req = requests.post(endpoint, json=data, timeout=5) + req.raise_for_status() + if req.ok: + print(r.content) + return True + return False + except Exception as e: + print(f"Failed to add wallet {self.address}: {e}") + return False + class Meta: database = db -db.create_tables([Admin, Wallet]) \ No newline at end of file +db.create_tables([User, Wallet]) \ No newline at end of file diff --git a/lws-web/lws/templates/base.html b/lws-web/lws/templates/includes/base.html similarity index 100% rename from lws-web/lws/templates/base.html rename to lws-web/lws/templates/includes/base.html diff --git a/lws-web/lws/templates/includes/navbar.html b/lws-web/lws/templates/includes/navbar.html new file mode 100644 index 0000000..e69de29 diff --git a/lws-web/lws/templates/login.html b/lws-web/lws/templates/login.html index 49dc45c..1842061 100644 --- a/lws-web/lws/templates/login.html +++ b/lws-web/lws/templates/login.html @@ -2,7 +2,7 @@
  • {{ message }}
  • {% endfor %}
    -
    +
    diff --git a/lws-web/lws/templates/wallet/add.html b/lws-web/lws/templates/wallet/add.html new file mode 100644 index 0000000..92415f2 --- /dev/null +++ b/lws-web/lws/templates/wallet/add.html @@ -0,0 +1,28 @@ +{% for message in get_flashed_messages() %} +
  • {{ message }}
  • +{% endfor %} +
    +
    + + + + + + + + + + + + + +
    + + + diff --git a/lws-web/lws/templates/wallet/show.html b/lws-web/lws/templates/wallet/show.html new file mode 100644 index 0000000..1d1a820 --- /dev/null +++ b/lws-web/lws/templates/wallet/show.html @@ -0,0 +1,10 @@ +

    {{ wallet.name }}

    +

    {{ wallet.description }}

    +

    {{ wallet.date }}

    +

    {{ wallet.restore_height }}

    +

    {{ wallet.address }}

    +
    +    {{ wallet.get_wallet_info() }}
    +
    + + \ No newline at end of file