Compare commits

..

7 Commits

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

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

@ -15,7 +15,7 @@ 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, Restore from wowstash.forms import Send, Delete, Restore
from wowstash.factory import db from wowstash.factory import db
from wowstash.models import User, ServerMessage from wowstash.models import User, DonatePrompt
from wowstash import config from wowstash import config
@ -70,6 +70,25 @@ def dashboard():
sleep(1.5) 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:
@ -83,9 +102,6 @@ def dashboard():
spend_key = wallet.spend_key() spend_key = wallet.spend_key()
view_key = wallet.view_key() view_key = wallet.view_key()
capture_event(current_user.id, 'load_dashboard') capture_event(current_user.id, 'load_dashboard')
sm = ServerMessage.query.all()
if sm:
flash(f'Server message from admin: <a href="https://google.com">yo</a>')
return render_template( return render_template(
'wallet/dashboard.html', 'wallet/dashboard.html',
transfers=all_transfers, transfers=all_transfers,
@ -211,7 +227,7 @@ 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(user.id, f'tx_fail_{msg_lower}') capture_event(user.id, f'tx_fail_{msg_lower[0:50]}')
else: else:
flash('Successfully sent transfer.') flash('Successfully sent transfer.')
capture_event(user.id, 'tx_success') capture_event(user.id, 'tx_success')

@ -3,7 +3,7 @@ from flask import Blueprint, url_for
import wowstash.models import wowstash.models
from wowstash.library.docker import docker from wowstash.library.docker import docker
from wowstash.models import User, PasswordReset, ServerMessage from wowstash.models import User, PasswordReset, Event
from wowstash.factory import db, bcrypt from wowstash.factory import db, bcrypt
@ -25,6 +25,29 @@ def reset_wallet(user_id):
def init(): def init():
db.create_all() db.create_all()
@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') @bp.cli.command('reset_password')
@click.argument('user_email') @click.argument('user_email')
@click.argument('duration') @click.argument('duration')
@ -42,16 +65,3 @@ def reset_password(user_email, duration):
db.session.add(pwr) db.session.add(pwr)
db.session.commit() 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)}') click.echo(f'[+] Password reset link #{pwr.id} for {user_email} expires in {duration} hours: {url_for("auth.reset", hash=pwr.hash)}')
@bp.cli.command('set_message')
@click.argument('msg_content')
def set_message(msg_content):
s = ServerMessage.query.all()
if s:
db.session.delete(s.first())
db.session.commit()
_s = ServerMessage(
content=msg_content
)
db.session.add(_s)
db.session.commit()

@ -1,4 +1,6 @@
from re import match as re_match 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, ValidationError 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"}) 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"})

@ -31,6 +31,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 \
--electrum-seed '{seed}' \ --electrum-seed '{seed}' \
--log-file /wallet/{u.id}-init.log \ --log-file /wallet/{u.id}-init.log \
--command refresh" --command refresh"
@ -43,6 +44,7 @@ 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} \
--trusted-daemon \
--log-file /wallet/{u.id}-init.log \ --log-file /wallet/{u.id}-init.log \
--command version --command version
""" """
@ -81,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:

@ -1,5 +1,8 @@
import requests
from wowstash.models import Event from wowstash.models import Event
from wowstash.factory import db from wowstash.factory import db
from wowstash import config
def capture_event(user_id, event_type): def capture_event(user_id, event_type):
@ -10,3 +13,11 @@ def capture_event(user_id, event_type):
db.session.add(event) db.session.add(event)
db.session.commit() db.session.commit()
return 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

@ -90,12 +90,18 @@ class PasswordReset(db.Model):
def __repr__(self): def __repr__(self):
return self.id return self.id
class ServerMessage(db.Model):
__tablename__ = 'server_message' class DonatePrompt(db.Model):
__tablename__ = 'donate_prompts'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text) user = db.Column(db.Integer, db.ForeignKey(User.id))
date = db.Column(db.DateTime, server_default=func.now()) 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): def __repr__(self):
return self.id return str(f'donate-prompt-{self.id}')

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

@ -1,35 +0,0 @@
<!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">Privacy Policy</h1>
<p>Here is the information I will collect from you:</p>
<ul>
<li>Web server access logs (Source IP address, browser user-agent, page requests, etc)</li>
<li>Email address and salted/hashed password (registration)</li>
<li>Application events and metrics (function execution and the user who triggered)</li>
</ul>
<p>I check logs and capture events for troubleshooting purposes only. None of this data is shared with any third parties because I'm not a fucking lame.</p>
</div>
</div>
</div>
</div>
</header>
{% include 'footer.html' %}
{% include 'scripts.html' %}
</body>
</html>

@ -117,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 }}
@ -130,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>