Add password reset mechanism (Issue #1)
- Added reset_token and reset_token_expiry fields to User model - Implemented generate_reset_token(), verify_reset_token(), and clear_reset_token() methods - Created password reset request form (/password-reset-request) - Created password reset form (/password-reset/<token>) - Added "Forgot password?" link to login page - Reset tokens expire after 1 hour for security - Created migration script to add new database columns - Reset links are logged (would be emailed in production) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
72
app.py
72
app.py
@@ -630,6 +630,78 @@ def login():
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@app.route('/password-reset-request', methods=['GET', 'POST'])
|
||||
def password_reset_request():
|
||||
"""Request a password reset"""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
email = request.form.get('email', '').strip().lower()
|
||||
|
||||
if not email:
|
||||
flash('Please enter your email address', 'error')
|
||||
return render_template('password_reset_request.html')
|
||||
|
||||
# Find user by email
|
||||
user = User.query.filter_by(email=email).first()
|
||||
|
||||
# Always show success message for security (don't reveal if email exists)
|
||||
flash('If an account exists with that email, a password reset link has been sent.', 'success')
|
||||
|
||||
if user and user.password_hash: # Only send reset if user has a password (not OAuth only)
|
||||
# Generate reset token
|
||||
token = user.generate_reset_token()
|
||||
|
||||
# Build reset URL
|
||||
reset_url = url_for('password_reset', token=token, _external=True)
|
||||
|
||||
# Log the reset URL (in production, this would be emailed)
|
||||
logger.info(f"Password reset requested for {email}. Reset URL: {reset_url}")
|
||||
|
||||
# For now, also flash it for development (remove in production)
|
||||
flash(f'Reset link (development only): {reset_url}', 'info')
|
||||
|
||||
return redirect(url_for('login'))
|
||||
|
||||
return render_template('password_reset_request.html')
|
||||
|
||||
|
||||
@app.route('/password-reset/<token>', methods=['GET', 'POST'])
|
||||
def password_reset(token):
|
||||
"""Reset password with token"""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Find user by token
|
||||
user = User.query.filter_by(reset_token=token).first()
|
||||
|
||||
if not user or not user.verify_reset_token(token):
|
||||
flash('Invalid or expired reset token', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
password = request.form.get('password', '')
|
||||
confirm_password = request.form.get('confirm_password', '')
|
||||
|
||||
if not password or len(password) < 6:
|
||||
flash('Password must be at least 6 characters', 'error')
|
||||
return render_template('password_reset.html')
|
||||
|
||||
if password != confirm_password:
|
||||
flash('Passwords do not match', 'error')
|
||||
return render_template('password_reset.html')
|
||||
|
||||
# Set new password
|
||||
user.set_password(password)
|
||||
user.clear_reset_token()
|
||||
|
||||
flash('Your password has been reset successfully. You can now log in.', 'success')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
return render_template('password_reset.html')
|
||||
|
||||
|
||||
# Auth0 Routes
|
||||
@app.route('/auth0/login')
|
||||
def auth0_login():
|
||||
|
||||
Reference in New Issue
Block a user