Compare commits

...

17 Commits

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

@ -10,12 +10,15 @@ export FLASK_ENV=production
mkdir -p $BASE
pgrep -F $BASE/gunicorn.pid
if [[ $? != 0 ]]; then
gunicorn \
--bind 0.0.0.0:4001 "wowstash.app:app" \
--bind 127.0.0.1:4001 "wowstash.app:app" \
--daemon \
--log-file $BASE/gunicorn.log \
--pid $BASE/gunicorn.pid \
--access-logfile $BASE/access.log \
--reload
sleep 1
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:
image: postgres:9.6.15-alpine
container_name: wowstash_db
restart: unless-stopped
ports:
- 5432:5432
- 127.0.0.1:5432:5432
environment:
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_USER: ${DB_USER}
@ -17,4 +18,23 @@ services:
image: redis:latest
container_name: wowstash_cache
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 time import sleep
from wowstash.blueprints.auth import auth_bp
from wowstash.forms import Register, Login, Delete
from wowstash.models import User
from wowstash.forms import Register, Login, Delete, ResetPassword
from wowstash.models import User, PasswordReset
from wowstash.factory import db, bcrypt
from wowstash.library.docker import docker
from wowstash.library.elasticsearch import send_es
from wowstash.library.helpers import capture_event
@auth_bp.route("/register", methods=["GET", "POST"])
@ -33,7 +33,7 @@ def register():
db.session.commit()
# Capture event, login user and redirect to wallet page
send_es({'type': 'register', 'user': user.email})
capture_event(user.id, 'register')
login_user(user)
return redirect(url_for('wallet.setup'))
@ -63,7 +63,7 @@ def login():
return redirect(url_for('auth.login'))
# Capture event, login user, and redirect to wallet page
send_es({'type': 'login', 'user': user.email})
capture_event(user.id, 'login')
login_user(user)
return redirect(url_for('wallet.dashboard'))
@ -73,9 +73,9 @@ def login():
def logout():
if current_user.is_authenticated:
docker.stop_container(current_user.wallet_container)
send_es({'type': 'stop_container', 'user': current_user.email})
capture_event(current_user.id, 'stop_container')
current_user.clear_wallet_data()
send_es({'type': 'logout', 'user': current_user.email})
capture_event(current_user.id, 'logout')
logout_user()
return redirect(url_for('meta.index'))
@ -85,13 +85,39 @@ def delete():
form = Delete()
if form.validate_on_submit():
docker.stop_container(current_user.wallet_container)
send_es({'type': 'stop_container', 'user': current_user.email})
capture_event(current_user.id, 'stop_container')
sleep(1)
docker.delete_wallet_data(current_user.id)
send_es({'type': 'delete_wallet', 'user': current_user.email})
capture_event(current_user.id, 'delete_wallet')
current_user.clear_wallet_data(reset_password=True, reset_wallet=True)
flash('Successfully deleted wallet data')
return redirect(url_for('wallet.setup'))
else:
flash('Please confirm deletion of the account')
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.db import Database
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('/')
def index():
return render_template('meta/index.html', node=daemon.info(), info=cache.get_coin_info())
@ -30,8 +36,6 @@ def health():
'docker': Docker().client.ping()
}), 200)
# @app.errorhandler(404)
# def not_found(error):
# return make_response(jsonify({
# 'error': 'Page not found'
# }), 404)
@meta_bp.route('/donate')
def donate():
return render_template('meta/donate.html')

