summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAssaf Gordon <assafgordon@gmail.com>2017-05-17 03:52:28 (GMT)
committerAssaf Gordon <assafgordon@gmail.com>2017-05-17 03:54:05 (GMT)
commite2ad8c1820462e4415b4cb281760d70fc5d96eb7 (patch)
tree7e0593e1eb1c378c2cb515855bfe8b4b45f53474
parent65377941f42112ac6262b09612e0390883b6d636 (diff)
downloadvaranusex-e2ad8c1820462e4415b4cb281760d70fc5d96eb7.zip
varanusex-e2ad8c1820462e4415b4cb281760d70fc5d96eb7.tar.gz
varanusex-e2ad8c1820462e4415b4cb281760d70fc5d96eb7.tar.bz2
account/lost-password: implement (without email sending yet)
-rw-r--r--varanusex/EmailToken.py49
-rw-r--r--varanusex/__init__.py6
-rw-r--r--varanusex/templates/account/lost-password-token.html70
-rw-r--r--varanusex/views/account.py43
-rw-r--r--varanusex/views/account_web_forms.py15
5 files changed, 179 insertions, 4 deletions
diff --git a/varanusex/EmailToken.py b/varanusex/EmailToken.py
new file mode 100644
index 0000000..4d0aaf2
--- /dev/null
+++ b/varanusex/EmailToken.py
@@ -0,0 +1,49 @@
+"""
+VaranusEx - Savannah Monitor
+
+Copyright (C) 2017 Assaf Gordon (assafgordon@gmail.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+# This simple module is based on:
+# http://explore-flask.readthedocs.io/en/latest/users.html
+
+from itsdangerous import URLSafeTimedSerializer
+
+class EmailToken(object):
+ def __init__(self, app=None):
+ self.ts = None
+ if app:
+ self.init_app(app)
+
+ def init_app(self, app):
+ self.ts = URLSafeTimedSerializer(app.config["SECRET_KEY"])
+
+ app.email_token = self
+
+ def generate_token(self,cleartext,salt=None):
+ return self.ts.dumps(cleartext,salt=salt)
+
+ def decode_token(self,token,salt=None,max_age=None):
+ #BadTimeSignature
+ #SignatureExpired
+ return self.ts.loads(token,salt=salt,max_age=max_age)
+
+ def valid_token(self,token,salt=None,max_age=None):
+ try:
+ a = decode_token(token,salt,max_age)
+ return True
+ except (BadTimeSignature,SignatureExpired):
+ return False
diff --git a/varanusex/__init__.py b/varanusex/__init__.py
index cb2b89f..faf1ffe 100644
--- a/varanusex/__init__.py
+++ b/varanusex/__init__.py
@@ -46,6 +46,12 @@ if app.config.get('DEBUG_SQL_ECHO',False):
login_manager = LoginManager()
login_manager.init_app(app)
+# Initialize the EmailToken module
+# (prepares signed,timed tokens to send by email)
+from varanusex.EmailToken import EmailToken
+email_token = EmailToken()
+email_token.init_app(app)
+
# Views and models must be loaded last, after app has been initialized.
# For objections to circular imports, see bottom notice at
diff --git a/varanusex/templates/account/lost-password-token.html b/varanusex/templates/account/lost-password-token.html
new file mode 100644
index 0000000..312d792
--- /dev/null
+++ b/varanusex/templates/account/lost-password-token.html
@@ -0,0 +1,70 @@
+{#
+VaranusEx - Savannah Monitor
+
+Copyright (C) 2017 Assaf Gordon (assafgordon@gmail.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+#}
+{% extends 'master-layout.html' %}
+
+{% block head %}
+<style>
+ div.formerror { color : red }
+</style>
+{% endblock %}
+
+{% block content %}
+
+<div class="header">
+ <h1>Savannah Account Configuration</h1>
+</div>
+
+<div class="content">
+
+ <h2 class="content-subhead">Reset Lost Password</h2>
+ <p>
+ <form class="pure-form pure-form-aligned" method="post">
+
+ {{ form.hidden_tag() }}
+
+ Please Enter the email associated with this account:
+ {{ form.email(size=40) }}
+ {% for message in form.email.errors %}
+ <div class="formerror">{{ message }}</div>
+ {% endfor %}
+ <br/>
+
+ {{ form.new_password1.label }}
+ {{ form.new_password1(size=20) }}
+ {% for message in form.new_password1.errors %}
+ <div class="formerror">{{ message }}</div>
+ {% endfor %}
+ <br/>
+
+ {{ form.new_password2.label }}
+ {{ form.new_password2(size=20) }}
+ {% for message in form.new_password2.errors %}
+ <div class="formerror">{{ message }}</div>
+ {% endfor %}
+ <br/>
+ <br/>
+
+ <input type="submit" value="Reset Password">
+ </form>
+
+ </p>
+
+</div>
+
+{% endblock %}
diff --git a/varanusex/views/account.py b/varanusex/views/account.py
index 1a4bf1e..3e50de9 100644
--- a/varanusex/views/account.py
+++ b/varanusex/views/account.py
@@ -19,13 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
from pprint import pprint
import re, datetime
from flask import Flask, render_template, url_for, redirect, request, \
- make_response
+ make_response, abort
from flask_login import login_required, login_user, \
logout_user, current_user
-
+import itsdangerous
from .account_web_forms import LoginForm, ChangeNameForm, ChangePasswordForm, \
ChangeEmailForm, ChangeSSHPubKeyForm, \
- LostPasswordForm
+ LostPasswordForm, ResetPasswordForm
from .. import app, db
from ..models.users import User
@@ -74,7 +74,10 @@ def account_lost_password():
email = f.email.data.strip()
user = User.query.filter_by(email=email).first()
if user:
- print("sending recovery to email '%s' (user '%s')" % (email, user.user_name))
+ token = app.email_token.generate_token(user.user_name,salt='lostpw')
+ url = url_for('account_lost_password_token',token=token,_external=True)
+ print("sending recovery to email '%s' (user '%s'), url= %s" \
+ % (email, user.user_name, url))
else:
print("password recovery attempt to unknown email '%s'" % (email))
@@ -85,6 +88,38 @@ def account_lost_password():
return render_template('account/lost-password.html',form=f)
+@app.route('/account/lostpw/<token>',methods=['GET','POST'])
+def account_lost_password_token(token):
+ try:
+ timeout = 60*60*3 # TODO: get from config file
+ user_name = app.email_token.decode_token(token,
+ salt='lostpw',
+ max_age=timeout)
+ except itsdangerous.BadTimeSignature as e:
+ abort(400) #TODO: add descriptive error + logs
+ except itsdangerous.SignatureExpired as e:
+ abort(400) #TODO: add descriptive error + logs
+
+ user = User.query.filter_by(user_name=user_name).first()
+ if not user:
+ # Something terribly wrong: a valid signature for username
+ # which isn't found?
+ abort(400)
+
+ f = ResetPasswordForm()
+ if f.validate_on_submit():
+ if user.email == f.email.data.strip():
+ # set the new password and continue
+ user.set_new_password(f.new_password1.data)
+ db.session.add(user)
+ db.session.commit()
+ # TODO: Flask message?
+ return redirect(url_for('login'))
+ else:
+ f.email.errors.append("Incorrect email")
+
+ return render_template('account/lost-password-token.html',form=f)
+
@login_required
@app.route("/my/account")
diff --git a/varanusex/views/account_web_forms.py b/varanusex/views/account_web_forms.py
index 0ebf29f..6262688 100644
--- a/varanusex/views/account_web_forms.py
+++ b/varanusex/views/account_web_forms.py
@@ -84,3 +84,18 @@ class LostPasswordForm(Form):
email = StringField('Email',
validators=[InputRequired(message="Email is required."),
Email(message="Invalid email address.")])
+
+class ResetPasswordForm(Form):
+ """
+ Change Password <Form>
+ """
+ email = StringField('Email',
+ validators=[InputRequired(message="Email is required."),
+ Email(message="Invalid email address.")])
+
+ new_password1 = PasswordField('New password',
+ validators=[InputRequired(),
+ EqualTo('new_password2',
+ message="passwords must match")])
+ new_password2 = PasswordField('Confirm new password',
+ validators=[InputRequired()])