Skip to content

Commit b703dc9

Browse files
committed
implements forgot password flow
1 parent df6002d commit b703dc9

File tree

9 files changed

+212
-6
lines changed

9 files changed

+212
-6
lines changed

app.py

+113-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
from flask import Flask, make_response, request, render_template, flash, redirect, Markup, jsonify, url_for, session
1717
from werkzeug.security import generate_password_hash, check_password_hash
1818
from dbms.models import user as userModel
19-
from utils import allowed_to_register, is_blacklisted
20-
from forms import SignupForm, LoginForm, UpdateForm
19+
from utils import allowed_to_register, is_blacklisted, check_email
20+
from forms import SignupForm, LoginForm, UpdateForm, ForgotForm, PasswordResetForm
2121
from flask_jwt_extended import create_access_token, \
2222
get_jwt_identity, jwt_required, \
2323
JWTManager, current_user, \
@@ -692,5 +692,115 @@ def fields_count_by_domain():
692692
}), 400
693693

694694

695+
@app.route('/forgot-password', methods=['GET', 'POST'])
696+
@jwt_required(optional=True)
697+
@csrf.exempt
698+
def forgot_password():
699+
app.config["WTF_CSRF_ENABLED"] = False
700+
user_agent = request.headers.get('User-Agent')
701+
postman_notebook_request = utils.check_non_web_user_agent(user_agent)
702+
703+
# check if already logged in
704+
user = get_identity_if_logedin()
705+
if user:
706+
if not postman_notebook_request:
707+
return redirect(app.config['DEVELOPMENT_BASE_URL'] + '/home')
708+
elif postman_notebook_request:
709+
return jsonify({'message': 'Already logged in'})
710+
711+
form = ForgotForm()
712+
713+
# POST
714+
if form.validate_on_submit():
715+
# gets email and password
716+
email = form.email.data
717+
718+
# check account exists
719+
user = userModel.User.query \
720+
.filter_by(email=email) \
721+
.first()
722+
if user:
723+
# create email
724+
token = generate_confirmation_token(email)
725+
url = url_for('reset_password', token=token, _external=True)
726+
html = render_template('reset-email.html', reset_url=url)
727+
subject = 'Reset password'
728+
729+
# send email
730+
send_email(email, subject, html)
731+
732+
# response
733+
msg = 'A link to reset your password has been sent to your email!'
734+
if postman_notebook_request:
735+
return jsonify({"message": msg})
736+
else:
737+
flash(message=Markup(f'A password reset link has been sent to "{email}".'), category='info')
738+
else:
739+
# unregistered or unactivated account
740+
msg = 'Account does not exist!'
741+
if postman_notebook_request:
742+
return jsonify({"message": msg})
743+
else:
744+
flash(message=Markup(f'A user with email "{email}" does not exist.'), category='info')
745+
746+
# GET
747+
return render_template('forgot-password.html', form=form)
748+
749+
750+
@app.route('/reset-password/<token>', methods=['GET', 'POST'])
751+
@jwt_required(optional=True)
752+
@csrf.exempt
753+
def reset_password(token):
754+
app.config["WTF_CSRF_ENABLED"] = False
755+
user_agent = request.headers.get('User-Agent')
756+
postman_notebook_request = utils.check_non_web_user_agent(user_agent)
757+
758+
# check if already logged in
759+
user = get_identity_if_logedin()
760+
if user:
761+
if not postman_notebook_request:
762+
return redirect(app.config['DEVELOPMENT_BASE_URL'] + '/home')
763+
elif postman_notebook_request:
764+
return jsonify({'message': 'Already logged in'})
765+
766+
try:
767+
# check if token is valid email
768+
email = confirm_token(token)
769+
770+
# checking if user exists
771+
user = userModel.User.query \
772+
.filter_by(email=email) \
773+
.first()
774+
if user:
775+
form = PasswordResetForm()
776+
# POST
777+
if form.validate_on_submit():
778+
# get new password
779+
password = form.password.data
780+
user_to_update = userModel.User.query.filter_by(email=email).first()
781+
782+
user_to_update.password = generate_password_hash(password)
783+
db.session.commit()
784+
785+
# response
786+
msg = 'Password updated succesfully!'
787+
if postman_notebook_request:
788+
return jsonify({"message": msg})
789+
else:
790+
flash(message=Markup(f'Password for email "{email}" has been updated.'), category='info')
791+
return redirect(app.config['DEVELOPMENT_BASE_URL'] + '/')
792+
793+
return render_template('reset-password.html', form=form)
794+
795+
except:
796+
msg = 'The confirmation link is invalid or has expired.'
797+
if postman_notebook_request:
798+
return jsonify({"message": msg})
799+
else:
800+
flash(message=msg, category='danger')
801+
return redirect(app.config['DEVELOPMENT_BASE_URL'] + '/forgot-password')
802+
803+
804+
695805
if __name__ == '__main__':
696-
app.run(host='0.0.0.0')
806+
app.run(host='0.0.0.0', debug=True)

