better password management
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,10 @@
|
||||
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
|
||||
from models import db, User, Notif, PasswordSetupToken
|
||||
from functools import wraps
|
||||
from datetime import datetime
|
||||
from utils import log_event, create_notification, get_unread_count
|
||||
import string
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
@@ -215,4 +216,81 @@ def init_routes(auth_bp):
|
||||
flash('Password changed successfully!', 'success')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
return render_template('auth/change_password.html')
|
||||
return render_template('auth/change_password.html')
|
||||
|
||||
@auth_bp.route('/setup-password/<token>', methods=['GET', 'POST'])
|
||||
def setup_password(token):
|
||||
# Find the token
|
||||
setup_token = PasswordSetupToken.query.filter_by(token=token).first()
|
||||
|
||||
if not setup_token or not setup_token.is_valid():
|
||||
flash('Invalid or expired password setup link. Please contact your administrator for a new link.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
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/setup_password.html')
|
||||
|
||||
if password != confirm_password:
|
||||
flash('Passwords do not match.', 'error')
|
||||
return render_template('auth/setup_password.html')
|
||||
|
||||
# Password requirements
|
||||
if len(password) < 8:
|
||||
flash('Password must be at least 8 characters long.', 'error')
|
||||
return render_template('auth/setup_password.html')
|
||||
|
||||
if not any(c.isupper() for c in password):
|
||||
flash('Password must contain at least one uppercase letter.', 'error')
|
||||
return render_template('auth/setup_password.html')
|
||||
|
||||
if not any(c.islower() for c in password):
|
||||
flash('Password must contain at least one lowercase letter.', 'error')
|
||||
return render_template('auth/setup_password.html')
|
||||
|
||||
if not any(c.isdigit() for c in password):
|
||||
flash('Password must contain at least one number.', 'error')
|
||||
return render_template('auth/setup_password.html')
|
||||
|
||||
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/setup_password.html')
|
||||
|
||||
# Update user's password
|
||||
user = setup_token.user
|
||||
user.set_password(password)
|
||||
|
||||
# Mark token as used
|
||||
setup_token.used = True
|
||||
|
||||
# Create password change notification
|
||||
create_notification(
|
||||
notif_type='password_changed',
|
||||
user_id=user.id,
|
||||
details={
|
||||
'message': 'Your password has been set up successfully.',
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
# Log password setup event
|
||||
log_event(
|
||||
event_type='user_update',
|
||||
details={
|
||||
'user_id': user.id,
|
||||
'user_name': f"{user.username} {user.last_name}",
|
||||
'update_type': 'password_setup',
|
||||
'success': True
|
||||
}
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flash('Password set up successfully! You can now log in.', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return render_template('auth/setup_password.html')
|
||||
@@ -1,6 +1,6 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from models import db, User, Notif
|
||||
from models import db, User, Notif, PasswordSetupToken
|
||||
from forms import UserForm
|
||||
from flask import abort
|
||||
from sqlalchemy import or_
|
||||
@@ -9,7 +9,9 @@ from utils import log_event, create_notification, get_unread_count
|
||||
import json
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
import string
|
||||
|
||||
contacts_bp = Blueprint('contacts', __name__, url_prefix='/contacts')
|
||||
|
||||
@@ -113,6 +115,10 @@ def new_contact():
|
||||
file.save(file_path)
|
||||
profile_picture = filename
|
||||
|
||||
# Generate a random password
|
||||
alphabet = string.ascii_letters + string.digits + string.punctuation
|
||||
random_password = ''.join(secrets.choice(alphabet) for _ in range(32))
|
||||
|
||||
# Create new user account
|
||||
user = User(
|
||||
username=form.first_name.data,
|
||||
@@ -126,10 +132,20 @@ def new_contact():
|
||||
is_admin=form.is_admin.data,
|
||||
profile_picture=profile_picture
|
||||
)
|
||||
user.set_password('changeme') # Set default password
|
||||
user.set_password(random_password) # Set random password
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
# Create password setup token
|
||||
token = secrets.token_urlsafe(32)
|
||||
setup_token = PasswordSetupToken(
|
||||
user_id=user.id,
|
||||
token=token,
|
||||
expires_at=datetime.utcnow() + timedelta(hours=24)
|
||||
)
|
||||
db.session.add(setup_token)
|
||||
db.session.commit()
|
||||
|
||||
# Create notification for the new user
|
||||
create_notification(
|
||||
notif_type='account_created',
|
||||
@@ -140,7 +156,8 @@ def new_contact():
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'created_by': f"{current_user.username} {current_user.last_name}",
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
'timestamp': datetime.utcnow().isoformat(),
|
||||
'setup_link': url_for('auth.setup_password', token=token, _external=True)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -159,7 +176,7 @@ def new_contact():
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
flash('User created successfully! They will need to change their password on first login.', 'success')
|
||||
flash('User created successfully! They will receive an email with a link to set up their password.', 'success')
|
||||
return redirect(url_for('contacts.contacts_list'))
|
||||
return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins)
|
||||
|
||||
@@ -446,4 +463,54 @@ def toggle_active(id):
|
||||
db.session.commit()
|
||||
|
||||
flash(f'User marked as {"active" if user.is_active else "inactive"}!', 'success')
|
||||
return redirect(url_for('contacts.contacts_list'))
|
||||
|
||||
@contacts_bp.route('/<int:id>/resend-setup', methods=['POST'])
|
||||
@login_required
|
||||
@require_password_change
|
||||
def resend_setup_link(id):
|
||||
result = admin_required()
|
||||
if result: return result
|
||||
|
||||
user = User.query.get_or_404(id)
|
||||
|
||||
# Create new password setup token
|
||||
token = secrets.token_urlsafe(32)
|
||||
setup_token = PasswordSetupToken(
|
||||
user_id=user.id,
|
||||
token=token,
|
||||
expires_at=datetime.utcnow() + timedelta(hours=24)
|
||||
)
|
||||
db.session.add(setup_token)
|
||||
|
||||
# Create notification for the user
|
||||
create_notification(
|
||||
notif_type='account_created',
|
||||
user_id=user.id,
|
||||
sender_id=current_user.id,
|
||||
details={
|
||||
'message': 'A new password setup link has been sent to you.',
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'created_by': f"{current_user.username} {current_user.last_name}",
|
||||
'timestamp': datetime.utcnow().isoformat(),
|
||||
'setup_link': url_for('auth.setup_password', token=token, _external=True)
|
||||
}
|
||||
)
|
||||
|
||||
# Log the event
|
||||
log_event(
|
||||
event_type='user_update',
|
||||
details={
|
||||
'user_id': user.id,
|
||||
'user_name': f"{user.username} {user.last_name}",
|
||||
'updated_by': current_user.id,
|
||||
'updated_by_name': f"{current_user.username} {current_user.last_name}",
|
||||
'update_type': 'password_setup_link_resend'
|
||||
}
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flash('Password setup link has been resent to the user.', 'success')
|
||||
return redirect(url_for('contacts.contacts_list'))
|
||||
Reference in New Issue
Block a user