Compare commits

..

20 Commits

Author SHA1 Message Date
lza_menace 8da92e6919 add some commands to list and wipe users to remove spammers 3 years ago
lza_menace b34cf05497 add simple email validator to form to prevent spam 3 years ago
lza_menace c54c37b10e setup donation page and begging 3 years ago
lza_menace 5a3bf57a32 fix bug where tx_fail string was too long 3 years ago
lza_menace 1a48e7db77 catch 500 errors and notify discord 3 years ago
lza_menace f178878fb5 update prod app script to be smoother 3 years ago
lza_menace 952d251160 set trusted daemon for all commands 3 years ago
lza_menace a5f2422abd add a little more detail to see wallet health/status 3 years ago
lza_menace abcf5b5dd9 bind flask to localhost 4 years ago
lza_menace fbc8917a7b oops 4 years ago
lza_menace 91ff0f8796 bind only to local ports 4 years ago
lza_menace cf20b46d7d exit cmd if user does not exist 4 years ago
lza_menace a009450caa add reset password functionality 4 years ago
lza_menace 34d47ad39f make sure db is always on 4 years ago
lza_menace 2829d698fa fix up meta tags and loading html 4 years ago
lza_menace 9428a47537 update graf container name 4 years ago
lza_menace 2567db144f Event logging/capture (#3)
allow longer event type strings in db

add graf container w/ pre-built dashboard

remove elasticsearch and use db based event logging

add init command

Co-authored-by: lza_menace <lza_menace@protonmail.com>
Reviewed-on: https://git.wownero.com/lza_menace/wowstash/pulls/3
4 years ago
lza_menace e8e97c9f1c 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
4 years ago
lza_menace 3a9c13919d add simple makefile 4 years ago
lza_menace a43fae4a1b remove unused file and adjust lock/unlock balance display 4 years ago

@ -1,9 +0,0 @@
FROM ubuntu:19.10
WORKDIR /srv
COPY requirements.txt .
RUN apt-get update && apt-get install python3-pip -y
RUN python3 -m pip install -r requirements.txt
COPY wowstash wowstash/
COPY bin/ bin/
EXPOSE 4001
CMD ["/srv/bin/prod-container"]

@ -0,0 +1,23 @@
.PHONY: format help
# Help system from https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
.DEFAULT_GOAL := help
help:
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
setup: ## Setup Python virtual environment
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
up: ## Bring up backend containers
docker-compose up -d
down: ## Bring down backend containers
docker-compose down
dev: ## Start development server
./bin/dev
dbshell: ## Start interactive session with database
docker-compose exec db psql -U wowstash

@ -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 $@

@ -10,12 +10,15 @@ export FLASK_ENV=production
mkdir -p $BASE mkdir -p $BASE
gunicorn \ pgrep -F $BASE/gunicorn.pid
--bind 0.0.0.0:4001 "wowstash.app:app" \
if [[ $? != 0 ]]; then
gunicorn \
--bind 127.0.0.1:4001 "wowstash.app:app" \
--daemon \ --daemon \
--log-file $BASE/gunicorn.log \ --log-file $BASE/gunicorn.log \
--pid $BASE/gunicorn.pid \ --pid $BASE/gunicorn.pid \
--access-logfile $BASE/access.log \
--reload --reload
sleep 1
echo "Starting gunicorn with pid $(cat $BASE/gunicorn.pid)" echo "Starting gunicorn with pid $(cat $BASE/gunicorn.pid)"
fi

@ -1,22 +0,0 @@
services:
kibana:
image: docker.elastic.co/kibana/kibana:7.1.0
ports:
- 5601:5601
environment:
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.1.0
environment:
- discovery.type=single-node
- node.name=elasticsearch
- cluster.name=es-docker-cluster
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- ./data/elasticsearch:/usr/share/elasticsearch/data
ports:
- 9200:9200

@ -5,8 +5,9 @@ services:
db: db:
image: postgres:9.6.15-alpine image: postgres:9.6.15-alpine
container_name: wowstash_db container_name: wowstash_db
restart: unless-stopped
ports: ports:
- 5432:5432 - 127.0.0.1:5432:5432
environment: environment:
POSTGRES_PASSWORD: ${DB_PASS} POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_USER: ${DB_USER} POSTGRES_USER: ${DB_USER}
@ -17,4 +18,23 @@ services:
image: redis:latest image: redis:latest
container_name: wowstash_cache container_name: wowstash_cache
ports: ports:
- 6379:6379 - 127.0.0.1:6379:6379
grafana:
image: grafana/grafana:6.5.0
container_name: wowstash_ops
restart: unless-stopped
ports:
- 127.0.0.1:3001:3000
environment:
HOSTNAME: grafana
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
GF_SERVER_ROOT_URL: ${GRAFANA_URL}
GF_ANALYTICS_REPORTING_ENABLED: "false"
GF_ANALYTICS_CHECK_FOR_UPDATES: "false"
GF_USERS_ALLOW_SIGN_UP: "false"
GF_USERS_ALLOW_ORG_CREATE: "false"
volumes:
- ./files/dashboards.yaml:/etc/grafana/provisioning/dashboards/default.yaml:ro
- ./files/wowstash_ops.json:/var/lib/grafana/dashboards/wowstash_ops.json:ro
- grafana:/var/lib/grafana

@ -0,0 +1,13 @@
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: true
editable: true
updateIntervalSeconds: 60
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards

@ -0,0 +1,311 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 1,
"links": [],
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 0
},
"hiddenSeries": false,
"id": 4,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 1,
"points": true,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"format": "time_series",
"group": [
{
"params": [
"$__interval",
"none"
],
"type": "time"
}
],
"metricColumn": "none",
"rawQuery": false,
"rawSql": "SELECT\n $__timeGroupAlias(register_date,$__interval),\n avg(id) AS \"id\"\nFROM users\nWHERE\n $__timeFilter(register_date)\nGROUP BY 1\nORDER BY 1",
"refId": "A",
"select": [
[
{
"params": [
"id"
],
"type": "column"
},
{
"params": [
"avg"
],
"type": "aggregate"
},
{
"params": [
"id"
],
"type": "alias"
}
]
],
"table": "users",
"timeColumn": "register_date",
"timeColumnType": "timestamp",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "User Registrations",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 0,
"fillGradient": 0,
"gridPos": {
"h": 11,
"w": 24,
"x": 0,
"y": 9
},
"hiddenSeries": false,
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"format": "time_series",
"group": [
{
"params": [
"$__interval",
"0"
],
"type": "time"
}
],
"metricColumn": "type",
"rawQuery": false,
"rawSql": "SELECT\n $__timeGroupAlias(date,$__interval,0),\n type AS metric,\n count(\"user\") AS \"id\"\nFROM events\nWHERE\n $__timeFilter(date)\nGROUP BY 1,2\nORDER BY 1,2",
"refId": "A",
"select": [
[
{
"params": [
"\"user\""
],
"type": "column"
},
{
"params": [
"count"
],
"type": "aggregate"
},
{
"params": [
"id"
],
"type": "alias"
}
]
],
"table": "events",
"timeColumn": "date",
"timeColumnType": "timestamp",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Event Activity",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": "",
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
],
"yaxis": {
"align": true,
"alignLevel": null
}
}
],
"refresh": false,
"schemaVersion": 21,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Wowstash Ops",
"uid": "zvTlfCbGz",
"version": 1
}

