|
|
@ -1,30 +1,25 @@
|
|
|
|
from os import environ as env
|
|
|
|
import requests
|
|
|
|
from secrets import token_urlsafe
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
import monero.address
|
|
|
|
import monero.address
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
from quart import Quart, render_template, redirect, request, flash, jsonify
|
|
|
|
from quart import Quart, render_template, redirect, request, flash
|
|
|
|
|
|
|
|
from quart_auth import (
|
|
|
|
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_bcrypt import Bcrypt
|
|
|
|
from quart_session import Session
|
|
|
|
from quart_session import Session
|
|
|
|
|
|
|
|
|
|
|
|
from lws.models import Admin, Wallet
|
|
|
|
from lws.models import User, Wallet
|
|
|
|
|
|
|
|
from lws import config
|
|
|
|
load_dotenv()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = Quart(__name__)
|
|
|
|
app = Quart(__name__)
|
|
|
|
app.config["TEMPLATES_AUTO_RELOAD"] = True
|
|
|
|
app.config["TEMPLATES_AUTO_RELOAD"] = config.TEMPLATES_AUTO_RELOAD
|
|
|
|
app.config["DEBUG"] = 1 == env.get("DEBUG", 1)
|
|
|
|
app.config["DEBUG"] = config.DEBUG
|
|
|
|
app.config["QUART_ENV"] = env.get("QUART_ENV", "development")
|
|
|
|
app.config["QUART_ENV"] = config.QUART_ENV
|
|
|
|
app.config["SECRET_KEY"] = env.get("SECRET_KEY", token_urlsafe(12))
|
|
|
|
app.config["SECRET_KEY"] = config.SECRET_KEY
|
|
|
|
app.config["SESSION_URI"] = env.get("SESSION_URI", "redis://127.0.0.1:6379")
|
|
|
|
app.config["SESSION_URI"] = config.SESSION_URI
|
|
|
|
app.config["SESSION_TYPE"] = "redis"
|
|
|
|
app.config["SESSION_TYPE"] = config.SESSION_TYPE
|
|
|
|
app.config["SESSION_PROTECTION"] = True
|
|
|
|
app.config["SESSION_PROTECTION"] = config.SESSION_PROTECTION
|
|
|
|
app.config["QUART_AUTH_DURATION"] = 60 * 60 # 1 hour
|
|
|
|
app.config["QUART_AUTH_DURATION"] = config.QUART_AUTH_DURATION
|
|
|
|
Session(app)
|
|
|
|
Session(app)
|
|
|
|
AuthManager(app)
|
|
|
|
AuthManager(app)
|
|
|
|
bcrypt = Bcrypt(app)
|
|
|
|
bcrypt = Bcrypt(app)
|
|
|
@ -33,15 +28,28 @@ bcrypt = Bcrypt(app)
|
|
|
|
@app.route("/")
|
|
|
|
@app.route("/")
|
|
|
|
@login_required
|
|
|
|
@login_required
|
|
|
|
async def index():
|
|
|
|
async def index():
|
|
|
|
admin_exists = Admin.select().first()
|
|
|
|
admin_exists = User.select().first()
|
|
|
|
if not admin_exists:
|
|
|
|
if not admin_exists:
|
|
|
|
return redirect("/setup")
|
|
|
|
return redirect("/setup")
|
|
|
|
return await render_template("index.html")
|
|
|
|
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"])
|
|
|
|
@app.route("/login", methods=["GET", "POST"])
|
|
|
|
async def login():
|
|
|
|
async def login():
|
|
|
|
if not Admin.select().first():
|
|
|
|
if not User.select().first():
|
|
|
|
await flash("must setup first")
|
|
|
|
await flash("must setup first")
|
|
|
|
return redirect("/setup")
|
|
|
|
return redirect("/setup")
|
|
|
|
form = await request.form
|
|
|
|
form = await request.form
|
|
|
@ -54,7 +62,7 @@ async def login():
|
|
|
|
if not password:
|
|
|
|
if not password:
|
|
|
|
await flash("must provide a password")
|
|
|
|
await flash("must provide a password")
|
|
|
|
return redirect("/login")
|
|
|
|
return redirect("/login")
|
|
|
|
user = Admin.select().where(Admin.username == username).first()
|
|
|
|
user = User.select().where(User.username == username).first()
|
|
|
|
if not user:
|
|
|
|
if not user:
|
|
|
|
await flash("this user does not exist")
|
|
|
|
await flash("this user does not exist")
|
|
|
|
return redirect("/login")
|
|
|
|
return redirect("/login")
|
|
|
@ -69,7 +77,7 @@ async def login():
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/setup", methods=["GET", "POST"])
|
|
|
|
@app.route("/setup", methods=["GET", "POST"])
|
|
|
|
async def setup():
|
|
|
|
async def setup():
|
|
|
|
if Admin.select().first():
|
|
|
|
if User.select().first():
|
|
|
|
await flash("Setup already completed")
|
|
|
|
await flash("Setup already completed")
|
|
|
|
return redirect("/")
|
|
|
|
return redirect("/")
|
|
|
|
form = await request.form
|
|
|
|
form = await request.form
|
|
|
@ -101,7 +109,7 @@ async def setup():
|
|
|
|
await flash("Invalid view key provided for address")
|
|
|
|
await flash("Invalid view key provided for address")
|
|
|
|
return redirect("/setup")
|
|
|
|
return redirect("/setup")
|
|
|
|
pw_hash = bcrypt.generate_password_hash(password).decode("utf-8")
|
|
|
|
pw_hash = bcrypt.generate_password_hash(password).decode("utf-8")
|
|
|
|
admin = Admin.create(
|
|
|
|
admin = User.create(
|
|
|
|
username=username,
|
|
|
|
username=username,
|
|
|
|
password=pw_hash,
|
|
|
|
password=pw_hash,
|
|
|
|
address=address,
|
|
|
|
address=address,
|
|
|
@ -111,7 +119,69 @@ async def setup():
|
|
|
|
return redirect("/")
|
|
|
|
return redirect("/")
|
|
|
|
return await render_template("setup.html")
|
|
|
|
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/<id>")
|
|
|
|
|
|
|
|
@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/<id>/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
|
|
|
|
# / - redirect to /setup if user not setup, to /login if not authenticated
|
|
|
|
# /setup - first time setup user account, encrypted session
|
|
|
|
# /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/remove - remove a wallet from LWS
|
|
|
|
# /wallet/:id/resync - resync wallet
|
|
|
|
# /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)
|
|
|
|
@app.errorhandler(Unauthorized)
|
|
|
|
async def redirect_to_login(*_):
|
|
|
|
async def redirect_to_login(*_):
|
|
|
|
return redirect("/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:
|
|
|
|
def run() -> None:
|
|
|
|
app.run()
|
|
|
|
app.run()
|