dbms/templates/base.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<a class="nav-link {{ 'active' if active_page == 'home' else '' }}" aria-current="page" href="{{ url_for('home') }}">Home</a>
4747
</li>
4848
<li class="nav-item">
49-
<a class="nav-link {{ 'active' if active_page == 'update' else '' }}" aria-current="page" href="{{ url_for('dashboard') }}">Dashboard</a>
49+
<a class="nav-link {{ 'active' if active_page == 'dashboard' else '' }}" aria-current="page" href="{{ url_for('dashboard') }}">Dashboard</a>
5050
</li>
5151
<li class="nav-item">
5252
<a class="nav-link {{ 'active' if active_page == 'update' else '' }}" aria-current="page" href="{{ url_for('update') }}">Account Settings</a>

dbms/templates/dashboard.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends 'base.html' %}
2-
{% set active_page='home' %}
2+
{% set active_page='dashboard' %}
33
{% block title %}
44
Home
55
{% endblock %}

dbms/templates/forgot-password.html

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{% extends 'auth-template.html' %}
2+
{% set active_page='login' %}
3+
{% block title %}
4+
Forgot Password
5+
{% endblock %}
6+
{% block form_heading %}
7+
Find your account
8+
{% endblock %}
9+
{% block auth_form_content %}
10+
<!-- Email input -->
11+
12+
<div class="form-outline mb-4 text-start">
13+
{{ form.email.label(class_="form-label ms-1") }}
14+
{{ form.email(class_="form-control", placeholder_="Enter your email") }}
15+
{% if form.email.errors %}
16+
<ul>
17+
{% for error in form.email.errors %}
18+
<li>{{ error }}</li>
19+
{% endfor %}
20+
</ul>
21+
{% endif %}
22+
</div>
23+
24+
<!-- Submit button -->
25+
<input type="submit" class="btn btn-primary btn-block mb-4" value="Find account">
26+
27+
<!-- Section: Design Block -->
28+
{% endblock %}

dbms/templates/login.html

+9
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,18 @@
4545

4646
<!-- Submit button -->
4747
<input type="submit" class="btn btn-primary btn-block mb-4" value="Sign in">
48+
49+
<!-- Forgot password link -->
4850
<div class="text-center">
51+
<a href="{{ url_for('forgot_password') }}">Forgot password?</a>
52+
53+
</div>
54+
55+
<!-- Sign up link -->
56+
<div class="text-center mt-2">
4957
<p>Don't have an account? <a href="{{ url_for('signup') }}">Sign up</a></p>
5058

5159
</div>
60+
5261
<!-- Section: Design Block -->
5362
{% endblock %}

dbms/templates/reset-email.html

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<p>Greetings! Follow this link to reset your password:</p>
2+
<p><a href="{{ reset_url }}">{{ reset_url }}</a></p>
3+
<br>
4+
<p>Cheers!</p>

dbms/templates/reset-password.html

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{% extends 'auth-template.html' %}
2+
{% set active_page='login' %}
3+
{% block title %}
4+
Reset Password
5+
{% endblock %}
6+
{% block form_heading %}
7+
Reset Password
8+
{% endblock %}
9+
{% block auth_form_content %}
10+
11+
<!-- Password input -->
12+
13+
<div class="form-outline mb-4 text-start">
14+
{{ form.password.label(class_="form-label ms-1") }}
15+
{{ form.password(class_="form-control", placeholder_="Enter your password") }}
16+
17+
{% if form.password.errors %}
18+
<ul>
19+
{% for error in form.password.errors %}
20+
<li>{{ error }}</li>
21+
{% endfor %}
22+
</ul>
23+
{% endif %}
24+
<p class="text-muted ms-1">Password must be at least 8 characters long. It must contain an upper-case letter, a lower-case letter and a number</p>
25+
</div>
26+
27+
<div class="form-outline mb-4 text-start">
28+
{{ form.confirm_pass.label(class_="form-label ms-1") }}
29+
{{ form.confirm_pass(class_="form-control", placeholder_="Confirm your password") }}
30+
{% if form.confirm_pass.errors %}
31+
<ul>
32+
{% for error in form.confirm_pass.errors %}
33+
<li>{{ error }}</li>
34+
{% endfor %}
35+
</ul>
36+
{% endif %}
37+
</div>
38+
39+
<!-- Submit button -->
40+
<input type="submit" class="btn btn-primary btn-block mb-4" value="Reset Password">
41+
42+
<!-- Section: Design Block -->
43+
{% endblock %}

forms.py

+12
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,15 @@ class UpdateForm(FlaskForm):
4545
"match!")])
4646

4747
discoverable = BooleanField('Do you want your profile to be discoverable?')
48+
49+
50+
class ForgotForm(FlaskForm):
51+
email = StringField('Email', validators=[InputRequired(), Email(message='Enter a valid email')])
52+
53+
54+
class PasswordResetForm(FlaskForm):
55+
password = PasswordField('New Password',
56+
validators=[DataRequired(), Regexp("^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}", message="Please follow the guidelines for a strong password")])
57+
confirm_pass = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password', message="Passwords "
58+
"don't "
59+
"match!")])

utils_activation/email.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ def send_email(to, subject, template):
1616
)
1717
mail.send(msg)
1818
except Exception as e:
19-
return e;
19+
return e

0 commit comments

Comments
 (0)