@ -3,11 +3,11 @@ from flask import request, render_template, session, redirect, url_for, flash
from flask_login import login_user, logout_user, current_user, login_required from flask_login import login_user, logout_user, current_user, login_required
from time import sleep from time import sleep
from wowstash.blueprints.auth import auth_bp from wowstash.blueprints.auth import auth_bp
from wowstash.forms import Register, Login, Delete from wowstash.forms import Register, Login, Delete, ResetPassword
from wowstash.models import User from wowstash.models import User, PasswordReset
from wowstash.factory import db, bcrypt from wowstash.factory import db, bcrypt
from wowstash.library.docker import docker from wowstash.library.docker import docker
from wowstash.helpers import capture_event from wowstash.library.helpers import capture_event
@auth_bp.route("/register", methods=["GET", "POST"]) @auth_bp.route("/register", methods=["GET", "POST"])
@ -33,9 +33,9 @@ def register():
db.session.commit() db.session.commit()
# Capture event, login user and redirect to wallet page # Capture event, login user and redirect to wallet page
capture_event('register', user) capture_event(user.id, 'register')
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)
@ -63,7 +63,7 @@ def login():
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
# Capture event, login user, and redirect to wallet page # Capture event, login user, and redirect to wallet page
capture_event('login', user) capture_event(user.id, 'login')
login_user(user) login_user(user)
return redirect(url_for('wallet.dashboard')) return redirect(url_for('wallet.dashboard'))
@ -73,9 +73,9 @@ def login():
def logout(): def logout():
if current_user.is_authenticated: if current_user.is_authenticated:
docker.stop_container(current_user.wallet_container) docker.stop_container(current_user.wallet_container)
capture_event('stop_container', current_user) capture_event(current_user.id, 'stop_container')
current_user.clear_wallet_data() current_user.clear_wallet_data()
capture_event('logout', current_user) capture_event(current_user.id, 'logout')
logout_user() logout_user()
return redirect(url_for('meta.index')) return redirect(url_for('meta.index'))
@ -85,13 +85,39 @@ def delete():
form = Delete() form = Delete()
if form.validate_on_submit(): if form.validate_on_submit():
docker.stop_container(current_user.wallet_container) docker.stop_container(current_user.wallet_container)
capture_event('stop_container', current_user) capture_event(current_user.id, 'stop_container')
sleep(1) sleep(1)
docker.delete_wallet_data(current_user.id) docker.delete_wallet_data(current_user.id)
capture_event('delete_wallet', current_user) capture_event(current_user.id, 'delete_wallet')
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'))
@auth_bp.route("/reset/<string:hash>", methods=["GET", "POST"])
def reset(hash):
hash = PasswordReset.query.filter(PasswordReset.hash==hash).first()
if not hash:
flash('Invalid password reset hash')
return redirect(url_for('auth.login'))
if hash.hours_elapsed() > hash.expiration_hours or hash.expired:
flash('Reset hash has expired')
return redirect(url_for('auth.login'))
form = ResetPassword()
if form.validate_on_submit():
try:
user = User.query.get(hash.user)
user.password = bcrypt.generate_password_hash(form.password.data).decode('utf8')
hash.expired = True
db.session.commit()
flash('Password reset successfully')
return redirect(url_for('auth.login'))
except:
flash('Error resetting password')
return redirect(url_for('auth.login'))
return render_template('auth/reset.html', form=form)

