diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index c91e76d..c44381b 100644 Binary files a/routes/__pycache__/auth.cpython-313.pyc and b/routes/__pycache__/auth.cpython-313.pyc differ diff --git a/routes/__pycache__/contacts.cpython-313.pyc b/routes/__pycache__/contacts.cpython-313.pyc index 0ce2c56..07b2291 100644 Binary files a/routes/__pycache__/contacts.cpython-313.pyc and b/routes/__pycache__/contacts.cpython-313.pyc differ diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc index 7ea0e73..35f2872 100644 Binary files a/routes/__pycache__/conversations.cpython-313.pyc and b/routes/__pycache__/conversations.cpython-313.pyc differ diff --git a/routes/auth.py b/routes/auth.py index 23dc561..37170ed 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -2,8 +2,8 @@ from flask import render_template, request, flash, redirect, url_for from flask_login import login_user, logout_user, login_required, current_user from models import db, User from functools import wraps -from utils import log_event from datetime import datetime +from utils import log_event def require_password_change(f): @wraps(f) @@ -29,24 +29,32 @@ def init_routes(auth_bp): if not user or not user.check_password(password): # Log failed login attempt - log_event('user_login', { - 'email': email, - 'success': False, - 'reason': 'invalid_credentials' - }) + log_event( + event_type='user_login', + details={ + 'email': email, + 'success': False, + 'reason': 'invalid_credentials' + } + ) + db.session.commit() flash('Please check your login details and try again.', 'danger') return redirect(url_for('auth.login')) login_user(user, remember=remember) # Log successful login - log_event('user_login', { - 'user_id': user.id, - 'email': email, - 'success': True, - 'remember': remember, - 'using_default_password': password == 'changeme' - }, user.id) + log_event( + event_type='user_login', + details={ + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'email': user.email, + 'success': True, + 'remember': remember + } + ) + db.session.commit() # Check if user is using default password if password == 'changeme': @@ -86,15 +94,17 @@ def init_routes(auth_bp): db.session.add(new_user) db.session.commit() - # Log user registration - log_event('user_register', { - 'email': email, - 'username': username, - 'timestamp': datetime.utcnow().isoformat(), - 'ip_address': request.remote_addr, - 'user_agent': request.user_agent.string, - 'registration_method': 'web_form' - }, new_user.id) + # Log successful registration + log_event( + event_type='user_create', + details={ + 'user_id': new_user.id, + 'user_name': f"{new_user.username} {new_user.last_name}", + 'email': new_user.email, + 'method': 'web_form' + } + ) + db.session.commit() login_user(new_user) return redirect(url_for('main.dashboard')) @@ -104,12 +114,16 @@ def init_routes(auth_bp): @auth_bp.route('/logout') @login_required def logout(): - # Log logout event - log_event('user_logout', { - 'user_id': current_user.id, - 'email': current_user.email - }, current_user.id) - + # Log logout event before logging out + log_event( + event_type='user_logout', + details={ + 'user_id': current_user.id, + 'user_name': f"{current_user.username} {current_user.last_name}", + 'email': current_user.email + } + ) + db.session.commit() logout_user() return redirect(url_for('auth.login')) @@ -122,22 +136,50 @@ def init_routes(auth_bp): confirm_password = request.form.get('confirm_password') if not current_user.check_password(current_password): + # Log failed password change attempt + log_event( + event_type='user_update', + details={ + 'user_id': current_user.id, + 'user_name': f"{current_user.username} {current_user.last_name}", + 'update_type': 'password_change', + 'success': False, + 'reason': 'invalid_current_password' + } + ) + db.session.commit() flash('Current password is incorrect.', 'danger') return redirect(url_for('auth.change_password')) if new_password != confirm_password: + # Log failed password change attempt + log_event( + event_type='user_update', + details={ + 'user_id': current_user.id, + 'user_name': f"{current_user.username} {current_user.last_name}", + 'update_type': 'password_change', + 'success': False, + 'reason': 'passwords_dont_match' + } + ) + db.session.commit() flash('New passwords do not match.', 'danger') return redirect(url_for('auth.change_password')) current_user.set_password(new_password) - db.session.commit() - # Log password change - log_event('user_update', { - 'user_id': current_user.id, - 'email': current_user.email, - 'update_type': 'password_change' - }, current_user.id) + # Log successful password change + log_event( + event_type='user_update', + details={ + 'user_id': current_user.id, + 'user_name': f"{current_user.username} {current_user.last_name}", + 'update_type': 'password_change', + 'success': True + } + ) + db.session.commit() flash('Password changed successfully!', 'success') return redirect(url_for('main.dashboard')) diff --git a/routes/contacts.py b/routes/contacts.py index d48f176..3b18bcc 100644 --- a/routes/contacts.py +++ b/routes/contacts.py @@ -5,6 +5,7 @@ from forms import UserForm from flask import abort from sqlalchemy import or_ from routes.auth import require_password_change +from utils import log_event import json import os from werkzeug.utils import secure_filename @@ -120,6 +121,22 @@ def new_contact(): user.set_password('changeme') # Set default password db.session.add(user) db.session.commit() + + # Log user creation event + log_event( + event_type='user_create', + details={ + 'created_by': current_user.id, + 'created_by_name': f"{current_user.username} {current_user.last_name}", + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'email': user.email, + 'is_admin': user.is_admin, + 'method': 'admin_creation' + } + ) + db.session.commit() + flash('User created successfully! They will need to change their password on first login.', 'success') return redirect(url_for('contacts.contacts_list')) return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins) @@ -138,6 +155,16 @@ def edit_profile(): flash('There must be at least one admin user in the system.', 'error') return render_template('contacts/form.html', form=form, title='Edit Profile', total_admins=total_admins) + # Store old values for comparison + old_values = { + 'user_name': f"{current_user.username} {current_user.last_name}", + 'email': current_user.email, + 'phone': current_user.phone, + 'company': current_user.company, + 'position': current_user.position, + 'is_admin': current_user.is_admin + } + current_user.username = form.first_name.data current_user.last_name = form.last_name.data current_user.email = form.email.data @@ -146,10 +173,39 @@ def edit_profile(): current_user.position = form.position.data current_user.notes = form.notes.data current_user.is_admin = form.is_admin.data + # Set password if provided + password_changed = False if form.new_password.data: current_user.set_password(form.new_password.data) + password_changed = True + db.session.commit() + + # Log profile update event + log_event( + event_type='user_update', + details={ + 'user_id': current_user.id, + 'user_name': f"{current_user.username} {current_user.last_name}", + 'updated_by': current_user.id, + 'updated_by_name': f"{current_user.username} {current_user.last_name}", + 'old_values': old_values, + 'new_values': { + 'username': current_user.username, + 'last_name': current_user.last_name, + 'email': current_user.email, + 'phone': current_user.phone, + 'company': current_user.company, + 'position': current_user.position, + 'is_admin': current_user.is_admin + }, + 'password_changed': password_changed, + 'method': 'self_update' + } + ) + db.session.commit() + flash('Profile updated successfully!', 'success') return redirect(url_for('contacts.contacts_list')) @@ -220,6 +276,17 @@ def edit_contact(id): if existing_user: flash('A user with this email already exists.', 'error') return render_template('contacts/form.html', form=form, title='Edit User', total_admins=total_admins, user=user) + + # Store old values for comparison + old_values = { + 'user_name': f"{user.username} {user.last_name}", + 'email': user.email, + 'phone': user.phone, + 'company': user.company, + 'position': user.position, + 'is_admin': user.is_admin + } + user.username = form.first_name.data user.last_name = form.last_name.data user.email = form.email.data @@ -228,10 +295,38 @@ def edit_contact(id): user.position = form.position.data user.notes = form.notes.data user.is_admin = form.is_admin.data + # Set password if provided + password_changed = False if form.new_password.data: user.set_password(form.new_password.data) + password_changed = True + db.session.commit() + + # Log user update 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}", + 'old_values': old_values, + 'new_values': { + 'user_name': f"{user.username} {user.last_name}", + 'email': user.email, + 'phone': user.phone, + 'company': user.company, + 'position': user.position, + 'is_admin': user.is_admin + }, + 'password_changed': password_changed, + 'method': 'admin_update' + } + ) + db.session.commit() + flash('User updated successfully!', 'success') return redirect(url_for('contacts.contacts_list')) return render_template('contacts/form.html', form=form, title='Edit User', total_admins=total_admins, user=user) @@ -246,6 +341,21 @@ def delete_contact(id): if user.email == current_user.email: flash('You cannot delete your own account.', 'error') return redirect(url_for('contacts.contacts_list')) + + # Log user deletion event + log_event( + event_type='user_delete', + details={ + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'deleted_by': current_user.id, + 'deleted_by_name': f"{current_user.username} {current_user.last_name}", + 'email': user.email, + 'is_admin': user.is_admin + } + ) + db.session.commit() + db.session.delete(user) db.session.commit() flash('User deleted successfully!', 'success') @@ -261,7 +371,25 @@ def toggle_active(id): if user.email == current_user.email: flash('You cannot deactivate your own account.', 'error') return redirect(url_for('contacts.contacts_list')) + + old_status = user.is_active user.is_active = not user.is_active + + # Log status change 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': 'status_change', + 'old_status': old_status, + 'new_status': user.is_active, + 'email': user.email + } + ) db.session.commit() + flash(f'User marked as {"active" if user.is_active else "inactive"}!', 'success') return redirect(url_for('contacts.contacts_list')) \ No newline at end of file diff --git a/routes/conversations.py b/routes/conversations.py index 3bcdbe9..523a9b7 100644 --- a/routes/conversations.py +++ b/routes/conversations.py @@ -3,6 +3,7 @@ from flask_login import login_required, current_user from models import db, Conversation, User, Message, MessageAttachment from forms import ConversationForm from routes.auth import require_password_change +from utils import log_event import os from werkzeug.utils import secure_filename from datetime import datetime @@ -83,6 +84,21 @@ def create_conversation(): db.session.add(conversation) db.session.commit() + + # Log conversation creation + log_event( + event_type='conversation_create', + details={ + 'conversation_id': conversation.id, + 'created_by': current_user.id, + 'created_by_name': f"{current_user.username} {current_user.last_name}", + 'name': conversation.name, + 'description': conversation.description, + 'member_count': len(conversation.members), + 'member_ids': [member.id for member in conversation.members] + } + ) + db.session.commit() flash('Conversation created successfully!', 'success') return redirect(url_for('conversations.conversations')) @@ -147,6 +163,22 @@ def add_member(conversation_id): else: conversation.members.append(user) db.session.commit() + + # Log member addition + log_event( + event_type='conversation_member_add', + details={ + 'conversation_id': conversation.id, + 'conversation_name': conversation.name, + 'added_by': current_user.id, + 'added_by_name': f"{current_user.username} {current_user.last_name}", + 'added_user_id': user.id, + 'added_by_name': f"{current_user.username} {current_user.last_name}", + 'added_user_email': user.email + } + ) + db.session.commit() + flash(f'{user.username} has been added to the conversation.', 'success') return redirect(url_for('conversations.conversation_members', conversation_id=conversation_id)) @@ -169,6 +201,22 @@ def remove_member(conversation_id, user_id): else: conversation.members.remove(user) db.session.commit() + + # Log member removal + log_event( + event_type='conversation_member_remove', + details={ + 'conversation_id': conversation.id, + 'conversation_name': conversation.name, + 'removed_by': current_user.id, + 'removed_by_name': f"{current_user.username} {current_user.last_name}", + 'removed_user_id': user.id, + 'removed_user_name': f"{user.username} {user.last_name}", + 'removed_user_email': user.email + } + ) + db.session.commit() + flash('User has been removed from the conversation.', 'success') return redirect(url_for('conversations.conversation_members', conversation_id=conversation_id)) @@ -184,6 +232,13 @@ def edit_conversation(conversation_id): form = ConversationForm(obj=conversation) if request.method == 'POST': + # Store old values for comparison + old_values = { + 'name': conversation.name, + 'description': conversation.description, + 'member_ids': [member.id for member in conversation.members] + } + # Get members from the form data member_ids = request.form.getlist('members') @@ -205,6 +260,25 @@ def edit_conversation(conversation_id): conversation.members.append(user) db.session.commit() + + # Log conversation update + log_event( + event_type='conversation_update', + details={ + 'conversation_id': conversation.id, + 'updated_by': current_user.id, + 'updated_by_name': f"{current_user.username} {current_user.last_name}", + 'old_values': old_values, + 'new_values': { + 'name': conversation.name, + 'description': conversation.description, + 'member_ids': [member.id for member in conversation.members], + 'member_names': [f"{member.username} {member.last_name}" for member in conversation.members] + } + } + ) + db.session.commit() + flash('Conversation members updated successfully!', 'success') # Check if redirect parameter is provided @@ -227,6 +301,20 @@ def delete_conversation(conversation_id): conversation = Conversation.query.get_or_404(conversation_id) + # Log conversation deletion + log_event( + event_type='conversation_delete', + details={ + 'conversation_id': conversation.id, + 'conversation_name': conversation.name, + 'deleted_by': current_user.id, + 'deleted_by_name': f"{current_user.username} {current_user.last_name}", + 'member_count': len(conversation.members), + 'message_count': Message.query.filter_by(conversation_id=conversation_id).count() + } + ) + db.session.commit() + # Delete all messages in the conversation Message.query.filter_by(conversation_id=conversation_id).delete() @@ -264,7 +352,6 @@ def get_messages(conversation_id): 'created_at': message.created_at.strftime('%b %d, %Y %H:%M'), 'sender_id': str(message.user_id), 'sender_name': f"{message.user.username} {message.user.last_name}", - 'sender_avatar': url_for('profile_pic', filename=message.user.profile_picture) if message.user.profile_picture else url_for('static', filename='default-avatar.png'), 'attachments': [{ 'name': attachment.name, 'size': attachment.size, @@ -323,6 +410,22 @@ def send_message(conversation_id): attachments.append(attachment) db.session.commit() + + # Log message creation + log_event( + event_type='message_create', + details={ + 'message_id': message.id, + 'conversation_id': conversation_id, + 'conversation_name': conversation.name, + 'sender_id': current_user.id, + 'sender_name': f"{current_user.username} {current_user.last_name}", + 'has_attachments': len(attachments) > 0, + 'attachment_count': len(attachments), + 'attachment_types': [get_file_extension(att.name) for att in attachments] if attachments else [] + } + ) + db.session.commit() # Prepare message data for response message_data = {