@ -10,12 +10,12 @@ from socket import socket
from datetime import datetime
from wowstash.blueprints.wallet import wallet_bp
from wowstash.library.docker import docker
from wowstash.library.elasticsearch import send_es
from wowstash.library.jsonrpc import Wallet, to_atomic
from wowstash.library.helpers import capture_event
from wowstash.library.jsonrpc import Wallet, daemon, to_atomic
from wowstash.library.cache import cache
from wowstash.forms import Send, Delete, Restore
from wowstash.factory import db
from wowstash.models import User
from wowstash.models import User, DonatePrompt
from wowstash import config
@ -29,6 +29,7 @@ def setup():
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'))
@ -69,6 +70,25 @@ def dashboard():
sleep(1.5)
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()
transfers = wallet.get_transfers()
for type in transfers:
@ -81,10 +101,13 @@ def dashboard():
seed = wallet.seed()
spend_key = wallet.spend_key()
view_key = wallet.view_key()
send_es({'type': 'load_dashboard', 'user': current_user.email})
capture_event(current_user.id, 'load_dashboard')
return render_template(
'wallet/dashboard.html',
transfers=all_transfers,
wallet_height=wallet.height(),
node_height=daemon.height(),
node_addr=config.DAEMON_HOST,
sorted_txes=get_sorted_txes(transfers),
balances=balances,
address=address,
@ -115,6 +138,7 @@ def connect():
current_user.wallet_container = wallet
current_user.wallet_start = datetime.utcnow()
db.session.commit()
capture_event(current_user.id, 'start_wallet')
data = {
'result': 'success',
'message': 'Wallet has been connected'
@ -133,6 +157,7 @@ def create():
if current_user.wallet_created is False:
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
db.session.commit()
return redirect(url_for('wallet.loading'))
@ -173,13 +198,13 @@ def send():
# Check if Wownero wallet is available
if wallet.connected is False:
flash('Wallet RPC interface is unavailable at this time. Try again later.')
send_es({'type': 'tx_fail_rpc_unavailable', 'user': user.email})
capture_event(user.id, 'tx_fail_rpc_unavailable')
return redirect(redirect_url)
# Quick n dirty check to see if address is WOW
if len(address) not in [97, 108]:
flash('Invalid Wownero address provided.')
send_es({'type': 'tx_fail_address_invalid', 'user': user.email})
capture_event(user.id, 'tx_fail_address_invalid')
return redirect(redirect_url)
# Check if we're sweeping or not
@ -191,7 +216,7 @@ def send():
amount = to_atomic(Decimal(send_form.amount.data))
except:
flash('Invalid Wownero amount specified.')
send_es({'type': 'tx_fail_amount_invalid', 'user': user.email})
capture_event(user.id, 'tx_fail_amount_invalid')
return redirect(redirect_url)
# Send transfer
@ -202,10 +227,10 @@ def send():
msg = tx['message'].capitalize()
msg_lower = tx['message'].replace(' ', '_').lower()
flash(f'There was a problem sending the transaction: {msg}')
send_es({'type': f'tx_fail_{msg_lower}', 'user': user.email})
capture_event(user.id, f'tx_fail_{msg_lower[0:50]}')
else:
flash('Successfully sent transfer.')
send_es({'type': 'tx_success', 'user': user.email})
capture_event(user.id, 'tx_success')
return redirect(redirect_url)
else:

@ -1,23 +1,67 @@
from wowstash.library.jsonrpc import wallet
from wowstash.models import Transaction
from wowstash.factory import db
import click
from flask import Blueprint, url_for
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')
def init_db():
bp = Blueprint("cli", "cli", cli_group=None)
@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()
# @app.cli.command('send_transfers')
def send_transfers():
txes = Transaction.query.all()
for i in txes:
print(i)
# tx = wallet.transfer(
# 0, current_user.subaddress_index, address, amount
# )
@bp.cli.command('list_users')
def list_users():
users = User.query.all()
for i in users:
print(f'{i.id} - {i.email}')
@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
TEMPLATES_AUTO_RELOAD = True
ELASTICSEARCH_ENABLED = False
# Social
SOCIAL = {

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

@ -1,4 +1,6 @@
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 wtforms import StringField, BooleanField
from wtforms.validators import DataRequired, ValidationError
@ -11,6 +13,12 @@ class Register(FlaskForm):
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"})
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):
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"})
@ -32,3 +40,11 @@ class Restore(FlaskForm):
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')

@ -9,7 +9,6 @@ from wowstash import config
from wowstash.models import User
from wowstash.factory import db
from wowstash.library.jsonrpc import daemon
from wowstash.library.elasticsearch import send_es
class Docker(object):
@ -32,6 +31,7 @@ class Docker(object):
--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"
@ -44,6 +44,7 @@ class Docker(object):
--mnemonic-language English \
--daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \
--daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \
--trusted-daemon \
--log-file /wallet/{u.id}-init.log \
--command version
"""
@ -66,7 +67,6 @@ class Docker(object):
}
}
)
send_es({'type': f'init_wallet', 'user': u.email})
return container.short_id
def start_wallet(self, user_id):
@ -83,6 +83,7 @@ class Docker(object):
--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 \
--log-file /wallet/{u.id}-rpc.log
"""
try:
@ -103,7 +104,6 @@ class Docker(object):
}
}
)
send_es({'type': 'start_wallet', 'user': u.email})
return container.short_id
except APIError as e:
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,4 +1,6 @@
from os import kill
from datetime import datetime
from secrets import token_urlsafe
from sqlalchemy import Column, Integer, DateTime, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func
@ -53,3 +55,53 @@ class User(db.Model):
def __repr__(self):
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}')

@ -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>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="/static/favicon.ico" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{ desc }}">
<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/font-awesome/css/font-awesome.min.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>

@ -17,7 +17,10 @@
<img src="data:image/png;base64,{{ qrcode }}" width=200 class="center">
<hr><br>
<h4>Balance</h4>
<p class="inline">{{ balances[1] | from_atomic }} WOW ({{ (balances[0] - balances[1]) | 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">
<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>
@ -114,7 +117,7 @@
{{ send_form.csrf_token }}
<div class="form-group">
{{ 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 class="form-group">
{{ send_form.amount.label }}
@ -127,6 +130,7 @@
</ul>
<input type="submit" value="Send" class="btn btn-link btn-outline btn-xl">
</form>
<a href="{{ url_for('meta.donate') }}" style="padding-top: 1em; display: block;">Donation Info</a>
</div>
</div>
</section>

@ -5,7 +5,7 @@
<body id="page-top">
<section class="section1">
<section class="section1" style="padding: 2em 0 !important;">
<div class="container">
<div class="section-heading text-center">
<h2>Your wallet is connecting</h2>
@ -31,8 +31,6 @@
</div>
</section>
{% include 'footer.html' %}
{% include 'scripts.html' %}
</body>

Loading…
Cancel
Save