@ -4,8 +4,14 @@ from wowstash.library.jsonrpc import daemon
from wowstash.library.cache import cache from wowstash.library.cache import cache
from wowstash.library.db import Database from wowstash.library.db import Database
from wowstash.library.docker import Docker from wowstash.library.docker import Docker
from wowstash.library.helpers import post_discord_webhook
@meta_bp.errorhandler(500)
def internal_error(error):
post_discord_webhook(f'500 error: {error.original_exception}')
return render_template('meta/500.html', error=error)
@meta_bp.route('/') @meta_bp.route('/')
def index(): def index():
return render_template('meta/index.html', node=daemon.info(), info=cache.get_coin_info()) return render_template('meta/index.html', node=daemon.info(), info=cache.get_coin_info())
@ -30,8 +36,6 @@ def health():
'docker': Docker().client.ping() 'docker': Docker().client.ping()
}), 200) }), 200)
# @app.errorhandler(404) @meta_bp.route('/donate')
# def not_found(error): def donate():
# return make_response(jsonify({ return render_template('meta/donate.html')
# 'error': 'Page not found'
# }), 404)

@ -10,21 +10,42 @@ from socket import socket
from datetime import datetime from datetime import datetime
from wowstash.blueprints.wallet import wallet_bp from wowstash.blueprints.wallet import wallet_bp
from wowstash.library.docker import docker from wowstash.library.docker import docker
from wowstash.helpers import capture_event from wowstash.library.helpers import capture_event
from wowstash.library.jsonrpc import Wallet, to_atomic from wowstash.library.jsonrpc import Wallet, daemon, 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, DonatePrompt
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)
capture_event(current_user.id, 'restore_wallet')
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,8 +67,28 @@ 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'))
# Redirect to donate page if user isnt prompted for a bit
dp = DonatePrompt.query.filter(
DonatePrompt.user == current_user.id
).order_by(
DonatePrompt.date.desc()
).first()
if not dp:
d = DonatePrompt(user=current_user.id)
db.session.add(d)
db.session.commit()
return redirect(url_for('meta.donate'))
# If havent seen donate page in some time, show again
if dp.hours_elapsed() > 168:
d = DonatePrompt(user=current_user.id)
db.session.add(d)
db.session.commit()
return redirect(url_for('meta.donate'))
address = wallet.get_address() address = wallet.get_address()
transfers = wallet.get_transfers() transfers = wallet.get_transfers()
for type in transfers: for type in transfers:
@ -60,10 +101,13 @@ def dashboard():
seed = wallet.seed() seed = wallet.seed()
spend_key = wallet.spend_key() spend_key = wallet.spend_key()
view_key = wallet.view_key() view_key = wallet.view_key()
# capture_event('load_dashboard', current_user) capture_event(current_user.id, 'load_dashboard')
return render_template( return render_template(
'wallet/dashboard.html', 'wallet/dashboard.html',
transfers=all_transfers, transfers=all_transfers,
wallet_height=wallet.height(),
node_height=daemon.height(),
node_addr=config.DAEMON_HOST,
sorted_txes=get_sorted_txes(transfers), sorted_txes=get_sorted_txes(transfers),
balances=balances, balances=balances,
address=address, address=address,
@ -79,6 +123,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 +138,44 @@ 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()
capture_event(current_user.id, 'start_wallet')
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)
capture_event(current_user.id, 'create_wallet')
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)
@ -130,13 +198,13 @@ def send():
# Check if Wownero wallet is available # Check if Wownero wallet is available
if wallet.connected is False: if wallet.connected is False:
flash('Wallet RPC interface is unavailable at this time. Try again later.') flash('Wallet RPC interface is unavailable at this time. Try again later.')
capture_event('tx_fail_rpc_unavailable', user) capture_event(user.id, 'tx_fail_rpc_unavailable')
return redirect(redirect_url) return redirect(redirect_url)
# Quick n dirty check to see if address is WOW # Quick n dirty check to see if address is WOW
if len(address) not in [97, 108]: if len(address) not in [97, 108]:
flash('Invalid Wownero address provided.') flash('Invalid Wownero address provided.')
capture_event('tx_fail_address_invalid', user) capture_event(user.id, 'tx_fail_address_invalid')
return redirect(redirect_url) return redirect(redirect_url)
# Check if we're sweeping or not # Check if we're sweeping or not
@ -148,7 +216,7 @@ def send():
amount = to_atomic(Decimal(send_form.amount.data)) amount = to_atomic(Decimal(send_form.amount.data))
except: except:
flash('Invalid Wownero amount specified.') flash('Invalid Wownero amount specified.')
capture_event('tx_fail_amount_invalid', user) capture_event(user.id, 'tx_fail_amount_invalid')
return redirect(redirect_url) return redirect(redirect_url)
# Send transfer # Send transfer
@ -159,10 +227,10 @@ def send():
msg = tx['message'].capitalize() msg = tx['message'].capitalize()
msg_lower = tx['message'].replace(' ', '_').lower() msg_lower = tx['message'].replace(' ', '_').lower()
flash(f'There was a problem sending the transaction: {msg}') flash(f'There was a problem sending the transaction: {msg}')
capture_event(f'tx_fail_{msg_lower}', user) capture_event(user.id, f'tx_fail_{msg_lower[0:50]}')
else: else:
flash('Successfully sent transfer.') flash('Successfully sent transfer.')
capture_event('tx_success', user) capture_event(user.id, 'tx_success')
return redirect(redirect_url) return redirect(redirect_url)
else: else:

