better password management

This commit is contained in:
2025-06-04 13:44:49 +02:00
parent 88c3bc1b5b
commit 41cdd5ec7f
24 changed files with 246 additions and 10 deletions

View File

@@ -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')

View File

@@ -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'))