Skip to content

Commit d8f5254

Browse files
Adrien forgotpassword (#61)
Added forgot password implementation with database
2 parents 8f72254 + f6f970e commit d8f5254

File tree

9 files changed

+332
-14
lines changed

9 files changed

+332
-14
lines changed

.DS_Store

6 KB
Binary file not shown.

flaskApp/.DS_Store

6 KB
Binary file not shown.

flaskApp/app.py

Lines changed: 143 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, json
22
import firebase_admin
33
from firebase_admin import credentials, firestore
4-
import bcrypt
4+
import bcrypt, secrets
55
from firebase.config import Config
66
from db_utils import upload_file_to_db, connect_to_database
77
from flask_mail import Mail, Message
88
import email_credentials
9+
from datetime import datetime, timedelta, timezone
910

1011
app = Flask(__name__)
1112
app.config.from_object(Config)
@@ -108,13 +109,133 @@ def pre_approved_access_code():
108109
return redirect(url_for('homepage'))
109110
return render_template('pre_approved_access_code.html')
110111

112+
@app.route('/reset_password', methods=['GET', 'POST'])
113+
def reset_password():
114+
if request.method == 'POST':
115+
email = request.form.get('email').strip().lower()
116+
user_ref = db.collection('users').document(email)
117+
user_doc = user_ref.get()
118+
119+
if user_doc.exists:
120+
verification_secret = secrets.token_hex(3)
121+
expiration_time = datetime.now(timezone.utc) + timedelta(minutes=15)
122+
123+
124+
user_ref.update({
125+
'verification_code': verification_secret,
126+
'verification_expiry': expiration_time
127+
})
128+
129+
subject = "HUA Password Reset"
130+
body = f"""Hi,
131+
132+
You recently requested a password reset for HUA. Your verification code is below.
133+
134+
{verification_secret}
135+
136+
This code will expire in 15 minutes.
137+
138+
If you didn't request a password reset, you can safely disregard this email.
139+
140+
This account is not monitored, please do not reply to this email.
141+
"""
142+
try:
143+
msg = Message(subject, sender="Huaverso@gmail.com", recipients=[email], body=body)
144+
mail.send(msg)
145+
146+
flash("A verification code has been sent to your email.", "success")
147+
return redirect(url_for('enter_code', email=email))
148+
except Exception:
149+
flash("Failed to send verification email. Please try again later.", "danger")
150+
return redirect(url_for('reset_password'))
151+
else:
152+
flash("No account with this email exists.", "danger")
153+
return redirect(url_for('reset_password'))
154+
155+
return render_template('reset_password.html')
156+
157+
@app.route('/enter_code', methods=['GET', 'POST'])
158+
def enter_code():
159+
email = request.args.get('email')
160+
if not email:
161+
flash("Invalid request.", "danger")
162+
return redirect(url_for('reset_password'))
163+
164+
if request.method == 'POST':
165+
entered_code = request.form.get('verification_code').strip()
166+
user_ref = db.collection('users').document(email)
167+
user_doc = user_ref.get()
168+
169+
if not user_doc.exists:
170+
flash("Invalid request.", "danger")
171+
return redirect(url_for('reset_password'))
172+
173+
stored_code = user_doc.to_dict().get('verification_code')
174+
expiry_time = user_doc.to_dict().get('verification_expiry')
175+
176+
if not stored_code or not expiry_time:
177+
flash("No verification code found. Please initiate the password reset again.", "danger")
178+
return redirect(url_for('reset_password'))
179+
180+
if entered_code != stored_code:
181+
flash("Invalid verification code.", "danger")
182+
return redirect(url_for('enter_code', email=email))
183+
184+
if datetime.now(timezone.utc) > expiry_time:
185+
flash("The verification code has expired. Please request a new one.", "danger")
186+
user_ref.update({
187+
'verification_code': firestore.DELETE_FIELD,
188+
'verification_expiry': firestore.DELETE_FIELD
189+
})
190+
return redirect(url_for('reset_password'))
191+
192+
return redirect(url_for('new_password', email=email))
193+
194+
return render_template('enter_code.html', email=email)
195+
196+
@app.route('/new_password', methods=['GET', 'POST'])
197+
def new_password():
198+
email = request.args.get('email')
199+
if not email:
200+
flash("Invalid request.", "danger")
201+
return redirect(url_for('reset_password'))
202+
203+
if request.method == 'POST':
204+
new_password = request.form.get('new_password')
205+
confirm_password = request.form.get('confirm_password')
206+
207+
if new_password != confirm_password:
208+
flash("Passwords do not match.", "danger")
209+
return redirect(url_for('new_password', email=email))
210+
211+
if len(new_password) < 6:
212+
flash("Password must be at least 6 characters long.", "danger")
213+
return redirect(url_for('new_password', email=email))
214+
215+
# Hash the new password and decode to store as string
216+
hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
217+
user_ref = db.collection('users').document(email)
218+
user_ref.update({'password': hashed_password})
219+
220+
user_ref.update({
221+
'verification_code': firestore.DELETE_FIELD,
222+
'verification_expiry': firestore.DELETE_FIELD
223+
})
224+
225+
flash("Your password has been successfully reset. You can now log in.", "success")
226+
return redirect(url_for('home'))
227+
228+
return render_template('new_password.html', email=email)
229+
230+
231+
111232

112233
@app.route('/login', methods=['GET', 'POST'])
113234
def login():
114-
email = request.form.get('email')
115-
password = request.form.get('password')
116-
117235
if request.method == 'POST':
236+
print("Form Data:", request.form)
237+
email = request.form.get('email')
238+
password = request.form.get('password')
118239
try:
119240
# Retrieve user data from Firestore
120241
user_ref = db.collection('users').document(email)
@@ -123,11 +244,21 @@ def login():
123244
if user_doc.exists:
124245
stored_hashed_password = user_doc.to_dict().get('password')
125246

126-
# Check if the password matches
127-
if bcrypt.checkpw(password.encode('utf-8'), stored_hashed_password.encode('utf-8')):
128-
return redirect(url_for('homepage', email=email))
247+
# Ensure the stored hashed password exists
248+
if stored_hashed_password:
249+
# Encode the stored hashed password to bytes
250+
stored_hashed_password_bytes = stored_hashed_password.encode('utf-8')
251+
252+
# Check if the password matches
253+
if bcrypt.checkpw(password.encode('utf-8'), stored_hashed_password_bytes):
254+
flash("Logged in successfully.", "success")
255+
return redirect(url_for('homepage', email=email))
256+
else:
257+
flash("Invalid password", "danger")
258+
return redirect(url_for('login'))
259+
129260
else:
130-
flash("Invalid password", "danger")
261+
flash("Password not set for this account.", "danger")
131262
return redirect(url_for('login'))
132263
else:
133264
flash("User not found", "danger")
@@ -136,8 +267,10 @@ def login():
136267
except Exception as e:
137268
flash(f"An error occurred: {str(e)}", "danger")
138269
return redirect(url_for('login'))
139-
if request.method == "GET":
140-
return render_template('login.html')
270+
271+
# Handle GET requests
272+
return render_template('login.html')
273+
141274

142275
@app.route("/dashboard")
143276
def dashboard():
-832 Bytes
Binary file not shown.

flaskApp/templates/enter_code.html

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Enter Verification Code</title>
7+
<!-- Bootstrap CSS -->
8+
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
9+
</head>
10+
<body>
11+
<div class="container mt-5">
12+
<div class="row justify-content-center">
13+
<div class="col-md-6">
14+
{% with messages = get_flashed_messages(with_categories=true) %}
15+
{% if messages %}
16+
{% for category, message in messages %}
17+
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
18+
{{ message }}
19+
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
20+
<span aria-hidden="true">&times;</span>
21+
</button>
22+
</div>
23+
{% endfor %}
24+
{% endif %}
25+
{% endwith %}
26+
<h2 class="text-center">Enter Verification Code</h2>
27+
<form method="POST" action="{{ url_for('enter_code') }}?email={{ email }}">
28+
<input type="hidden" name="email" value="{{ email }}">
29+
<div class="form-group">
30+
<label for="verificationCode">Verification Code</label>
31+
<input type="text" id="verificationCode" name="verification_code" class="form-control" placeholder="Enter verification code" required>
32+
</div>
33+
<button type="submit" class="btn btn-primary btn-block">Verify Code</button>
34+
</form>
35+
<a href="{{ url_for('reset_password') }}" class="btn btn-secondary btn-block mt-2">Back</a>
36+
</div>
37+
</div>
38+
</div>
39+
<!-- Bootstrap JS -->
40+
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
41+
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
42+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
43+
</body>
44+
</html>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Register</title>
7+
<!-- Bootstrap CSS -->
8+
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
9+
</head>
10+
<body>
11+
<div class="container mt-5">
12+
<div class="row justify-content-center">
13+
<div class="col-md-4">
14+
<h2 class="text-center">Reset Your Password</h2>
15+
16+
<!-- Flash message for successful or failed reset -->
17+
{% with messages = get_flashed_messages() %}
18+
{% if messages %}
19+
<div class="alert alert-danger" role="alert">
20+
{{ messages[0] }}
21+
</div>
22+
{% endif %}
23+
{% endwith %}
24+
25+
<form method="POST" action="/reset_password">
26+
<!-- Email input -->
27+
<div class="form-group">
28+
<label for="formEmail">Email address</label>
29+
<input type="email" id="formEmail" name="email" class="form-control" placeholder="Enter email" required>
30+
</div>
31+
32+
33+
<!-- Submit button -->
34+
<button type="submit" class="btn btn-primary btn-block">Request Email Reset Code</button>
35+
</form>
36+
</div>
37+
</div>
38+
</div>
39+
40+
<!-- Bootstrap JS -->
41+
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
42+
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
43+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
44+
</body>
45+
</html>

flaskApp/templates/login.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,20 @@
1414
<h2 class="text-center">Sign In</h2>
1515

1616
<!-- Flash message for invalid credentials -->
17-
{% with messages = get_flashed_messages() %}
17+
{% with messages = get_flashed_messages(with_categories=true) %}
1818
{% if messages %}
19-
<div class="alert alert-danger" role="alert">
20-
{{ messages[0] }}
21-
</div>
19+
{% for category, message in messages %}
20+
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
21+
{{ message }}
22+
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
23+
<span aria-hidden="true">&times;</span>
24+
</button>
25+
</div>
26+
{% endfor %}
2227
{% endif %}
2328
{% endwith %}
2429

30+
2531
<form method="POST" action="/login">
2632
<!-- Email input -->
2733
<div class="form-group">
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Set New Password</title>
7+
<!-- Bootstrap CSS -->
8+
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
9+
</head>
10+
<body>
11+
<div class="container mt-5">
12+
<div class="row justify-content-center">
13+
<div class="col-md-6">
14+
{% with messages = get_flashed_messages(with_categories=true) %}
15+
{% if messages %}
16+
{% for category, message in messages %}
17+
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
18+
{{ message }}
19+
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
20+
<span aria-hidden="true">&times;</span>
21+
</button>
22+
</div>
23+
{% endfor %}
24+
{% endif %}
25+
{% endwith %}
26+
<h2 class="text-center">Set New Password</h2>
27+
<form method="POST" action="{{ url_for('new_password') }}?email={{ email }}">
28+
<input type="hidden" name="email" value="{{ email }}">
29+
<div class="form-group">
30+
<label for="newPassword">New Password</label>
31+
<input type="password" id="newPassword" name="new_password" class="form-control" placeholder="Enter new password" required>
32+
</div>
33+
<div class="form-group">
34+
<label for="confirmPassword">Confirm New Password</label>
35+
<input type="password" id="confirmPassword" name="confirm_password" class="form-control" placeholder="Confirm new password" required>
36+
</div>
37+
<button type="submit" class="btn btn-primary btn-block">Reset Password</button>
38+
</form>
39+
<a href="{{ url_for('enter_code', email=email) }}" class="btn btn-secondary btn-block mt-2">Back</a>
40+
</div>
41+
</div>
42+
</div>
43+
<!-- Bootstrap JS -->
44+
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
45+
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
46+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
47+
</body>
48+
</html>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Reset Password</title>
7+
<!-- Bootstrap CSS -->
8+
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
9+
</head>
10+
<body>
11+
<div class="container mt-5">
12+
<div class="row justify-content-center">
13+
<div class="col-md-6">
14+
{% with messages = get_flashed_messages(with_categories=true) %}
15+
{% if messages %}
16+
{% for category, message in messages %}
17+
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
18+
{{ message }}
19+
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
20+
<span aria-hidden="true">&times;</span>
21+
</button>
22+
</div>
23+
{% endfor %}
24+
{% endif %}
25+
{% endwith %}
26+
<h2 class="text-center">Reset Your Password</h2>
27+
<form method="POST" action="{{ url_for('reset_password') }}">
28+
<div class="form-group">
29+
<label for="formEmail">Email Address</label>
30+
<input type="email" id="formEmail" name="email" class="form-control" placeholder="Enter your email" required>
31+
</div>
32+
<button type="submit" class="btn btn-primary btn-block">Request Password Reset</button>
33+
</form>
34+
</div>
35+
</div>
36+
</div>
37+
<!-- Bootstrap JS -->
38+
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
39+
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
40+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
41+
</body>
42+
</html>

0 commit comments

Comments
 (0)