@ -1,23 +1,67 @@
from wowstash.library.jsonrpc import wallet import click
from wowstash.models import Transaction from flask import Blueprint, url_for
from wowstash.factory import db
import wowstash.models
from wowstash.library.docker import docker
from wowstash.models import User, PasswordReset, Event
from wowstash.factory import db, bcrypt
# @app.errorhandler(404)
def not_found(error):
return make_response(jsonify({
'error': 'Page not found'
}), 404)
# @app.cli.command('initdb') bp = Blueprint("cli", "cli", cli_group=None)
def init_db():
@bp.cli.command('clean_containers')
def clean_containers():
docker.cleanup()
@bp.cli.command('reset_wallet')
@click.argument('user_id')
def reset_wallet(user_id):
user = User.query.get(user_id)
user.clear_wallet_data()
print(f'Wallet data cleared for user {user.id}')
@bp.cli.command('init')
def init():
db.create_all() db.create_all()
# @app.cli.command('send_transfers') @bp.cli.command('list_users')
def send_transfers(): def list_users():
txes = Transaction.query.all() users = User.query.all()
for i in txes: for i in users:
print(i) print(f'{i.id} - {i.email}')
# tx = wallet.transfer(
# 0, current_user.subaddress_index, address, amount @bp.cli.command('wipe_user')
# ) @click.argument('user_id')
def wipe_user(user_id):
user = User.query.get(user_id)
if user:
events = Event.query.filter(Event.user == user.id)
for i in events:
print(f'[+] Deleting event {i.id} for user {user.id}')
db.session.delete(i)
print(f'[+] Deleting user {user.id}')
db.session.delete(user)
db.session.commit()
return True
else:
print('That user id does not exist')
return False
@bp.cli.command('reset_password')
@click.argument('user_email')
@click.argument('duration')
def reset_password(user_email, duration):
user = User.query.filter(User.email==user_email).first()
if not user:
click.echo('[!] Email address does not exist!')
return
pwr = PasswordReset(
user=user.id,
hash=PasswordReset().generate_hash(),
expiration_hours=duration
)
db.session.add(pwr)
db.session.commit()
click.echo(f'[+] Password reset link #{pwr.id} for {user_email} expires in {duration} hours: {url_for("auth.reset", hash=pwr.hash)}')

