Add seed restore functionality (#2)

minor css adjustment

simplify wallet init and enter teh matrix

add another check on seed inputs

add stronger language around wallet deletion and seed restores

add command to manually reset wallet data for a user

update connect payload and adjust minor template/js

remove debug statements

refactor loading javascript, html, and status payload

remove unused ref to .map

tighten up logic and html template

save template updates

Merge branch 'master' of git.wownero.com:lza_menace/wowstash into seed-restores

modify workflow to allow seed restoral

Co-authored-by: lza_menace <lza_menace@protonmail.com>
Reviewed-on: https://git.wownero.com/lza_menace/wowstash/pulls/2
This commit is contained in:
lza_menace 2020-12-30 08:43:23 +00:00
parent 3a9c13919d
commit e8e97c9f1c
15 changed files with 335 additions and 78 deletions

View file

@ -3,5 +3,5 @@
source .venv/bin/activate source .venv/bin/activate
export FLASK_APP=wowstash/app.py export FLASK_APP=wowstash/app.py
export FLASK_SECRETS=config.py export FLASK_SECRETS=config.py
export FLASK_DEBUG=1 export FLASK_DEBUG=0
flask $1 flask $@

View file

@ -35,7 +35,7 @@ def register():
# Capture event, login user and redirect to wallet page # Capture event, login user and redirect to wallet page
send_es({'type': 'register', 'user': user.email}) send_es({'type': 'register', 'user': user.email})
login_user(user) login_user(user)
return redirect(url_for('wallet.dashboard')) return redirect(url_for('wallet.setup'))
return render_template("auth/register.html", form=form) return render_template("auth/register.html", form=form)
@ -91,7 +91,7 @@ def delete():
send_es({'type': 'delete_wallet', 'user': current_user.email}) send_es({'type': 'delete_wallet', 'user': current_user.email})
current_user.clear_wallet_data(reset_password=True, reset_wallet=True) current_user.clear_wallet_data(reset_password=True, reset_wallet=True)
flash('Successfully deleted wallet data') flash('Successfully deleted wallet data')
return redirect(url_for('meta.index')) return redirect(url_for('wallet.setup'))
else: else:
flash('Please confirm deletion of the account') flash('Please confirm deletion of the account')
return redirect(url_for('wallet.dashboard')) return redirect(url_for('wallet.dashboard'))

View file

@ -13,18 +13,38 @@ from wowstash.library.docker import docker
from wowstash.library.elasticsearch import send_es from wowstash.library.elasticsearch import send_es
from wowstash.library.jsonrpc import Wallet, to_atomic from wowstash.library.jsonrpc import Wallet, to_atomic
from wowstash.library.cache import cache from wowstash.library.cache import cache
from wowstash.forms import Send, Delete from wowstash.forms import Send, Delete, Restore
from wowstash.factory import db from wowstash.factory import db
from wowstash.models import User from wowstash.models import User
from wowstash import config from wowstash import config
@wallet_bp.route('/wallet/setup', methods=['GET', 'POST'])
@login_required
def setup():
if current_user.wallet_created:
return redirect(url_for('wallet.dashboard'))
else:
restore_form = Restore()
if restore_form.validate_on_submit():
c = docker.create_wallet(current_user.id, restore_form.seed.data)
cache.store_data(f'init_wallet_{current_user.id}', 30, c)
current_user.wallet_created = True
db.session.commit()
return redirect(url_for('wallet.loading'))
else:
return render_template(
'wallet/setup.html',
restore_form=restore_form
)
@wallet_bp.route('/wallet/loading') @wallet_bp.route('/wallet/loading')
@login_required @login_required
def loading(): def loading():
if current_user.wallet_connected and current_user.wallet_created: if current_user.wallet_connected and current_user.wallet_created:
sleep(1)
return redirect(url_for('wallet.dashboard')) return redirect(url_for('wallet.dashboard'))
if current_user.wallet_created is False:
return redirect(url_for('wallet.setup'))
return render_template('wallet/loading.html') return render_template('wallet/loading.html')
@wallet_bp.route('/wallet/dashboard') @wallet_bp.route('/wallet/dashboard')
@ -46,6 +66,7 @@ def dashboard():
return redirect(url_for('wallet.loading')) return redirect(url_for('wallet.loading'))
if not wallet.connected: if not wallet.connected:
sleep(1.5)
return redirect(url_for('wallet.loading')) return redirect(url_for('wallet.loading'))
address = wallet.get_address() address = wallet.get_address()
@ -79,6 +100,13 @@ def dashboard():
@wallet_bp.route('/wallet/connect') @wallet_bp.route('/wallet/connect')
@login_required @login_required
def connect(): def connect():
if current_user.wallet_created is False:
data = {
'result': 'fail',
'message': 'Wallet not yet created'
}
return jsonify(data)
if current_user.wallet_connected is False: if current_user.wallet_connected is False:
wallet = docker.start_wallet(current_user.id) wallet = docker.start_wallet(current_user.id)
port = docker.get_port(wallet) port = docker.get_port(wallet)
@ -87,27 +115,42 @@ def connect():
current_user.wallet_container = wallet current_user.wallet_container = wallet
current_user.wallet_start = datetime.utcnow() current_user.wallet_start = datetime.utcnow()
db.session.commit() db.session.commit()
data = {
'result': 'success',
'message': 'Wallet has been connected'
}
else:
data = {
'result': 'fail',
'message': 'Wallet is already connected'
}
return 'ok' return jsonify(data)
@wallet_bp.route('/wallet/create') @wallet_bp.route('/wallet/create')
@login_required @login_required
def create(): def create():
if current_user.wallet_created is False: if current_user.wallet_created is False:
docker.create_wallet(current_user.id) c = docker.create_wallet(current_user.id)
cache.store_data(f'init_wallet_{current_user.id}', 30, c)
current_user.wallet_created = True current_user.wallet_created = True
db.session.commit() db.session.commit()
return redirect(url_for('wallet.loading'))
return 'ok' else:
return redirect(url_for('wallet.dashboard'))
@wallet_bp.route('/wallet/status') @wallet_bp.route('/wallet/status')
@login_required @login_required
def status(): def status():
user_vol = docker.get_user_volume(current_user.id)
create_container = cache.get_data(f'init_wallet_{current_user.id}')
data = { data = {
'created': current_user.wallet_created, 'created': current_user.wallet_created,
'connected': current_user.wallet_connected, 'connected': current_user.wallet_connected,
'port': current_user.wallet_port, 'port': current_user.wallet_port,
'container': current_user.wallet_container 'container': current_user.wallet_container,
'volume': docker.volume_exists(user_vol),
'initializing': docker.container_exists(create_container)
} }
return jsonify(data) return jsonify(data)

View file

@ -1,3 +1,4 @@
import click
from flask import Flask from flask import Flask
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect from flask_wtf.csrf import CSRFProtect
@ -68,6 +69,14 @@ def create_app():
from wowstash.library.docker import docker from wowstash.library.docker import docker
docker.cleanup() docker.cleanup()
@app.cli.command('reset_wallet')
@click.argument('user_id')
def reset_wallet(user_id):
from wowstash.models import User
user = User.query.get(user_id)
user.clear_wallet_data()
print(f'Wallet data cleared for user {user.id}')
# Routes/blueprints # Routes/blueprints
from wowstash.blueprints.auth import auth_bp from wowstash.blueprints.auth import auth_bp
from wowstash.blueprints.wallet import wallet_bp from wowstash.blueprints.wallet import wallet_bp

View file

@ -1,6 +1,7 @@
from re import match as re_match
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField from wtforms import StringField, BooleanField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired, ValidationError
class Register(FlaskForm): class Register(FlaskForm):
@ -20,3 +21,14 @@ class Send(FlaskForm):
class Delete(FlaskForm): class Delete(FlaskForm):
confirm = BooleanField('Confirm Account and Wallet Deletion:', validators=[DataRequired()], render_kw={"class": "form-control-span"}) confirm = BooleanField('Confirm Account and Wallet Deletion:', validators=[DataRequired()], render_kw={"class": "form-control-span"})
class Restore(FlaskForm):
seed = StringField('Seed Phrase', validators=[DataRequired()], render_kw={"placeholder": "25 word mnemonic seed phrase", "class": "form-control"})
risks_accepted = BooleanField('I accept the risks:', validators=[DataRequired()], render_kw={"class": "form-control-span"})
def validate_seed(self, seed):
regex = '^[\w\s]+$'
if bool(re_match(regex, self.seed.data)) is False:
raise ValidationError('Invalid seed provided; must be alphanumeric characters only')
if len(self.seed.data.split()) != 25:
raise ValidationError("Invalid seed provided; must be standard Wownero 25 word format")

View file

@ -17,6 +17,13 @@ class Cache(object):
value=data value=data
) )
def get_data(self, item_name):
data = self.redis.get(item_name)
if data:
return data.decode()
else:
return None
def get_coin_info(self): def get_coin_info(self):
info = self.redis.get("coin_info") info = self.redis.get("coin_info")
if info: if info:

