from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session from flask_login import current_user, login_required from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail from routes.auth import require_password_change import os from werkzeug.utils import secure_filename from sqlalchemy import func, case, literal_column, text from datetime import datetime, timedelta import logging import sys import time from forms import CompanySettingsForm from utils import log_event, create_notification, get_unread_count from io import StringIO import csv # Set up logging to show in console logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(sys.stdout) ] ) logger = logging.getLogger(__name__) def init_routes(main_bp): @main_bp.context_processor def inject_site_settings(): site_settings = SiteSettings.query.first() return dict(site_settings=site_settings) @main_bp.context_processor def inject_unread_notifications(): if current_user.is_authenticated: unread_count = get_unread_count(current_user.id) return {'unread_notifications': unread_count} return {'unread_notifications': 0} @main_bp.route('/') @login_required @require_password_change def home(): return redirect(url_for('main.dashboard')) @main_bp.route('/dashboard') @login_required @require_password_change def dashboard(): logger.info("Loading dashboard...") # Get 3 most recent users recent_contacts = User.query.order_by(User.created_at.desc()).limit(3).all() # Count active and inactive users active_count = User.query.filter_by(is_active=True).count() inactive_count = User.query.filter_by(is_active=False).count() # Get recent events (last 7) if current_user.is_admin: recent_events = Event.query.order_by(Event.timestamp.desc()).limit(7).all() else: # Get events where user is the actor OR events from conversations they are a member of recent_events = Event.query.filter( db.or_( Event.user_id == current_user.id, # User's own actions db.and_( Event.event_type.in_(['conversation_create', 'message_create']), # Conversation-related events Event.details['conversation_id'].cast(db.Integer).in_( db.session.query(Conversation.id) .join(Conversation.members) .filter(User.id == current_user.id) ) ) ) ).order_by(Event.timestamp.desc()).limit(7).all() # Room count and size logic if current_user.is_admin: logger.info("Loading admin dashboard...") room_count = Room.query.count() # Get total file and folder counts for admin file_count = RoomFile.query.filter_by(type='file').count() folder_count = RoomFile.query.filter_by(type='folder').count() logger.info(f"Admin stats - Files: {file_count}, Folders: {folder_count}") # Get total size of all files including trash total_size = db.session.query(func.sum(RoomFile.size)).filter(RoomFile.type == 'file').scalar() or 0 # Get recent activity for all files recent_activity = db.session.query( RoomFile, Room, User ).join( Room, RoomFile.room_id == Room.id ).join( User, RoomFile.uploaded_by == User.id ).filter( RoomFile.deleted == False, RoomFile.uploaded_at.isnot(None) ).order_by( RoomFile.uploaded_at.desc() ).limit(10).all() logger.info(f"Recent activity query results: {len(recent_activity)}") if len(recent_activity) == 0: # Debug query to see what files exist all_files = RoomFile.query.filter_by(deleted=False).all() logger.info(f"Total non-deleted files: {len(all_files)}") for file in all_files[:5]: # Log first 5 files for debugging logger.info(f"File: {file.name}, Uploaded: {file.uploaded_at}, Type: {file.type}") # Format the activity data formatted_activity = [] for file, room, user in recent_activity: activity = { 'name': file.name, 'type': file.type, 'room': room, 'uploader': user, 'uploaded_at': file.uploaded_at, 'is_starred': current_user in file.starred_by, 'is_deleted': file.deleted, 'can_download': True # Admin can download everything } formatted_activity.append(activity) formatted_activities = formatted_activity logger.info(f"Formatted activities: {len(formatted_activities)}") # Get storage usage by file type including trash storage_by_type = db.session.query( case( (RoomFile.name.like('%.%'), func.substring(RoomFile.name, func.strpos(RoomFile.name, '.') + 1)), else_=literal_column("'unknown'") ).label('extension'), func.count(RoomFile.id).label('count'), func.sum(RoomFile.size).label('total_size') ).filter( RoomFile.type == 'file' ).group_by('extension').all() # Get trash and starred stats for admin trash_count = RoomFile.query.filter_by(deleted=True).count() starred_count = RoomFile.query.filter(RoomFile.starred_by.contains(current_user)).count() # Get oldest trash date and total trash size oldest_trash = RoomFile.query.filter_by(deleted=True).order_by(RoomFile.deleted_at.asc()).first() oldest_trash_date = oldest_trash.deleted_at.strftime('%Y-%m-%d') if oldest_trash and oldest_trash.deleted_at else None trash_size = db.session.query(db.func.sum(RoomFile.size)).filter(RoomFile.deleted==True).scalar() or 0 # Get files that will be deleted in next 7 days seven_days_from_now = datetime.utcnow() + timedelta(days=7) thirty_days_ago = datetime.utcnow() - timedelta(days=30) pending_deletion = RoomFile.query.filter( RoomFile.deleted==True, RoomFile.deleted_at <= thirty_days_ago, RoomFile.deleted_at > thirty_days_ago - timedelta(days=7) ).count() # Get trash file type breakdown trash_by_type = db.session.query( case( (RoomFile.name.like('%.%'), func.substring(RoomFile.name, func.strpos(RoomFile.name, '.') + 1)), else_=literal_column("'unknown'") ).label('extension'), func.count(RoomFile.id).label('count') ).filter( RoomFile.deleted==True ).group_by('extension').all() else: # Get rooms the user has access to accessible_rooms = Room.query.filter(Room.members.any(id=current_user.id)).all() room_count = len(accessible_rooms) # Get file and folder counts for accessible rooms room_ids = [room.id for room in accessible_rooms] file_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.type == 'file').count() folder_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.type == 'folder').count() # Get total size of files in accessible rooms including trash total_size = db.session.query(func.sum(RoomFile.size)).filter( RoomFile.room_id.in_(room_ids), RoomFile.type == 'file' ).scalar() or 0 # Get recent activity for accessible rooms recent_activity = db.session.query( RoomFile, Room, User ).join( Room, RoomFile.room_id == Room.id ).join( User, RoomFile.uploaded_by == User.id ).filter( RoomFile.room_id.in_(room_ids), RoomFile.deleted == False, RoomFile.uploaded_at.isnot(None) # Ensure uploaded_at is not null ).order_by( RoomFile.uploaded_at.desc() ).limit(10).all() logger.info(f"Recent activity query results (non-admin): {len(recent_activity)}") if len(recent_activity) == 0: # Debug query to see what files exist all_files = RoomFile.query.filter( RoomFile.room_id.in_(room_ids), RoomFile.deleted == False ).all() logger.info(f"Total non-deleted files in accessible rooms: {len(all_files)}") for file in all_files[:5]: # Log first 5 files for debugging logger.info(f"File: {file.name}, Uploaded: {file.uploaded_at}, Type: {file.type}") # Format the activity data formatted_activity = [] user_perms = {p.room_id: p for p in RoomMemberPermission.query.filter( RoomMemberPermission.room_id.in_(room_ids), RoomMemberPermission.user_id==current_user.id ).all()} for file, room, user in recent_activity: perm = user_perms.get(room.id) activity = { 'name': file.name, 'type': file.type, 'room': room, 'uploader': user, 'uploaded_at': file.uploaded_at, 'is_starred': current_user in file.starred_by, 'is_deleted': file.deleted, 'can_download': perm.can_download if perm else False } formatted_activity.append(activity) formatted_activities = formatted_activity # Get storage usage by file type for accessible rooms including trash storage_by_type = db.session.query( case( (RoomFile.name.like('%.%'), func.substring(RoomFile.name, func.strpos(RoomFile.name, '.') + 1)), else_=literal_column("'unknown'") ).label('extension'), func.count(RoomFile.id).label('count'), func.sum(RoomFile.size).label('total_size') ).filter( RoomFile.room_id.in_(room_ids), RoomFile.type == 'file' ).group_by('extension').all() # Get trash and starred stats for user's accessible rooms trash_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).count() starred_count = RoomFile.query.filter( RoomFile.room_id.in_(room_ids), RoomFile.starred_by.contains(current_user) ).count() # Get oldest trash date and total trash size for user's rooms oldest_trash = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).order_by(RoomFile.deleted_at.asc()).first() oldest_trash_date = oldest_trash.deleted_at.strftime('%Y-%m-%d') if oldest_trash and oldest_trash.deleted_at else None trash_size = db.session.query(db.func.sum(RoomFile.size)).filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).scalar() or 0 # Get files that will be deleted in next 7 days for user's rooms seven_days_from_now = datetime.utcnow() + timedelta(days=7) thirty_days_ago = datetime.utcnow() - timedelta(days=30) pending_deletion = RoomFile.query.filter( RoomFile.room_id.in_(room_ids), RoomFile.deleted==True, RoomFile.deleted_at <= thirty_days_ago, RoomFile.deleted_at > thirty_days_ago - timedelta(days=7) ).count() # Get trash file type breakdown for user's rooms trash_by_type = db.session.query( case( (RoomFile.name.like('%.%'), func.substring(RoomFile.name, func.strpos(RoomFile.name, '.') + 1)), else_=literal_column("'unknown'") ).label('extension'), func.count(RoomFile.id).label('count') ).filter( RoomFile.room_id.in_(room_ids), RoomFile.deleted==True ).group_by('extension').all() # Get conversation statistics if current_user.is_admin: conversation_count = Conversation.query.count() message_count = Message.query.count() attachment_count = MessageAttachment.query.count() conversation_total_size = db.session.query(db.func.sum(MessageAttachment.size)).scalar() or 0 recent_conversations = Conversation.query.order_by(Conversation.created_at.desc()).limit(5).all() else: # For regular users, only show their conversations conversation_count = Conversation.query.filter(Conversation.members.any(id=current_user.id)).count() message_count = Message.query.join(Conversation).filter(Conversation.members.any(id=current_user.id)).count() attachment_count = MessageAttachment.query.join(Message).join(Conversation).filter(Conversation.members.any(id=current_user.id)).count() conversation_total_size = db.session.query(db.func.sum(MessageAttachment.size)).join(Message).join(Conversation).filter(Conversation.members.any(id=current_user.id)).scalar() or 0 recent_conversations = Conversation.query.filter(Conversation.members.any(id=current_user.id)).order_by(Conversation.created_at.desc()).limit(5).all() return render_template('dashboard/dashboard.html', recent_contacts=recent_contacts, active_count=active_count, inactive_count=inactive_count, room_count=room_count, file_count=file_count, folder_count=folder_count, total_size=total_size, # Room storage size storage_by_type=storage_by_type, trash_count=trash_count, starred_count=starred_count, oldest_trash_date=oldest_trash_date, trash_size=trash_size, pending_deletion=pending_deletion, trash_by_type=trash_by_type, recent_events=recent_events, is_admin=current_user.is_admin, conversation_count=conversation_count, message_count=message_count, attachment_count=attachment_count, conversation_total_size=conversation_total_size, # Conversation storage size recent_conversations=recent_conversations) UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads', 'profile_pics') if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) @main_bp.route('/profile', methods=['GET', 'POST']) @login_required @require_password_change def profile(): if request.method == 'POST': logger.debug(f"Profile form submitted with data: {request.form}") logger.debug(f"Files in request: {request.files}") try: # Handle profile picture removal if 'remove_picture' in request.form: logger.debug("Removing profile picture") if current_user.profile_picture: # Delete the old profile picture file old_picture_path = os.path.join(UPLOAD_FOLDER, current_user.profile_picture) if os.path.exists(old_picture_path): os.remove(old_picture_path) current_user.profile_picture = None db.session.commit() flash('Profile picture removed successfully!', 'success') return redirect(url_for('main.profile')) new_email = request.form.get('email') logger.debug(f"New email: {new_email}") # Check if the new email is already used by another user if new_email != current_user.email: existing_user = User.query.filter_by(email=new_email).first() if existing_user: flash('A user with this email already exists.', 'error') return render_template('profile/profile.html') # Handle profile picture upload file = request.files.get('profile_picture') if file and file.filename: logger.debug(f"Uploading new profile picture: {file.filename}") filename = secure_filename(file.filename) file_path = os.path.join(UPLOAD_FOLDER, filename) file.save(file_path) current_user.profile_picture = filename # Update user information current_user.username = request.form.get('first_name') current_user.last_name = request.form.get('last_name') current_user.email = new_email current_user.phone = request.form.get('phone') current_user.company = request.form.get('company') current_user.position = request.form.get('position') current_user.notes = request.form.get('notes') logger.debug(f"Updated user data: username={current_user.username}, last_name={current_user.last_name}, email={current_user.email}") # Handle password change if provided new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_password') if new_password: if not confirm_password: flash('Please confirm your new password.', 'error') return render_template('profile/profile.html') if new_password != confirm_password: flash('Passwords do not match.', 'error') return render_template('profile/profile.html') current_user.set_password(new_password) # Create password change notification create_notification( notif_type='password_changed', user_id=current_user.id, details={ 'message': 'Your password has been changed successfully.', 'timestamp': datetime.utcnow().isoformat() } ) flash('Password updated successfully.', 'success') elif confirm_password: flash('Please enter a new password.', 'error') return render_template('profile/profile.html') # Create event details event_details = { 'user_id': current_user.id, 'email': current_user.email, 'update_type': 'profile_update', 'updated_fields': { '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, 'notes': current_user.notes, 'profile_picture': bool(current_user.profile_picture) }, 'changes': { 'username': request.form.get('first_name'), 'last_name': request.form.get('last_name'), 'email': request.form.get('email'), 'phone': request.form.get('phone'), 'company': request.form.get('company'), 'position': request.form.get('position'), 'notes': request.form.get('notes'), 'password_changed': bool(new_password) } } logger.debug(f"Preparing to create profile update event with details: {event_details}") # Commit all changes db.session.commit() logger.debug("Profile changes and event committed to database successfully") flash('Profile updated successfully!', 'success') return redirect(url_for('main.dashboard')) except Exception as e: logger.error(f"Error updating profile: {str(e)}") logger.error(f"Full error details: {str(e.__class__.__name__)}: {str(e)}") db.session.rollback() flash('An error occurred while updating your profile.', 'error') return render_template('profile/profile.html') return render_template('profile/profile.html') @main_bp.route('/starred') @login_required @require_password_change def starred(): return render_template('starred/starred.html') @main_bp.route('/conversations') @login_required @require_password_change def conversations(): return redirect(url_for('conversations.conversations')) @main_bp.route('/trash') @login_required @require_password_change def trash(): return render_template('trash/trash.html') @main_bp.route('/notifications') @login_required @require_password_change def notifications(): # Get filter parameters notif_type = request.args.get('notif_type', '') date_range = request.args.get('date_range', '7d') page = request.args.get('page', 1, type=int) per_page = 10 # Calculate date range end_date = datetime.utcnow() if date_range == '24h': start_date = end_date - timedelta(days=1) elif date_range == '7d': start_date = end_date - timedelta(days=7) elif date_range == '30d': start_date = end_date - timedelta(days=30) else: start_date = None # Build query query = Notif.query.filter_by(user_id=current_user.id) if notif_type: query = query.filter_by(notif_type=notif_type) if start_date: query = query.filter(Notif.timestamp >= start_date) # Get total count for pagination total_notifs = query.count() total_pages = (total_notifs + per_page - 1) // per_page # Get paginated notifications notifications = query.order_by(Notif.timestamp.desc()).paginate(page=page, per_page=per_page) # Check if this is an AJAX request if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({ 'notifications': [{ 'id': notif.id, 'notif_type': notif.notif_type, 'timestamp': notif.timestamp.strftime('%Y-%m-%d %H:%M:%S'), 'read': notif.read, 'details': notif.details, 'sender': { 'username': notif.sender.username, 'last_name': notif.sender.last_name } if notif.sender else None } for notif in notifications.items], 'total_pages': total_pages, 'current_page': page }) return render_template('notifications/notifications.html', notifications=notifications.items, total_pages=total_pages, current_page=page) @main_bp.route('/api/notifications') @login_required def get_notifications(): # Get filter parameters notif_type = request.args.get('notif_type', '') date_range = request.args.get('date_range', '7d') page = request.args.get('page', 1, type=int) per_page = 10 # Calculate date range end_date = datetime.utcnow() if date_range == '24h': start_date = end_date - timedelta(days=1) elif date_range == '7d': start_date = end_date - timedelta(days=7) elif date_range == '30d': start_date = end_date - timedelta(days=30) else: start_date = None # Build query query = Notif.query.filter_by(user_id=current_user.id) if notif_type: query = query.filter_by(notif_type=notif_type) if start_date: query = query.filter(Notif.timestamp >= start_date) # Get total count for pagination total_notifs = query.count() total_pages = (total_notifs + per_page - 1) // per_page # Get paginated notifications notifications = query.order_by(Notif.timestamp.desc()).paginate(page=page, per_page=per_page) return jsonify({ 'notifications': [{ 'id': notif.id, 'notif_type': notif.notif_type, 'timestamp': notif.timestamp.isoformat(), 'read': notif.read, 'details': notif.details, 'sender': { 'id': notif.sender.id, 'username': notif.sender.username, 'last_name': notif.sender.last_name } if notif.sender else None } for notif in notifications.items], 'total_pages': total_pages, 'current_page': page }) @main_bp.route('/api/notifications/') @login_required def get_notification_details(notif_id): notif = Notif.query.get_or_404(notif_id) if notif.user_id != current_user.id: return jsonify({'error': 'Unauthorized'}), 403 return jsonify({ 'id': notif.id, 'notif_type': notif.notif_type, 'timestamp': notif.timestamp.isoformat(), 'read': notif.read, 'details': notif.details, 'sender': { 'id': notif.sender.id, 'username': notif.sender.username } if notif.sender else None }) @main_bp.route('/api/notifications//read', methods=['POST']) @login_required def mark_notification_read(notif_id): notif = Notif.query.get_or_404(notif_id) if notif.user_id != current_user.id: return jsonify({'error': 'Unauthorized'}), 403 notif.read = True db.session.commit() return jsonify({'success': True}) @main_bp.route('/api/notifications/mark-all-read', methods=['POST']) @login_required def mark_all_notifications_read(): result = Notif.query.filter_by(user_id=current_user.id, read=False).update({'read': True}) db.session.commit() return jsonify({'success': True, 'count': result}) @main_bp.route('/api/notifications/', methods=['DELETE']) @login_required def delete_notification(notif_id): notif = Notif.query.get_or_404(notif_id) if notif.user_id != current_user.id: return jsonify({'error': 'Unauthorized'}), 403 db.session.delete(notif) db.session.commit() return jsonify({'success': True}) @main_bp.route('/settings') @login_required def settings(): if not current_user.is_admin: flash('You do not have permission to access settings.', 'error') return redirect(url_for('main.index')) active_tab = request.args.get('tab', 'colors') site_settings = SiteSettings.get_settings() company_form = CompanySettingsForm() # Get events for the events tab events = None total_pages = 0 current_page = 1 users = {} if active_tab == 'events': page = request.args.get('page', 1, type=int) per_page = 10 events = Event.query.order_by(Event.timestamp.desc()).paginate(page=page, per_page=per_page) total_pages = events.pages current_page = events.page # Get all users for the events user_ids = set() for event in events.items: user_ids.add(event.user_id) if event.details and 'target_user_id' in event.details: user_ids.add(event.details['target_user_id']) users = {user.id: user for user in User.query.filter(User.id.in_(user_ids)).all()} # Get email templates for the email templates tab email_templates = EmailTemplate.query.filter_by(is_active=True).all() # Get mails for the mails tab mails = None if active_tab == 'mails': page = request.args.get('page', 1, type=int) per_page = 10 mails = Mail.query.order_by(Mail.created_at.desc()).paginate(page=page, per_page=per_page) total_pages = mails.pages current_page = mails.page users = User.query.order_by(User.username).all() if request.method == 'GET': company_form.company_name.data = site_settings.company_name company_form.company_website.data = site_settings.company_website company_form.company_email.data = site_settings.company_email company_form.company_phone.data = site_settings.company_phone company_form.company_address.data = site_settings.company_address company_form.company_city.data = site_settings.company_city company_form.company_state.data = site_settings.company_state company_form.company_zip.data = site_settings.company_zip company_form.company_country.data = site_settings.company_country company_form.company_description.data = site_settings.company_description company_form.company_industry.data = site_settings.company_industry return render_template('settings/settings.html', primary_color=site_settings.primary_color, secondary_color=site_settings.secondary_color, active_tab=active_tab, site_settings=site_settings, events=events.items if events else None, mails=mails, total_pages=total_pages, current_page=current_page, users=users, email_templates=email_templates, form=company_form) @main_bp.route('/settings/colors', methods=['POST']) @login_required def update_colors(): if not current_user.is_admin: flash('Only administrators can update settings.', 'error') return redirect(url_for('main.dashboard')) logger.debug(f"Form data: {request.form}") logger.debug(f"CSRF token: {request.form.get('csrf_token')}") primary_color = request.form.get('primary_color') secondary_color = request.form.get('secondary_color') logger.debug(f"Primary color: {primary_color}") logger.debug(f"Secondary color: {secondary_color}") if not primary_color or not secondary_color: flash('Both primary and secondary colors are required.', 'error') return redirect(url_for('main.settings')) site_settings = SiteSettings.get_settings() site_settings.primary_color = primary_color site_settings.secondary_color = secondary_color try: db.session.commit() logger.debug("Colors updated successfully in database") # Log the color settings update log_event( event_type='settings_update', details={ 'user_id': current_user.id, 'user_name': f"{current_user.username} {current_user.last_name}", 'update_type': 'colors', 'changes': { 'primary_color': primary_color, 'secondary_color': secondary_color } } ) db.session.commit() flash('Color settings updated successfully!', 'success') except Exception as e: logger.error(f"Error updating colors: {str(e)}") db.session.rollback() flash('An error occurred while updating color settings.', 'error') return redirect(url_for('main.settings')) @main_bp.route('/settings/colors/reset', methods=['POST']) @login_required def reset_colors(): if not current_user.is_admin: flash('Only administrators can update settings.', 'error') return redirect(url_for('main.dashboard')) site_settings = SiteSettings.get_settings() site_settings.primary_color = '#16767b' # Default from colors.css site_settings.secondary_color = '#741b5f' # Default from colors.css try: db.session.commit() flash('Colors reset to defaults successfully!', 'success') except Exception as e: db.session.rollback() flash('An error occurred while resetting colors.', 'error') return redirect(url_for('main.settings')) @main_bp.route('/settings/company', methods=['POST']) @login_required def update_company_settings(): if not current_user.is_admin: flash('Only administrators can update settings.', 'error') return redirect(url_for('main.dashboard')) form = CompanySettingsForm() if not form.validate(): flash('Please check the form for errors.', 'error') return redirect(url_for('main.settings')) site_settings = SiteSettings.get_settings() # Handle logo upload if form.company_logo.data: logo_file = form.company_logo.data if logo_file.filename: # Delete old logo if it exists if site_settings.company_logo: old_logo_path = os.path.join('static', 'uploads', 'company_logos', site_settings.company_logo) if os.path.exists(old_logo_path): os.remove(old_logo_path) # Save new logo filename = secure_filename(logo_file.filename) # Add timestamp to filename to prevent caching issues filename = f"{int(time.time())}_{filename}" logo_path = os.path.join('static', 'uploads', 'company_logos', filename) logo_file.save(logo_path) site_settings.company_logo = filename # Update all company fields site_settings.company_name = form.company_name.data site_settings.company_website = form.company_website.data site_settings.company_email = form.company_email.data site_settings.company_phone = form.company_phone.data site_settings.company_address = form.company_address.data site_settings.company_city = form.company_city.data site_settings.company_state = form.company_state.data site_settings.company_zip = form.company_zip.data site_settings.company_country = form.company_country.data site_settings.company_description = form.company_description.data site_settings.company_industry = form.company_industry.data try: db.session.commit() # Log the company settings update log_event( event_type='settings_update', details={ 'user_id': current_user.id, 'user_name': f"{current_user.username} {current_user.last_name}", 'update_type': 'company_settings', 'changes': { 'company_name': site_settings.company_name, 'company_website': site_settings.company_website, 'company_email': site_settings.company_email, 'company_phone': site_settings.company_phone, 'company_address': site_settings.company_address, 'company_city': site_settings.company_city, 'company_state': site_settings.company_state, 'company_zip': site_settings.company_zip, 'company_country': site_settings.company_country, 'company_description': site_settings.company_description, 'company_industry': site_settings.company_industry, 'logo_updated': bool(form.company_logo.data) } } ) db.session.commit() flash('Company settings updated successfully!', 'success') except Exception as e: logger.error(f"Error updating company settings: {str(e)}") db.session.rollback() flash('An error occurred while updating company settings.', 'error') return redirect(url_for('main.settings')) @main_bp.route('/dynamic-colors.css') def dynamic_colors(): """Generate dynamic CSS variables based on site settings""" logger.info(f"[Dynamic Colors] Request from: {request.referrer}") # Get colors from settings site_settings = SiteSettings.get_settings() primary_color = site_settings.primary_color secondary_color = site_settings.secondary_color logger.info(f"[Dynamic Colors] Current colors - Primary: {primary_color}, Secondary: {secondary_color}") # Convert hex to RGB for opacity calculations def hex_to_rgb(hex_color): hex_color = hex_color.lstrip('#') return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) def rgb_to_hex(rgb): return '#{:02x}{:02x}{:02x}'.format(rgb[0], rgb[1], rgb[2]) def lighten_color(hex_color, amount=0.2): rgb = hex_to_rgb(hex_color) rgb = tuple(min(255, int(c + (255 - c) * amount)) for c in rgb) return rgb_to_hex(rgb) # Calculate derived colors primary_rgb = hex_to_rgb(primary_color) secondary_rgb = hex_to_rgb(secondary_color) # Lighten colors for hover states primary_light = lighten_color(primary_color, 0.2) secondary_light = lighten_color(secondary_color, 0.2) # Generate CSS with opacity variables css = f""" :root {{ /* Primary Colors */ --primary-color: {primary_color}; --primary-light: {primary_light}; --primary-rgb: {primary_rgb[0]}, {primary_rgb[1]}, {primary_rgb[2]}; --primary-opacity-8: rgba({primary_rgb[0]}, {primary_rgb[1]}, {primary_rgb[2]}, 0.08); --primary-opacity-15: rgba({primary_rgb[0]}, {primary_rgb[1]}, {primary_rgb[2]}, 0.15); --primary-opacity-25: rgba({primary_rgb[0]}, {primary_rgb[1]}, {primary_rgb[2]}, 0.25); --primary-opacity-50: rgba({primary_rgb[0]}, {primary_rgb[1]}, {primary_rgb[2]}, 0.5); /* Secondary Colors */ --secondary-color: {secondary_color}; --secondary-light: {secondary_light}; --secondary-rgb: {secondary_rgb[0]}, {secondary_rgb[1]}, {secondary_rgb[2]}; --secondary-opacity-8: rgba({secondary_rgb[0]}, {secondary_rgb[1]}, {secondary_rgb[2]}, 0.08); --secondary-opacity-15: rgba({secondary_rgb[0]}, {secondary_rgb[1]}, {secondary_rgb[2]}, 0.15); --secondary-opacity-25: rgba({secondary_rgb[0]}, {secondary_rgb[1]}, {secondary_rgb[2]}, 0.25); --secondary-opacity-50: rgba({secondary_rgb[0]}, {secondary_rgb[1]}, {secondary_rgb[2]}, 0.5); /* Chart Colors */ --chart-color-1: {primary_color}; --chart-color-2: {secondary_color}; --chart-color-3: {lighten_color(primary_color, 0.4)}; --chart-color-4: {lighten_color(secondary_color, 0.4)}; }} """ logger.info(f"[Dynamic Colors] Generated CSS with primary color: {primary_color}") logger.info(f"[Dynamic Colors] Cache version: {site_settings.updated_at.timestamp()}") return Response(css, mimetype='text/css') @main_bp.route('/settings/events') @login_required def events(): if not current_user.is_admin: flash('Only administrators can access event logs.', 'error') return redirect(url_for('main.dashboard')) # Get filter parameters event_type = request.args.get('event_type') date_range = request.args.get('date_range', '7d') user_id = request.args.get('user_id') page = request.args.get('page', 1, type=int) per_page = 10 # Calculate date range end_date = datetime.utcnow() if date_range == '24h': start_date = end_date - timedelta(days=1) elif date_range == '7d': start_date = end_date - timedelta(days=7) elif date_range == '30d': start_date = end_date - timedelta(days=30) else: start_date = None # Build query query = Event.query if event_type: query = query.filter_by(event_type=event_type) if start_date: query = query.filter(Event.timestamp >= start_date) if user_id: query = query.filter_by(user_id=user_id) # Get total count for pagination total_events = query.count() total_pages = (total_events + per_page - 1) // per_page # Get paginated events events = query.order_by(Event.timestamp.desc()).paginate(page=page, per_page=per_page) # Get all users for filter dropdown users = User.query.order_by(User.username).all() # Check if this is an AJAX request if request.headers.get('X-Requested-With') == 'XMLHttpRequest': logger.info(f"Processing AJAX request for events. Found {len(events.items)} events") return render_template('settings/tabs/events.html', events=events.items, total_pages=total_pages, current_page=page, event_type=event_type, date_range=date_range, user_id=user_id, users=users, csrf_token=session.get('csrf_token')) # For full page requests, render the full settings page site_settings = SiteSettings.get_settings() return render_template('settings/settings.html', primary_color=site_settings.primary_color, secondary_color=site_settings.secondary_color, active_tab='events', site_settings=site_settings, events=events.items, total_pages=total_pages, current_page=page, users=users, csrf_token=session.get('csrf_token')) @main_bp.route('/api/events/') @login_required def get_event_details(event_id): if not current_user.is_admin: 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 = { 'id': event.id, 'event_type': event.event_type, 'timestamp': event.timestamp.isoformat(), '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) @main_bp.route('/settings/events/download') @login_required def download_events(): if not current_user.is_admin: flash('Only administrators can download event logs.', 'error') return redirect(url_for('main.dashboard')) # Get filter parameters event_type = request.args.get('event_type') date_range = request.args.get('date_range', '7d') user_id = request.args.get('user_id') # Calculate date range end_date = datetime.utcnow() if date_range == '24h': start_date = end_date - timedelta(days=1) elif date_range == '7d': start_date = end_date - timedelta(days=7) elif date_range == '30d': start_date = end_date - timedelta(days=30) else: start_date = None # Build query query = Event.query if event_type: query = query.filter_by(event_type=event_type) if start_date: query = query.filter(Event.timestamp >= start_date) if user_id: query = query.filter_by(user_id=user_id) # Get all events events = query.order_by(Event.timestamp.desc()).all() # Create CSV content import csv import io output = io.StringIO() writer = csv.writer(output) # Write header writer.writerow(['Timestamp', 'Event Type', 'User', 'Details', 'IP Address']) # Write data for event in events: user_name = f"{event.user.username} {event.user.last_name}" if event.user else "System" writer.writerow([ event.timestamp.strftime('%Y-%m-%d %H:%M:%S'), event.event_type, user_name, str(event.details), event.ip_address ]) # Create the response output.seek(0) return Response( output, mimetype='text/csv', headers={ 'Content-Disposition': f'attachment; filename=event_log_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.csv' } ) @main_bp.route('/settings/email-templates/', methods=['PUT']) @login_required def update_email_template(template_id): if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 template = EmailTemplate.query.get_or_404(template_id) data = request.get_json() if not data: return jsonify({'error': 'No data provided'}), 400 template.subject = data.get('subject', template.subject) template.body = data.get('body', template.body) try: db.session.commit() # Log the template update log_event( event_type='settings_update', details={ 'user_id': current_user.id, 'user_name': f"{current_user.username} {current_user.last_name}", 'update_type': 'email_template', 'template_id': template.id, 'template_name': template.name, 'changes': { 'subject': template.subject, 'body': template.body } } ) db.session.commit() return jsonify({ 'message': 'Template updated successfully', 'template': { 'id': template.id, 'name': template.name, 'subject': template.subject, 'body': template.body } }) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @main_bp.route('/settings/mails') @login_required def mails(): if not current_user.is_admin: flash('Only administrators can access mail logs.', 'error') return redirect(url_for('main.dashboard')) # Get filter parameters status = request.args.get('status') date_range = request.args.get('date_range', '7d') user_id = request.args.get('user_id') page = request.args.get('page', 1, type=int) per_page = 10 # Calculate date range end_date = datetime.utcnow() if date_range == '24h': start_date = end_date - timedelta(days=1) elif date_range == '7d': start_date = end_date - timedelta(days=7) elif date_range == '30d': start_date = end_date - timedelta(days=30) else: start_date = None # Build query query = Mail.query if status: query = query.filter_by(status=status) if start_date: query = query.filter(Mail.created_at >= start_date) if user_id: query = query.filter(Mail.recipient == User.query.get(user_id).email) # Get total count for pagination total_mails = query.count() total_pages = (total_mails + per_page - 1) // per_page # Get paginated mails mails = query.order_by(Mail.created_at.desc()).paginate(page=page, per_page=per_page) # Get all users for filter dropdown users = User.query.order_by(User.username).all() # Check if this is an AJAX request if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return render_template('settings/tabs/mails.html', mails=mails.items, total_pages=total_pages, current_page=page, status=status, date_range=date_range, user_id=user_id, users=users, csrf_token=session.get('csrf_token')) # For full page requests, render the full settings page site_settings = SiteSettings.get_settings() return render_template('settings/settings.html', primary_color=site_settings.primary_color, secondary_color=site_settings.secondary_color, active_tab='mails', site_settings=site_settings, mails=mails.items, total_pages=total_pages, current_page=page, users=users, csrf_token=session.get('csrf_token')) @main_bp.route('/settings/mails/') @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 }) @main_bp.route('/settings/mails/download') @login_required def download_mails(): if not current_user.is_admin: flash('Only administrators can download mail logs.', 'error') return redirect(url_for('main.dashboard')) # Get filter parameters status = request.args.get('status') date_range = request.args.get('date_range', '7d') user_id = request.args.get('user_id') # Calculate date range end_date = datetime.utcnow() if date_range == '24h': start_date = end_date - timedelta(days=1) elif date_range == '7d': start_date = end_date - timedelta(days=7) elif date_range == '30d': start_date = end_date - timedelta(days=30) else: start_date = None # Build query query = Mail.query if status: query = query.filter_by(status=status) if start_date: query = query.filter(Mail.created_at >= start_date) if user_id: query = query.filter(Mail.recipient == User.query.get(user_id).email) # Get all mails mails = query.order_by(Mail.created_at.desc()).all() # Create CSV output = StringIO() writer = csv.writer(output) # Write header writer.writerow([ 'Created At', 'Recipient', 'Subject', 'Status', 'Template', 'Sent At' ]) # Write data for mail in mails: writer.writerow([ mail.created_at.strftime('%Y-%m-%d %H:%M:%S'), mail.recipient, mail.subject, mail.status, mail.template.name if mail.template else '-', mail.sent_at.strftime('%Y-%m-%d %H:%M:%S') if mail.sent_at else '-' ]) output.seek(0) return Response( output, mimetype='text/csv', headers={ 'Content-Disposition': f'attachment; filename=mail_log_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.csv' } )