@ -37,7 +37,6 @@ DB_PASS = 'zzzzzzzzz'
# Development # Development
TEMPLATES_AUTO_RELOAD = True TEMPLATES_AUTO_RELOAD = True
ELASTICSEARCH_ENABLED = False
# Social # Social
SOCIAL = { SOCIAL = {
@ -46,10 +45,3 @@ SOCIAL = {
'comment': 'https://webchat.freenode.net/?room=#wownero', 'comment': 'https://webchat.freenode.net/?room=#wownero',
'reddit': 'https://reddit.com/r/wownero' 'reddit': 'https://reddit.com/r/wownero'
} }
# Mattermost
MM_ENABLED = False
MM_CHANNEL = 'wowstash'
MM_USERNAME = 'WOW Stash'
MM_ICON = 'https://wowstash.app/static/img/wow-treasure-chest.png'
MM_ENDPOINT = 'xxxxxxx'

@ -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
@ -62,18 +63,14 @@ def create_app():
else: else:
return float(atomic) return float(atomic)
# CLI
@app.cli.command('clean_containers')
def clean_containers():
from wowstash.library.docker import docker
docker.cleanup()
# 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
from wowstash.blueprints.meta import meta_bp from wowstash.blueprints.meta import meta_bp
from wowstash.cli import bp as cli_bp
app.register_blueprint(meta_bp) app.register_blueprint(meta_bp)
app.register_blueprint(auth_bp) app.register_blueprint(auth_bp)
app.register_blueprint(wallet_bp) app.register_blueprint(wallet_bp)
app.register_blueprint(cli_bp)
return app return app

@ -1,6 +1,9 @@
from re import match as re_match
from re import fullmatch as re_fullmatch
from re import compile as re_compile
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):
@ -10,6 +13,12 @@ class Register(FlaskForm):
terms_reviewed = BooleanField('Terms Reviewed:', validators=[DataRequired()], render_kw={"class": "form-control-span"}) terms_reviewed = BooleanField('Terms Reviewed:', validators=[DataRequired()], render_kw={"class": "form-control-span"})
privacy_reviewed = BooleanField('Privacy Policy Reviewed:', validators=[DataRequired()], render_kw={"class": "form-control-span"}) privacy_reviewed = BooleanField('Privacy Policy Reviewed:', validators=[DataRequired()], render_kw={"class": "form-control-span"})
def validate_email(self, email):
regex = re_compile(r'([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+')
if not re_fullmatch(regex, self.email.data):
raise ValidationError('This appears to be an invalid email. Contact admins if you feel this is incorrect.')
class Login(FlaskForm): class Login(FlaskForm):
email = StringField('Email Address:', validators=[DataRequired()], render_kw={"placeholder": "Email", "class": "form-control", "type": "email"}) email = StringField('Email Address:', validators=[DataRequired()], render_kw={"placeholder": "Email", "class": "form-control", "type": "email"})
password = StringField('Password:', validators=[DataRequired()], render_kw={"placeholder": "Password", "class": "form-control", "type": "password"}) password = StringField('Password:', validators=[DataRequired()], render_kw={"placeholder": "Password", "class": "form-control", "type": "password"})
@ -20,3 +29,22 @@ 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")
class ResetPassword(FlaskForm):
password = StringField('Password', validators=[DataRequired()], render_kw={"placeholder": "Password", "type": "password"})
password_confirmed = StringField('Confirm Password', validators=[DataRequired()], render_kw={"placeholder": "Confirm Password", "type": "password"})
def validate_password(self, password):
if self.password.data != self.password_confirmed.data:
raise ValidationError('Passwords do not match')

@ -1,7 +0,0 @@
from wowstash.library.elasticsearch import send_es
from wowstash.library.mattermost import post_webhook
def capture_event(event_type, user_obj):
send_es({'type': event_type, 'user': user_obj.email})
post_webhook(f'`{event_type}` from user {user_obj.id}')

@ -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:

@ -9,7 +9,6 @@ from wowstash import config
from wowstash.models import User from wowstash.models import User
from wowstash.factory import db from wowstash.factory import db
from wowstash.library.jsonrpc import daemon from wowstash.library.jsonrpc import daemon
from wowstash.helpers import capture_event
class Docker(object): class Docker(object):
@ -19,11 +18,25 @@ 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()
if seed:
command = f"""sh -c "yes '' | wownero-wallet-cli \
--restore-deterministic-wallet \
--generate-new-wallet /wallet/{u.id}.wallet \
--restore-height 0 \
--password {u.wallet_password} \
--daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \
--daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \
--trusted-daemon \
--electrum-seed '{seed}' \
--log-file /wallet/{u.id}-init.log \
--command refresh"
"""
else:
command = f"""wownero-wallet-cli \ command = f"""wownero-wallet-cli \
--generate-new-wallet /wallet/{u.id}.wallet \ --generate-new-wallet /wallet/{u.id}.wallet \
--restore-height {daemon.info()['height']} \ --restore-height {daemon.info()['height']} \
@ -31,7 +44,8 @@ class Docker(object):
--mnemonic-language English \ --mnemonic-language English \
--daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \ --daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \
--daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \ --daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \
--log-file /wallet/{u.id}-create.log --trusted-daemon \
--log-file /wallet/{u.id}-init.log \
--command version --command version
""" """
if not self.volume_exists(volume_name): if not self.volume_exists(volume_name):
@ -43,7 +57,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 +67,6 @@ class Docker(object):
} }
} }
) )
capture_event('create_wallet', u)
return container.short_id return container.short_id
def start_wallet(self, user_id): def start_wallet(self, user_id):
@ -70,6 +83,7 @@ class Docker(object):
--password {u.wallet_password} \ --password {u.wallet_password} \
--daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \ --daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \
--daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \ --daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \
--trusted-daemon \
--log-file /wallet/{u.id}-rpc.log --log-file /wallet/{u.id}-rpc.log
""" """
try: try:
@ -90,7 +104,6 @@ class Docker(object):
} }
} }
) )
capture_event('start_wallet', u)
return container.short_id return container.short_id
except APIError as e: except APIError as e:
if str(e).startswith('409'): if str(e).startswith('409'):

