password reset
This commit is contained in:
Binary file not shown.
Binary file not shown.
159
routes/auth.py
159
routes/auth.py
@@ -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)
|
||||
@@ -1420,36 +1420,21 @@ def init_routes(main_bp):
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
event = Event.query.get_or_404(event_id)
|
||||
logger.info(f"Raw event object: {event}")
|
||||
logger.info(f"Event details type: {type(event.details)}")
|
||||
logger.info(f"Event details value: {event.details}")
|
||||
|
||||
# Convert details to dict if it's a string
|
||||
details = event.details
|
||||
if isinstance(details, str):
|
||||
try:
|
||||
import json
|
||||
details = json.loads(details)
|
||||
except json.JSONDecodeError:
|
||||
details = {'raw_details': details}
|
||||
|
||||
# Return the raw event data
|
||||
response_data = {
|
||||
return jsonify({
|
||||
'id': event.id,
|
||||
'event_type': event.event_type,
|
||||
'timestamp': event.timestamp.isoformat(),
|
||||
'details': event.details,
|
||||
'ip_address': event.ip_address,
|
||||
'user_agent': event.user_agent,
|
||||
'user': {
|
||||
'id': event.user.id,
|
||||
'username': event.user.username,
|
||||
'last_name': event.user.last_name
|
||||
} if event.user else None,
|
||||
'ip_address': event.ip_address,
|
||||
'user_agent': event.user_agent,
|
||||
'details': details
|
||||
}
|
||||
|
||||
logger.info(f"Sending response: {response_data}")
|
||||
return jsonify(response_data)
|
||||
'last_name': event.user.last_name,
|
||||
'email': event.user.email
|
||||
} if event.user else None
|
||||
})
|
||||
|
||||
@main_bp.route('/settings/events/download')
|
||||
@login_required
|
||||
@@ -1722,4 +1707,26 @@ def init_routes(main_bp):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'results': results
|
||||
})
|
||||
|
||||
@main_bp.route('/api/mails/<int:mail_id>')
|
||||
@login_required
|
||||
def get_mail_details(mail_id):
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
mail = Mail.query.get_or_404(mail_id)
|
||||
|
||||
return jsonify({
|
||||
'id': mail.id,
|
||||
'recipient': mail.recipient,
|
||||
'subject': mail.subject,
|
||||
'body': mail.body,
|
||||
'status': mail.status,
|
||||
'created_at': mail.created_at.isoformat(),
|
||||
'sent_at': mail.sent_at.isoformat() if mail.sent_at else None,
|
||||
'template': {
|
||||
'id': mail.template.id,
|
||||
'name': mail.template.name
|
||||
} if mail.template else None
|
||||
})
|
||||
Reference in New Issue
Block a user