diff --git a/wowstash/blueprints/auth/routes.py b/wowstash/blueprints/auth/routes.py index 6f23b6d..e77e05c 100644 --- a/wowstash/blueprints/auth/routes.py +++ b/wowstash/blueprints/auth/routes.py @@ -3,8 +3,8 @@ 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.helpers import capture_event @@ -95,3 +95,29 @@ def delete(): else: flash('Please confirm deletion of the account') return redirect(url_for('wallet.dashboard')) + +@auth_bp.route("/reset/", 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) diff --git a/wowstash/cli.py b/wowstash/cli.py index 7def856..f67e947 100644 --- a/wowstash/cli.py +++ b/wowstash/cli.py @@ -1,23 +1,43 @@ -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 +from wowstash.factory import db, bcrypt -# @app.errorhandler(404) -def not_found(error): - return make_response(jsonify({ - 'error': 'Page not found' - }), 404) +bp = Blueprint("cli", "cli", cli_group=None) -# @app.cli.command('initdb') -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() -# @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('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!') + + 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)}') diff --git a/wowstash/factory.py b/wowstash/factory.py index f51c5bf..2eb4731 100644 --- a/wowstash/factory.py +++ b/wowstash/factory.py @@ -63,31 +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}') - - @app.cli.command('init') - def init(): - import wowstash.models - db.create_all() - # 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 diff --git a/wowstash/forms.py b/wowstash/forms.py index 8afc725..a8c4337 100644 --- a/wowstash/forms.py +++ b/wowstash/forms.py @@ -32,3 +32,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') diff --git a/wowstash/models.py b/wowstash/models.py index 4582b64..ec39bbc 100644 --- a/wowstash/models.py +++ b/wowstash/models.py @@ -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 @@ -65,3 +67,25 @@ class Event(db.Model): 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 diff --git a/wowstash/templates/auth/reset.html b/wowstash/templates/auth/reset.html new file mode 100644 index 0000000..73f0d3c --- /dev/null +++ b/wowstash/templates/auth/reset.html @@ -0,0 +1,67 @@ + + + + {% include 'head.html' %} + + + + {% include 'navbar.html' %} + + + +
+
+
+
+ {{ form.csrf_token }} + {% for f in form %} + {% if f.name != 'csrf_token' %} + {% if f.type == 'BooleanField' %} +
+ {{ f.label }} + {{ f }} +
+ {% else %} +
+ {{ f.label }} + {{ f }} +
+ {% endif %} + {% endif %} + {% endfor %} +
    + {% for field, errors in form.errors.items() %} +
  • {{ form[field].label }}: {{ ', '.join(errors) }}
  • + {% endfor %} +
+ +
+
+
+
+ + {% include 'footer.html' %} + + {% include 'scripts.html' %} + + + +