@ -1,22 +0,0 @@
from datetime import datetime
from elasticsearch import Elasticsearch
from wowstash import config
def send_es(data):
if getattr(config, 'ELASTICSEARCH_ENABLED', False):
try:
es = Elasticsearch(
[getattr(config, 'ELASTICSEARCH_HOST', 'localhost')]
)
now = datetime.utcnow()
index_ts = now.strftime('%Y%m%d')
data['datetime'] = now
es.index(
index="{}-{}".format(
getattr(config, 'ELASTICSEARCH_INDEX_NAME', 'wowstash'),
index_ts
), body=data)
except Exception as e:
print('Could not capture event in Elasticsearch: ', e)
pass # I don't really care if this logs...

@ -0,0 +1,23 @@
import requests
from wowstash.models import Event
from wowstash.factory import db
from wowstash import config
def capture_event(user_id, event_type):
event = Event(
user=user_id,
type=event_type
)
db.session.add(event)
db.session.commit()
return
def post_discord_webhook(text):
try:
r = requests.post(config.DISCORD_URL, data={"content": text})
r.raise_for_status()
return True
except:
return False

@ -1,22 +0,0 @@
from requests import post as r_post
from json import dumps
from flask import current_app
from wowstash import config
def post_webhook(msg):
if getattr(config, 'MM_ENABLED', False):
try:
if current_app.config["DEBUG"]:
msg = "[DEBUG] " + msg
data = {
"text": msg,
"channel": config.MM_CHANNEL,
"username": config.MM_USERNAME,
"icon_url": config.MM_ICON
}
res = r_post(config.MM_ENDPOINT, data=dumps(data))
res.raise_for_status()
return True
except:
return False

@ -1,117 +0,0 @@
import requests
import six
import json
import operator
from tipbot import config
from decimal import Decimal
PICOWOW = Decimal('0.00000000001')
class Wallet(object):
def __init__(self):
self.host = config.WALLET_HOST
self.port = config.WALLET_PORT
self.proto = config.WALLET_PROTO
self.username = config.WALLET_USER
self.password = config.WALLET_PASS
self.endpoint = '{}://{}:{}/json_rpc'.format(
self.proto, self.host, self.port
)
self.auth = requests.auth.HTTPDigestAuth(
self.username, self.password
)
try:
r = self.height()
height = r['height']
self.connected = True
except:
self.connected = False
def make_wallet_rpc(self, method, params={}):
r = requests.get(
self.endpoint,
data=json.dumps({'method': method, 'params': params}),
auth=self.auth
)
# print(r.status_code)
if 'error' in r.json():
return r.json()['error']
else:
return r.json()['result']
def height(self):
return self.make_wallet_rpc('get_height', {})
def spend_key(self):
return self.make_wallet_rpc('query_key', {'key_type': 'spend_key'})['key']
def view_key(self):
return self.make_wallet_rpc('query_key', {'key_type': 'view_key'})['key']
def seed(self):
return self.make_wallet_rpc('query_key', {'key_type': 'mnemonic'})['key']
def accounts(self):
accounts = []
_accounts = self.make_wallet_rpc('get_accounts')
idx = 0
self.master_address = _accounts['subaddress_accounts'][0]['base_address']
for _acc in _accounts['subaddress_accounts']:
assert idx == _acc['account_index']
accounts.append(_acc['account_index'])
idx += 1
return accounts
def new_account(self, label=None):
_account = self.make_wallet_rpc('create_account', {'label': label})
return _account['account_index']
def addresses(self, account=0, addr_indices=None):
qdata = {'account_index': account}
if addr_indices:
qdata['address_index'] = addr_indices
_addresses = self.make_wallet_rpc('get_address', qdata)
addresses = [None] * (max(map(operator.itemgetter('address_index'), _addresses['addresses'])) + 1)
for _addr in _addresses['addresses']:
addresses[_addr['address_index']] = _addr['address']
return addresses
def new_address(self, account=0, label=None):
data = {'account_index': account, 'label': label}
_address = self.make_wallet_rpc('create_address', data)
return (_address['address_index'], _address['address'])
def balances(self, account=0):
data = {'account_index': account}
_balance = self.make_wallet_rpc('get_balance', data)
return (from_atomic(_balance['balance']), from_atomic(_balance['unlocked_balance']))
def transfer(self, dest_address, amount, priority, account):
data = {
'account_index': account,
'destinations': [{'address': dest_address, 'amount': to_atomic(amount)}],
'priority': priority,
'unlock_time': 0,
'get_tx_key': True,
'get_tx_hex': True,
'new_algorithm': True,
'do_not_relay': False,
'ring_size': 22
}
transfer = self.make_wallet_rpc('transfer', data)
return transfer
def to_atomic(amount):
if not isinstance(amount, (Decimal, float) + six.integer_types):
raise ValueError("Amount '{}' doesn't have numeric type. Only Decimal, int, long and "
"float (not recommended) are accepted as amounts.")
return int(amount * 10**11)
def from_atomic(amount):
return (Decimal(amount) * PICOWOW).quantize(PICOWOW)
def as_wownero(amount):
return Decimal(amount).quantize(PICOWOW)

