password reset

This commit is contained in:
2025-06-19 16:11:42 +02:00
parent efdb6d50c3
commit 7092167001
20 changed files with 680 additions and 72 deletions

View File

@@ -1,10 +1,12 @@
from flask import render_template, request, flash, redirect, url_for, Blueprint, jsonify
from flask_login import login_user, logout_user, login_required, current_user
from models import db, User, Notif, PasswordSetupToken
from models import db, User, Notif, PasswordSetupToken, PasswordResetToken
from functools import wraps
from datetime import datetime
from datetime import datetime, timedelta
from utils import log_event, create_notification, get_unread_count
from utils.notification import generate_mail_from_notification
import string
import secrets
auth_bp = Blueprint('auth', __name__)
@@ -306,4 +308,155 @@ def init_routes(auth_bp):
flash('Password set up successfully! Welcome to DocuPulse.', 'success')
return redirect(url_for('main.dashboard'))
return render_template('auth/setup_password.html')
return render_template('auth/setup_password.html')
@auth_bp.route('/forgot-password', methods=['GET', 'POST'])
def forgot_password():
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
if request.method == 'POST':
email = request.form.get('email')
if not email:
flash('Please enter your email address.', 'error')
return render_template('auth/forgot_password.html')
# Check if user exists
user = User.query.filter_by(email=email).first()
if user:
# Generate a secure token
token = secrets.token_urlsafe(32)
# Create password reset token
reset_token = PasswordResetToken(
user_id=user.id,
token=token,
expires_at=datetime.utcnow() + timedelta(hours=1), # 1 hour expiration
ip_address=request.remote_addr
)
db.session.add(reset_token)
# Create notification for password reset
notif = create_notification(
notif_type='password_reset',
user_id=user.id,
details={
'message': 'You requested a password reset. Click the link below to reset your password.',
'reset_link': url_for('auth.reset_password', token=token, _external=True),
'expiry_time': (datetime.utcnow() + timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S UTC'),
'ip_address': request.remote_addr,
'timestamp': datetime.utcnow().isoformat()
},
generate_mail=False # Don't auto-generate email, we'll do it manually
)
# Generate and send email manually
if notif:
generate_mail_from_notification(notif)
# Log the password reset request
log_event(
event_type='user_update',
details={
'user_id': user.id,
'user_name': f"{user.username} {user.last_name}",
'email': user.email,
'update_type': 'password_reset_request',
'ip_address': request.remote_addr,
'success': True
}
)
db.session.commit()
# Always show success message to prevent email enumeration
flash('If an account with that email exists, a password reset link has been sent to your email address.', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/forgot_password.html')
@auth_bp.route('/reset-password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
# Find the token
reset_token = PasswordResetToken.query.filter_by(token=token).first()
if not reset_token or not reset_token.is_valid():
flash('Invalid or expired password reset link. Please request a new password reset.', 'error')
return redirect(url_for('auth.forgot_password'))
if request.method == 'POST':
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
if not password or not confirm_password:
flash('Please fill in all fields.', 'error')
return render_template('auth/reset_password.html', token=token)
if password != confirm_password:
flash('Passwords do not match.', 'error')
return render_template('auth/reset_password.html', token=token)
# Password requirements
if len(password) < 8:
flash('Password must be at least 8 characters long.', 'error')
return render_template('auth/reset_password.html', token=token)
if not any(c.isupper() for c in password):
flash('Password must contain at least one uppercase letter.', 'error')
return render_template('auth/reset_password.html', token=token)
if not any(c.islower() for c in password):
flash('Password must contain at least one lowercase letter.', 'error')
return render_template('auth/reset_password.html', token=token)
if not any(c.isdigit() for c in password):
flash('Password must contain at least one number.', 'error')
return render_template('auth/reset_password.html', token=token)
if not any(c in string.punctuation for c in password):
flash('Password must contain at least one special character.', 'error')
return render_template('auth/reset_password.html', token=token)
# Update user's password
user = reset_token.user
user.set_password(password)
# Mark token as used
reset_token.used = True
# Create password change notification
create_notification(
notif_type='password_changed',
user_id=user.id,
details={
'message': 'Your password has been reset successfully.',
'timestamp': datetime.utcnow().isoformat()
}
)
# Log password reset event
log_event(
event_type='user_update',
details={
'user_id': user.id,
'user_name': f"{user.username} {user.last_name}",
'email': user.email,
'update_type': 'password_reset',
'ip_address': request.remote_addr,
'success': True
}
)
db.session.commit()
# Log the user in and redirect to dashboard
login_user(user)
flash('Password reset successfully! Welcome back to DocuPulse.', 'success')
return redirect(url_for('main.dashboard'))
return render_template('auth/reset_password.html', token=token)