View file

@ -19,21 +19,34 @@ class Docker(object):
self.wallet_dir = expanduser(getattr(config, 'WALLET_DIR', '~/data/wallets')) self.wallet_dir = expanduser(getattr(config, 'WALLET_DIR', '~/data/wallets'))
self.listen_port = 8888 self.listen_port = 8888
def create_wallet(self, user_id): def create_wallet(self, user_id, seed=None):
u = User.query.get(user_id) u = User.query.get(user_id)
volume_name = self.get_user_volume(u.id) volume_name = self.get_user_volume(u.id)
u.wallet_password = token_urlsafe(12) u.wallet_password = token_urlsafe(12)
db.session.commit() db.session.commit()
command = f"""wownero-wallet-cli \ if seed:
--generate-new-wallet /wallet/{u.id}.wallet \ command = f"""sh -c "yes '' | wownero-wallet-cli \
--restore-height {daemon.info()['height']} \ --restore-deterministic-wallet \
--password {u.wallet_password} \ --generate-new-wallet /wallet/{u.id}.wallet \
--mnemonic-language English \ --restore-height 0 \
--daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \ --password {u.wallet_password} \
--daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \ --daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \
--log-file /wallet/{u.id}-create.log --daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \
--command version --electrum-seed '{seed}' \
""" --log-file /wallet/{u.id}-init.log \
--command refresh"
"""
else:
command = f"""wownero-wallet-cli \
--generate-new-wallet /wallet/{u.id}.wallet \
--restore-height {daemon.info()['height']} \
--password {u.wallet_password} \
--mnemonic-language English \
--daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \
--daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \
--log-file /wallet/{u.id}-init.log \
--command version
"""
if not self.volume_exists(volume_name): if not self.volume_exists(volume_name):
self.client.volumes.create( self.client.volumes.create(
name=volume_name, name=volume_name,
@ -43,7 +56,7 @@ class Docker(object):
self.wownero_image, self.wownero_image,
command=command, command=command,
auto_remove=True, auto_remove=True,
name=f'create_wallet_{u.id}', name=f'init_wallet_{u.id}',
remove=True, remove=True,
detach=True, detach=True,
volumes={ volumes={
@ -53,7 +66,7 @@ class Docker(object):
} }
} }
) )
send_es({'type': 'create_wallet', 'user': u.email}) send_es({'type': f'init_wallet', 'user': u.email})
return container.short_id return container.short_id
def start_wallet(self, user_id): def start_wallet(self, user_id):

View file

@ -552,3 +552,82 @@ ol li {
border-radius: 4px; border-radius: 4px;
padding: 6px; padding: 6px;
} }
.teh_matrix {
margin: 2em auto;
}
.teh_matrix img.doge {
opacity: 0;
position: absolute;
top: 10px;
left: 40px;
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-ms-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
}
.teh_matrix span.centered {
opacity: 0;
display: none;
color: #02ff44;
font-weight: bold;
font-size: 20px;
position: absolute;
top: 88px;
width: 400px;
text-align: center;
font-family: monospace;
word-break: break-word;
margin: 20px;
text-shadow: black 1px 1px 0;
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-ms-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
}
.teh_matrix span.title {
opacity: 0;
color: #02ff44;
font-weight: bold;
font-size: 18px;
position: absolute;
top: 28px;
left: 100px;
width: 300px;
font-family: monospace;
word-break: break-word;
text-shadow: black 1px 1px 0;
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-ms-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
}
.teh_matrix span.body {
position: absolute;;
opacity: 0;
color: #02ff44;
font-weight: bold;
font-size: 18px;
top: 70px;
left: 100px;
width: 300px;
font-family: monospace;
word-break: break-word;
text-shadow: black 1px 1px 0;
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-ms-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,11 @@
<div class="header-content mx-auto"> <div class="header-content mx-auto">
<h1 class="mb-5">Manage your Wownero funds securely and anonymously.</h1> <h1 class="mb-5">Manage your Wownero funds securely and anonymously.</h1>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<a href="{{ url_for('wallet.dashboard') }}" class="btn btn-outline btn-xl">{% if current_user.wallet_created %}Wallet Dashboard{% else %}Create Wallet{% endif %}</a> {% if current_user.wallet_created %}
<a href="{{ url_for('wallet.dashboard') }}" class="btn btn-outline btn-xl">Wallet Dashboard</a>
{% else %}
<a href="{{ url_for('wallet.setup') }}" class="btn btn-outline btn-xl">Setup Wallet</a>
{% endif %}
{% else %} {% else %}
<a href="{{ url_for('auth.register') }}" class="btn btn-outline btn-xl">Register</a> <a href="{{ url_for('auth.register') }}" class="btn btn-outline btn-xl">Register</a>
<a href="{{ url_for('auth.login') }}" class="btn btn-outline btn-xl">Login</a> <a href="{{ url_for('auth.login') }}" class="btn btn-outline btn-xl">Login</a>

View file

@ -4,6 +4,80 @@
<script src="/static/js/main.js"></script> <script src="/static/js/main.js"></script>
<script src="/static/js/noty.js"></script> <script src="/static/js/noty.js"></script>
{% if request.path == '/wallet/loading' %}
<script type="text/javascript">
function check_status(){
fetch('/wallet/status')
.then((resp) => resp.json())
.then(function(data) {
// If we've created a wallet and volume, but not connected a container and are not restoring, attempt connecting
if(data['created'] && data['volume'] && data['connected'] == false && data['initializing'] == false){
fetch('/wallet/connect')
}
// If all of the above and now the wallet is connected, go to dashboard
if(data['created'] && data['volume'] && data['connected']){
window.setInterval(function(){
window.location.href = "{{ url_for('wallet.dashboard') }}"
}, 3000);
}
})
}
$(document).ready(function () {
// Check wallet status every few seconds...
window.setInterval(function(){
check_status();
}, 7000);
// ...but also check on initial page load
check_status();
// enter teh matrix
let q = document.getElementById('q');
let width = q.width;
let height = q.height;
let yPositions = Array(300).join(0).split('');
let ctx = q.getContext('2d');
let draw = function () {
ctx.fillStyle = 'rgba(0,0,0,.05)';
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = '#0F0';
ctx.font = '10pt Georgia';
yPositions.map(function (y, index) {
let text = String.fromCharCode(1e2 + Math.random() * 33);
let x = (index * 10) + 10;
q.getContext('2d').fillText(text, x, y);
if (y > 100 + Math.random() * 1e4) {
yPositions[index] = 0;
}
else {
yPositions[index] = y + 10;
}
});
};
let matrix_interval = null;
function RunMatrix() {
matrix_interval = setInterval(draw, 33);
}
RunMatrix();
$('.teh_matrix span').each(function(i, obj){
jQuery(obj).css('opacity', '1');
});
$('.teh_matrix img').each(function(i, obj){
jQuery(obj).css('opacity', '1');
});
});
</script>
{% endif %}
{% if request.path == '/wallet/dashboard' %} {% if request.path == '/wallet/dashboard' %}
<script type="text/javascript" src="/static/js/zxing.js"></script> <script type="text/javascript" src="/static/js/zxing.js"></script>
<script type="text/javascript"> <script type="text/javascript">

View file

@ -154,6 +154,7 @@
<div class="section-heading text-center"> <div class="section-heading text-center">
<h2>Delete Account</h2> <h2>Delete Account</h2>
<p>You can and should delete your wallet from the server. Please ensure you have copied the mnemonic seed from the secrets above if there are still funds associated with the keys.</p> <p>You can and should delete your wallet from the server. Please ensure you have copied the mnemonic seed from the secrets above if there are still funds associated with the keys.</p>
<p>I highly recommend making a new wallet on your own and transferring funds there to ensure only you have full ownership and visibility into the private keys / seed. Not your keys, not your crypto!</p>
<form method="POST" action="{{ url_for('auth.delete') }}" class="send-form"> <form method="POST" action="{{ url_for('auth.delete') }}" class="send-form">
{{ delete_form.csrf_token }} {{ delete_form.csrf_token }}
{% for f in delete_form %} {% for f in delete_form %}
@ -168,7 +169,7 @@
{% for field, errors in delete_form.errors.items() %} {% for field, errors in delete_form.errors.items() %}
<li>{{ send_form[field].label }}: {{ ', '.join(errors) }}</li> <li>{{ send_form[field].label }}: {{ ', '.join(errors) }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
<input type="submit" value="Delete" class="btn btn-link btn-outline btn-xl"> <input type="submit" value="Delete" class="btn btn-link btn-outline btn-xl">
</form> </form>
</div> </div>

View file

@ -5,67 +5,32 @@
<body id="page-top"> <body id="page-top">
{% include 'navbar.html' %} <section class="section1">
<section class="section2">
<div class="container"> <div class="container">
<div class="section-heading text-center"> <div class="section-heading text-center">
{% if current_user.wallet_created == False %}
<h2>Your wallet is being created</h2>
{% else %}
<h2>Your wallet is connecting</h2> <h2>Your wallet is connecting</h2>
{% endif %} <p>Go smoke a fatty. This page should auto-refresh when it's ready...if not, click the button below. <br /><br />If you are restoring from a seed, please allow several minutes for the process to complete.</p>
<p>Go smoke a fatty. This page should auto-refresh when it's ready...if not, click the button below</p> <div class="teh_matrix" style="position: relative;border-radius:4px;width:450px;margin-bottom:32px;border:1px solid green;height:250px;overflow:hidden;background-color:black;">
<img src="/static/img/loading-cat.gif" width=300> <canvas id="q" width="450px" height="250px"></canvas>
<span class="centered"></span>
<img class="doge" width=80px src="/static/img/loading-doge.png"/>
<span class="title">Very Secure Login™</span>
<span class="body">
☑ many encryptions<br>
☑ very password<br>
☑ NASA certified<br>
☑ such login<br>
</span>
</div>
<span class="dashboard-buttons"> <span class="dashboard-buttons">
<div class="col-sm-12 dashboard-button"> <div class="col-sm-12 dashboard-button">
<a class="btn btn-lg btn-link btn-outline btn-xl" href="{{ url_for('wallet.dashboard') }}">Check Again</a> <a class="btn-link" href="{{ url_for('wallet.dashboard') }}">Check Again</a>
</div> </div>
</span> </span>
</div> </div>
</div> </div>
</section> </section>
<script>
function check_wallet_status(attrib) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4){
let res = JSON.parse(xhr.responseText);
if (res[attrib] == true) {
window.location.href = "{{ url_for('wallet.dashboard') }}"
}
}
};
xhr.open('GET', '{{ url_for("wallet.status") }}');
xhr.send();
}
{% if current_user.wallet_connected == False and current_user.wallet_created == True %}
document.addEventListener("DOMContentLoaded", function(){
var xhr = new XMLHttpRequest();
xhr.open('GET', '{{ url_for("wallet.connect") }}');
xhr.send();
});
{% endif %}
{% if current_user.wallet_connected == False and current_user.wallet_created == False %}
document.addEventListener("DOMContentLoaded", function(){
var xhr = new XMLHttpRequest();
xhr.open('GET', '{{ url_for("wallet.create") }}');
xhr.send();
});
{% endif %}
window.setInterval(function(){
{% if current_user.wallet_connected == False and current_user.wallet_created == True %}
check_wallet_status('connected');
{% else %}
check_wallet_status('created');
{% endif %}
}, 6000);
</script>
{% include 'footer.html' %} {% include 'footer.html' %}
{% include 'scripts.html' %} {% include 'scripts.html' %}

View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
{% include 'head.html' %}
<body id="page-top">
{% include 'navbar.html' %}
<section class="section2">
<div class="container">
<div class="section-heading text-center">
<h2>Setup Wallet</h2>
<p>Alrighty there hoss, pick an option below...</p>
<hr><br /><br />
<a class="btn btn-lg btn-link btn-outline btn-xl" href="{{ url_for('wallet.create') }}">Create new wallet</a>
<hr><br /><br />
<form method="POST" action="{{ url_for('wallet.setup') }}" class="send-form">
<p><strong>! WARNING !</strong><br /> If you input a mnemonic seed here I could theoretically steal your funds, even without a wallet on my server; so could a hacker if they compromised my server.</p>
<p>You <strong>can</strong> and <strong>should</strong> use a <a href="https://wownero.org/#wallets" target="_blank">wallet</a> you can run locally to ensure your funds are safe, especially if there is a lot there. Proceed at your own risk.</p>
{{ restore_form.csrf_token }}
{% for f in restore_form %}
{% if f.name != 'csrf_token' %}
<div class="form-group">
{{ f.label }}
{{ f }}
</div>
{% endif %}
{% endfor %}
<ul>
{% for field, errors in restore_form.errors.items() %}
<li>{{ restore_form[field].label }}: {{ ', '.join(errors) }}</li>
{% endfor %}
</ul>
<input type="submit" value="Restore From Seed" class="btn btn-link btn-outline btn-xl">
</form>
</div>
</div>
</section>
{% include 'footer.html' %}
{% include 'scripts.html' %}
</body>
</html>