@ -1,4 +1,6 @@
from os import kill from os import kill
from datetime import datetime
from secrets import token_urlsafe
from sqlalchemy import Column, Integer, DateTime, String, ForeignKey from sqlalchemy import Column, Integer, DateTime, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func from sqlalchemy.sql import func
@ -53,3 +55,53 @@ class User(db.Model):
def __repr__(self): def __repr__(self):
return self.email return self.email
class Event(db.Model):
__tablename__ = 'events'
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(60))
user = db.Column(db.Integer, db.ForeignKey(User.id))
date = db.Column(db.DateTime, server_default=func.now())
def __repr__(self):
return self.id
class PasswordReset(db.Model):
__tablename__ = 'password_reset'
id = db.Column(db.Integer, primary_key=True)
user = db.Column(db.Integer, db.ForeignKey(User.id))
date = db.Column(db.DateTime, server_default=func.now())
hash = db.Column(db.String(80))
expiration_hours = db.Column(db.Integer)
expired = db.Column(db.Boolean, default=False)
def generate_hash(self):
return token_urlsafe(16)
def hours_elapsed(self):
now = datetime.utcnow()
diff = now - self.date
return diff.total_seconds() / 60 / 60
def __repr__(self):
return self.id
class DonatePrompt(db.Model):
__tablename__ = 'donate_prompts'
id = db.Column(db.Integer, primary_key=True)
user = db.Column(db.Integer, db.ForeignKey(User.id))
date = db.Column(db.DateTime, server_default=func.now())
def hours_elapsed(self):
now = datetime.utcnow()
diff = now - self.date
return diff.total_seconds() / 60 / 60
def __repr__(self):
return str(f'donate-prompt-{self.id}')

@ -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

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
{% include 'head.html' %}
<body id="page-top">
{% include 'navbar.html' %}
<!-- <header class="masthead">
<div class="container h-100">
<div class="row h-100">
<div class="col-lg-12 my-auto">
<div class="header-content mx-auto">
<h1 class="mb-4">Reset your password</h1>
<p>Wownero is a privacy centric cryptocurrency and is most safely managed on your own personal devices, on your own network, and with your own copy of the blockchain. This is a publicly accessible website, and while strict security measures are implemented on our servers, your account's security <strong>cannot</strong> be guaranteed. </p>
<p>If you decide to use this site for managing your funds, you do so at your own risk and are bound by the terms and conditions of this site. Practice good operational security and do not use this site for large amounts of funds.</p>
<div>
<a href="{{ url_for('meta.faq') }}">FAQ</a> -
<a href="{{ url_for('meta.terms') }}">Terms</a> -
<a href="{{ url_for('meta.privacy') }}">Privacy</a>
</div><br>
<a href="#register" class="btn btn-outline btn-xl js-scroll-trigger">Proceed</a>
</div>
</div>
</div>
</div>
</header> -->
<section class="section1" id="reset">
<div class="container">
<div class="section-heading text-center">
<form method="POST" action="">
{{ form.csrf_token }}
{% for f in form %}
{% if f.name != 'csrf_token' %}
{% if f.type == 'BooleanField' %}
<div class="form-group-span">
{{ f.label }}
{{ f }}
</div>
{% else %}
<div class="form-group">
{{ f.label }}
{{ f }}
</div>
{% endif %}
{% endif %}
{% endfor %}
<ul>
{% for field, errors in form.errors.items() %}
<li>{{ form[field].label }}: {{ ', '.join(errors) }}</li>
{% endfor %}
</ul>
<input type="submit" value="Reset" class="btn btn-link btn-outline-inverse btn-xl">
</form>
</div>
</div>
</section>
{% include 'footer.html' %}
{% include 'scripts.html' %}
</body>
</html>

