diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 19e3c6d..15107a8 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/__pycache__/utils.cpython-313.pyc b/__pycache__/utils.cpython-313.pyc index e3bb6db..0c3fa99 100644 Binary files a/__pycache__/utils.cpython-313.pyc and b/__pycache__/utils.cpython-313.pyc differ diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index 3792ef5..012fb5a 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 82385d2..0ce2c56 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 bc544d1..7ea0e73 100644 Binary files a/routes/__pycache__/conversations.cpython-313.pyc and b/routes/__pycache__/conversations.cpython-313.pyc differ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 28d31bb..41152ba 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/__pycache__/room_members.cpython-313.pyc b/routes/__pycache__/room_members.cpython-313.pyc index f79e776..7e6e591 100644 Binary files a/routes/__pycache__/room_members.cpython-313.pyc and b/routes/__pycache__/room_members.cpython-313.pyc differ diff --git a/routes/__pycache__/rooms.cpython-313.pyc b/routes/__pycache__/rooms.cpython-313.pyc index 8a6e38e..9066e79 100644 Binary files a/routes/__pycache__/rooms.cpython-313.pyc and b/routes/__pycache__/rooms.cpython-313.pyc differ diff --git a/routes/__pycache__/trash.cpython-313.pyc b/routes/__pycache__/trash.cpython-313.pyc index 3b39dbf..1522d8f 100644 Binary files a/routes/__pycache__/trash.cpython-313.pyc and b/routes/__pycache__/trash.cpython-313.pyc differ diff --git a/routes/main.py b/routes/main.py index 9038adf..53fe545 100644 --- a/routes/main.py +++ b/routes/main.py @@ -1,6 +1,6 @@ -from flask import render_template, Blueprint, redirect, url_for, request, flash, Response +from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify from flask_login import current_user, login_required -from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings +from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event from routes.auth import require_password_change import os from werkzeug.utils import secure_filename @@ -9,6 +9,7 @@ from datetime import datetime, timedelta import logging import sys import time +from flask import session # Set up logging to show in console logging.basicConfig( @@ -530,4 +531,80 @@ def init_routes(main_bp): 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') \ No newline at end of file + 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 = 50 + + # 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() + + 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')) + + @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) + return jsonify({ + 'id': event.id, + 'event_type': event.event_type, + 'user': { + 'id': event.user.id, + 'username': event.user.username, + 'last_name': event.user.last_name + }, + 'timestamp': event.timestamp.isoformat(), + 'details': event.details, + 'ip_address': event.ip_address, + 'user_agent': event.user_agent + }) \ No newline at end of file diff --git a/static/js/events.js b/static/js/events.js new file mode 100644 index 0000000..eee460b --- /dev/null +++ b/static/js/events.js @@ -0,0 +1,95 @@ +document.addEventListener('DOMContentLoaded', function() { + // Initialize variables + let currentPage = 1; + const totalPages = parseInt(document.getElementById('totalPages').textContent); + + // Get filter elements + const eventTypeFilter = document.getElementById('eventTypeFilter'); + const dateRangeFilter = document.getElementById('dateRangeFilter'); + const userFilter = document.getElementById('userFilter'); + const applyFiltersBtn = document.getElementById('applyFilters'); + + // Get pagination elements + const prevPageBtn = document.getElementById('prevPage'); + const nextPageBtn = document.getElementById('nextPage'); + const currentPageSpan = document.getElementById('currentPage'); + + // Event details modal + const eventDetailsModal = document.getElementById('eventDetailsModal'); + const eventDetailsContent = document.getElementById('eventDetailsContent'); + + // Function to update URL with current filters + function updateURL() { + const params = new URLSearchParams(window.location.search); + params.set('page', currentPage); + if (eventTypeFilter.value) params.set('event_type', eventTypeFilter.value); + if (dateRangeFilter.value) params.set('date_range', dateRangeFilter.value); + if (userFilter.value) params.set('user_id', userFilter.value); + window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`); + } + + // Function to load events with current filters + function loadEvents() { + const params = new URLSearchParams(); + params.set('page', currentPage); + if (eventTypeFilter.value) params.set('event_type', eventTypeFilter.value); + if (dateRangeFilter.value) params.set('date_range', dateRangeFilter.value); + if (userFilter.value) params.set('user_id', userFilter.value); + + window.location.href = `${window.location.pathname}?${params.toString()}`; + } + + // Function to load event details + function loadEventDetails(eventId) { + fetch(`/api/events/${eventId}`) + .then(response => response.json()) + .then(data => { + const formattedDetails = JSON.stringify(data.details, null, 2); + eventDetailsContent.textContent = formattedDetails; + }) + .catch(error => { + console.error('Error loading event details:', error); + eventDetailsContent.textContent = 'Error loading event details'; + }); + } + + // Event listeners for filters + applyFiltersBtn.addEventListener('click', function() { + currentPage = 1; + loadEvents(); + }); + + // Event listeners for pagination + prevPageBtn.addEventListener('click', function() { + if (currentPage > 1) { + currentPage--; + loadEvents(); + } + }); + + nextPageBtn.addEventListener('click', function() { + if (currentPage < totalPages) { + currentPage++; + loadEvents(); + } + }); + + // Event listener for event details modal + eventDetailsModal.addEventListener('show.bs.modal', function(event) { + const button = event.relatedTarget; + const eventId = button.getAttribute('data-event-id'); + loadEventDetails(eventId); + }); + + // Initialize filters from URL parameters + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has('event_type')) eventTypeFilter.value = urlParams.get('event_type'); + if (urlParams.has('date_range')) dateRangeFilter.value = urlParams.get('date_range'); + if (urlParams.has('user_id')) userFilter.value = urlParams.get('user_id'); + if (urlParams.has('page')) currentPage = parseInt(urlParams.get('page')); + + // Update pagination buttons state + prevPageBtn.disabled = currentPage === 1; + nextPageBtn.disabled = currentPage === totalPages; + currentPageSpan.textContent = currentPage; +}); \ No newline at end of file diff --git a/templates/settings/settings.html b/templates/settings/settings.html index d75f4b9..94e81c6 100644 --- a/templates/settings/settings.html +++ b/templates/settings/settings.html @@ -4,6 +4,7 @@ {% from "settings/tabs/company_info.html" import company_info_tab %} {% from "settings/tabs/security.html" import security_tab %} {% from "settings/tabs/debugging.html" import debugging_tab %} +{% from "settings/tabs/events.html" import events_tab %} {% from "settings/components/reset_colors_modal.html" import reset_colors_modal %} {% block title %}Settings - DocuPulse{% endblock %} @@ -41,6 +42,11 @@ Security +