@ -1,9 +1,29 @@
{% set desc = 'Wowstash; a web wallet for the Wownero cryptocurrency.' %}
{% set img = 'https://wowstash.app/static/img/wow-treasure-chest.png' %}
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content=""> <meta name="description" content="{{ desc }}">
<link rel="shortcut icon" href="/static/favicon.ico" /> <meta name="author" content="@lza_menace">
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<meta name="theme-color" content="#ffffff">
<meta name="apple-mobile-web-app-title" content="Wowstash">
<meta name="application-name" content="Wowstash">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="keywords" content="wownero, cryptocurrency, wallet">
<meta name="twitter:title" content="Wowstash">
<meta name="twitter:description" content="{{ desc }}">
<meta name="twitter:image" content="{{ img }}">
<meta name="twitter:card" content="summary_large_image">
<meta property="og:url" content="https://wowstash.app/">
<meta property="og:image" content="{{ img }}">
<meta property="og:description" content="{{ desc }}">
<meta property="og:title" content="Wowstash">
<link rel="shortcut icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/vendor/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/vendor/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/vendor/font-awesome/css/font-awesome.min.css"> <link rel="stylesheet" href="/static/vendor/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="/static/css/main.css"> <link rel="stylesheet" href="/static/css/main.css">

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
{% include 'head.html' %}
<body id="page-top">
{% include 'navbar.html' %}
<header class="masthead">
<div class="container h-100">
<div class="row h-100">
<div class="col-lg-12 my-auto">
<div class="header-content-sm mx-auto">
<h2 class="mb-4">Error</h2>
<p>There was an error - an administrator has been notified.</p>
<p>Error: <code style="background-color: white;">{{ error.original_exception }}</code></p>
</div>
</div>
</div>
</div>
</header>
{% include 'footer.html' %}
{% include 'scripts.html' %}
</body>
</html>

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
{% include 'head.html' %}
<body id="page-top">
{% include 'navbar.html' %}
<header class="masthead">
<div class="container h-100">
<div class="row h-100">
<div class="col-lg-12 my-auto">
<div class="header-content mx-auto">
<h1 class="mb-4">Donate</h1>
<p>Hey, this service is provided to you for free, please consider donating some WOW since I both donate my time and money keeping the service alive and paying for hosting.</p>
<p>lza_menace: <code style="background-color: white;">Wo59kvcHiDd48sstysDqGgBAN1fECLKALKw2bPUJhS4UjX9wj2SK4e4GH6HvrBmot6cBrWNE1T65UR6a5SLbzh882c1SXEhiK</code></p>
<a href="{{ url_for('wallet.dashboard') }}?to_addr=Wo59kvcHiDd48sstysDqGgBAN1fECLKALKw2bPUJhS4UjX9wj2SK4e4GH6HvrBmot6cBrWNE1T65UR6a5SLbzh882c1SXEhiK#send" class="btn btn-outline btn-xl js-scroll-trigger">Ok, take me to my wallet</a>
<br/><br/>
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target=_blank class="btn btn-outline btn-xl js-scroll-trigger">Nah, fuck you menace.</a>
</div>
</div>
</div>
</div>
</header>
{% include 'footer.html' %}
{% include 'scripts.html' %}
</body>
</html>

@ -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>

@ -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">

@ -17,7 +17,10 @@
<img src="data:image/png;base64,{{ qrcode }}" width=200 class="center"> <img src="data:image/png;base64,{{ qrcode }}" width=200 class="center">
<hr><br> <hr><br>
<h4>Balance</h4> <h4>Balance</h4>
<p class="inline">{{ balances[1] | from_atomic }} WOW ({{ balances[0] | from_atomic }} locked)</p> <p class="inline">{{ balances[1] | from_atomic }} WOW ({{ (balances[0] - balances[1]) | from_atomic }} locked)</p><br />
<p class="inline">Wallet height: {{ wallet_height['height'] }}</p><br />
<p class="inline">Node height: {{ node_height['height'] }}</p><br />
<p class="inline">Node address: {{ node_addr }}</p>
<span class="dashboard-buttons"> <span class="dashboard-buttons">
<div class="col-sm-6 dashboard-button"> <div class="col-sm-6 dashboard-button">
<a class="btn btn-lg btn-link btn-outline btn-xl js-scroll-trigger" href="#transfers">See Txes</a> <a class="btn btn-lg btn-link btn-outline btn-xl js-scroll-trigger" href="#transfers">See Txes</a>
@ -114,7 +117,7 @@
{{ send_form.csrf_token }} {{ send_form.csrf_token }}
<div class="form-group"> <div class="form-group">
{{ send_form.address.label }} {{ send_form.address.label }}
{{ send_form.address }} <input class="form-control" id="address" name="address" placeholder="Wownero address" required="" type="text" value="{{ request.args.to_addr }}">
</div> </div>
<div class="form-group"> <div class="form-group">
{{ send_form.amount.label }} {{ send_form.amount.label }}
@ -127,6 +130,7 @@
</ul> </ul>
<input type="submit" value="Send" class="btn btn-link btn-outline btn-xl"> <input type="submit" value="Send" class="btn btn-link btn-outline btn-xl">
</form> </form>
<a href="{{ url_for('meta.donate') }}" style="padding-top: 1em; display: block;">Donation Info</a>
</div> </div>
</div> </div>
</section> </section>
@ -154,6 +158,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 %}

@ -5,69 +5,32 @@
<body id="page-top"> <body id="page-top">
{% include 'navbar.html' %} <section class="section1" style="padding: 2em 0 !important;">
<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 'scripts.html' %} {% include 'scripts.html' %}
</body> </body>

@ -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>
Loading…
Cancel
Save