diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 19e3c6d..bc5267f 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/app.py b/app.py index 8d89b5f..bd08a76 100644 --- a/app.py +++ b/app.py @@ -25,6 +25,7 @@ def create_app(): app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secure-secret-key-here') app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'static', 'uploads') app.config['CSS_VERSION'] = os.getenv('CSS_VERSION', '1.0.0') # Add CSS version for cache busting + app.config['JS_VERSION'] = os.getenv('JS_VERSION', '1.0.0') # Add JS version for cache busting # Initialize extensions db.init_app(app) diff --git a/models.py b/models.py index 02a1fb8..30deecf 100644 --- a/models.py +++ b/models.py @@ -205,46 +205,27 @@ class MessageAttachment(db.Model): return f'' class EventType(Enum): - # User events USER_LOGIN = 'user_login' USER_LOGOUT = 'user_logout' - USER_CREATE = 'user_create' + USER_REGISTER = 'user_register' USER_UPDATE = 'user_update' - USER_DELETE = 'user_delete' - - # Room events - ROOM_CREATE = 'room_create' - ROOM_UPDATE = 'room_update' - ROOM_DELETE = 'room_delete' - ROOM_MEMBER_ADD = 'room_member_add' - ROOM_MEMBER_REMOVE = 'room_member_remove' - ROOM_PERMISSION_UPDATE = 'room_permission_update' - - # File events FILE_UPLOAD = 'file_upload' - FILE_DOWNLOAD = 'file_download' FILE_DELETE = 'file_delete' - FILE_RENAME = 'file_rename' + FILE_DOWNLOAD = 'file_download' + FILE_RESTORE = 'file_restore' FILE_MOVE = 'file_move' + FILE_RENAME = 'file_rename' FILE_STAR = 'file_star' FILE_UNSTAR = 'file_unstar' - - # Conversation events + ROOM_CREATE = 'room_create' + ROOM_DELETE = 'room_delete' + ROOM_UPDATE = 'room_update' + ROOM_JOIN = 'room_join' + ROOM_LEAVE = 'room_leave' CONVERSATION_CREATE = 'conversation_create' - CONVERSATION_UPDATE = 'conversation_update' CONVERSATION_DELETE = 'conversation_delete' - CONVERSATION_MEMBER_ADD = 'conversation_member_add' - CONVERSATION_MEMBER_REMOVE = 'conversation_member_remove' - - # Message events - MESSAGE_CREATE = 'message_create' - MESSAGE_UPDATE = 'message_update' - MESSAGE_DELETE = 'message_delete' - MESSAGE_ATTACHMENT_ADD = 'message_attachment_add' - MESSAGE_ATTACHMENT_REMOVE = 'message_attachment_remove' - - # Settings events - SETTINGS_UPDATE = 'settings_update' + MESSAGE_SENT = 'message_sent' + ATTACHMENT_DOWNLOAD = 'attachment_download' class Event(db.Model): __tablename__ = 'events' diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index 3792ef5..76dea33 100644 Binary files a/routes/__pycache__/auth.cpython-313.pyc and b/routes/__pycache__/auth.cpython-313.pyc differ diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc index bc544d1..80b98ba 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..21b9a7e 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/auth.py b/routes/auth.py index 15fab4e..9591b6b 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -2,6 +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.event_logger import log_event +from models import EventType def require_password_change(f): @wraps(f) @@ -31,6 +33,13 @@ def init_routes(auth_bp): login_user(user, remember=remember) + # Log successful login + log_event( + event_type=EventType.USER_LOGIN, + user_id=user.id, + details={'remember': remember} + ) + # Check if user is using default password if password == 'changeme': flash('Please change your password before continuing.', 'warning') @@ -69,6 +78,13 @@ def init_routes(auth_bp): db.session.add(new_user) db.session.commit() + # Log user creation + log_event( + event_type=EventType.USER_CREATE, + user_id=new_user.id, + details={'email': email, 'username': username} + ) + login_user(new_user) return redirect(url_for('main.dashboard')) @@ -77,6 +93,11 @@ def init_routes(auth_bp): @auth_bp.route('/logout') @login_required def logout(): + # Log logout before actually logging out + log_event( + event_type=EventType.USER_LOGOUT, + user_id=current_user.id + ) logout_user() return redirect(url_for('auth.login')) @@ -98,6 +119,14 @@ def init_routes(auth_bp): current_user.set_password(new_password) db.session.commit() + + # Log password change + log_event( + event_type=EventType.USER_UPDATE, + user_id=current_user.id, + details={'action': 'password_change'} + ) + flash('Password changed successfully!', 'success') return redirect(url_for('main.dashboard')) diff --git a/routes/conversations.py b/routes/conversations.py index 3bcdbe9..3eca36c 100644 --- a/routes/conversations.py +++ b/routes/conversations.py @@ -1,8 +1,9 @@ from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file from flask_login import login_required, current_user -from models import db, Conversation, User, Message, MessageAttachment +from models import db, Conversation, User, Message, MessageAttachment, EventType from forms import ConversationForm from routes.auth import require_password_change +from utils.event_logger import log_event import os from werkzeug.utils import secure_filename from datetime import datetime @@ -84,6 +85,17 @@ def create_conversation(): db.session.add(conversation) db.session.commit() + # Log conversation creation + log_event( + event_type=EventType.CONVERSATION_CREATE, + user_id=current_user.id, + details={ + 'conversation_id': conversation.id, + 'conversation_name': conversation.name, + 'member_count': len(conversation.members) + } + ) + flash('Conversation created successfully!', 'success') return redirect(url_for('conversations.conversations')) return render_template('conversations/create_conversation.html', form=form) @@ -227,6 +239,18 @@ def delete_conversation(conversation_id): conversation = Conversation.query.get_or_404(conversation_id) + # Log conversation deletion before deleting + log_event( + event_type=EventType.CONVERSATION_DELETE, + user_id=current_user.id, + details={ + 'conversation_id': conversation_id, + 'conversation_name': conversation.name, + 'message_count': len(conversation.messages), + 'member_count': len(conversation.members) + } + ) + # Delete all messages in the conversation Message.query.filter_by(conversation_id=conversation_id).delete() @@ -324,6 +348,18 @@ def send_message(conversation_id): db.session.commit() + # Log message sent + log_event( + event_type=EventType.MESSAGE_SENT, + user_id=current_user.id, + details={ + 'conversation_id': conversation_id, + 'message_id': message.id, + 'has_attachments': len(attachments) > 0, + 'attachment_count': len(attachments) + } + ) + # Prepare message data for response message_data = { 'id': message.id, @@ -358,6 +394,19 @@ def download_attachment(message_id, attachment_index): try: attachment = message.attachments[attachment_index] + + # Log attachment download + log_event( + event_type=EventType.ATTACHMENT_DOWNLOAD, + user_id=current_user.id, + details={ + 'conversation_id': conversation.id, + 'message_id': message_id, + 'attachment_name': attachment.name, + 'attachment_size': attachment.size + } + ) + return send_file( attachment.path, as_attachment=True, diff --git a/routes/main.py b/routes/main.py index 9038adf..67159fd 100644 --- a/routes/main.py +++ b/routes/main.py @@ -1,7 +1,8 @@ -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, EventType from routes.auth import require_password_change +from utils.event_logger import log_event import os from werkzeug.utils import secure_filename from sqlalchemy import func, case, literal_column, text @@ -279,6 +280,14 @@ def init_routes(main_bp): os.remove(old_picture_path) current_user.profile_picture = None db.session.commit() + + # Log profile picture removal + log_event( + event_type=EventType.USER_UPDATE, + user_id=current_user.id, + details={'action': 'remove_profile_picture'} + ) + flash('Profile picture removed successfully!', 'success') return redirect(url_for('main.profile')) @@ -289,6 +298,10 @@ def init_routes(main_bp): if existing_user: flash('A user with this email already exists.', 'error') return render_template('profile/profile.html') + + # Track changes for event logging + changes = {} + # Handle profile picture upload file = request.files.get('profile_picture') if file and file.filename: @@ -296,14 +309,31 @@ def init_routes(main_bp): file_path = os.path.join(UPLOAD_FOLDER, filename) file.save(file_path) current_user.profile_picture = filename + changes['profile_picture'] = True + # 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') + if current_user.username != request.form.get('first_name'): + current_user.username = request.form.get('first_name') + changes['username'] = True + if current_user.last_name != request.form.get('last_name'): + current_user.last_name = request.form.get('last_name') + changes['last_name'] = True + if current_user.email != new_email: + current_user.email = new_email + changes['email'] = True + if current_user.phone != request.form.get('phone'): + current_user.phone = request.form.get('phone') + changes['phone'] = True + if current_user.company != request.form.get('company'): + current_user.company = request.form.get('company') + changes['company'] = True + if current_user.position != request.form.get('position'): + current_user.position = request.form.get('position') + changes['position'] = True + if current_user.notes != request.form.get('notes'): + current_user.notes = request.form.get('notes') + changes['notes'] = True + # Handle password change if provided new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_password') @@ -312,9 +342,20 @@ def init_routes(main_bp): flash('Passwords do not match.', 'error') return render_template('profile/profile.html') current_user.set_password(new_password) + changes['password'] = True flash('Password updated successfully.', 'success') + try: db.session.commit() + + # Log profile update if any changes were made + if changes: + log_event( + event_type=EventType.USER_UPDATE, + user_id=current_user.id, + details={'changes': changes} + ) + flash('Profile updated successfully!', 'success') except Exception as e: db.session.rollback() @@ -355,11 +396,18 @@ def init_routes(main_bp): site_settings = SiteSettings.get_settings() active_tab = request.args.get('tab', 'colors') + + # Get events for the events tab + events = [] + if active_tab == 'events': + events = Event.query.order_by(Event.timestamp.desc()).limit(50).all() + 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) + site_settings=site_settings, + events=events) @main_bp.route('/settings/colors', methods=['POST']) @login_required @@ -530,4 +578,105 @@ 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('/api/events') + @login_required + def get_events(): + if not current_user.is_admin: + return jsonify({'success': False, 'error': 'Unauthorized'}), 403 + + # Get filter parameters + page = request.args.get('page', 1, type=int) + event_type = request.args.get('eventType') + date_range = request.args.get('dateRange', '24h') + user_id = request.args.get('userId') + + # Build query + query = Event.query + + # Apply filters + if event_type: + query = query.filter_by(event_type=event_type) + if user_id: + query = query.filter_by(user_id=user_id) + + # Apply date range filter + if date_range != 'all': + now = datetime.utcnow() + if date_range == '24h': + start_date = now - timedelta(days=1) + elif date_range == '7d': + start_date = now - timedelta(days=7) + elif date_range == '30d': + start_date = now - timedelta(days=30) + query = query.filter(Event.timestamp >= start_date) + + # Get total count for pagination + total_count = query.count() + per_page = 50 + total_pages = (total_count + per_page - 1) // per_page + + # Get paginated results + events = query.order_by(Event.timestamp.desc())\ + .offset((page - 1) * per_page)\ + .limit(per_page)\ + .all() + + return jsonify({ + 'success': True, + 'events': [{ + '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 or '' + }, + 'ip_address': event.ip_address, + 'details': event.details + } for event in events], + 'total_pages': total_pages + }) + + @main_bp.route('/api/events/') + @login_required + def get_event_details(event_id): + if not current_user.is_admin: + return jsonify({'success': False, 'error': 'Unauthorized'}), 403 + + event = Event.query.get_or_404(event_id) + + return jsonify({ + 'success': True, + 'event': { + 'id': event.id, + 'event_type': event.event_type, + 'timestamp': event.timestamp.isoformat(), + 'user': { + 'id': event.user.id, + 'username': event.user.username + }, + 'ip_address': event.ip_address, + 'user_agent': event.user_agent, + 'details': event.details + } + }) + + @main_bp.route('/api/users') + @login_required + def get_users(): + if not current_user.is_admin: + return jsonify({'success': False, 'error': 'Unauthorized'}), 403 + + users = User.query.order_by(User.username).all() + + return jsonify({ + 'success': True, + 'users': [{ + 'id': user.id, + 'username': user.username, + 'last_name': user.last_name or '' + } for user in users] + }) \ No newline at end of file diff --git a/static/js/events.js b/static/js/events.js new file mode 100644 index 0000000..e1e1147 --- /dev/null +++ b/static/js/events.js @@ -0,0 +1,272 @@ +/** + * @fileoverview Manages the events page functionality. + * This file handles event filtering, pagination, and modal interactions. + */ + +document.addEventListener('DOMContentLoaded', function() { + // Elements + const eventTypeFilter = document.getElementById('eventTypeFilter'); + const dateRangeFilter = document.getElementById('dateRangeFilter'); + const userFilter = document.getElementById('userFilter'); + const applyFiltersBtn = document.getElementById('applyFilters'); + const eventsTableBody = document.getElementById('eventsTableBody'); + const prevPageBtn = document.getElementById('prevPage'); + const nextPageBtn = document.getElementById('nextPage'); + const currentPageSpan = document.getElementById('currentPage'); + const totalPagesSpan = document.getElementById('totalPages'); + const eventDetailsModal = document.getElementById('eventDetailsModal'); + const eventDetailsContent = document.getElementById('eventDetailsContent'); + + // State + let currentPage = 1; + let totalPages = 1; + let currentFilters = { + eventType: '', + dateRange: '24h', + userId: '' + }; + + /** + * Loads events based on current filters and page + */ + async function loadEvents() { + try { + const response = await fetch(`/api/events?page=${currentPage}&${new URLSearchParams(currentFilters)}`); + const data = await response.json(); + + if (data.success) { + renderEvents(data.events); + updatePagination(data.total_pages); + } else { + console.error('Failed to load events:', data.error); + } + } catch (error) { + console.error('Error loading events:', error); + } + } + + /** + * Renders events in the table + */ + function renderEvents(events) { + if (!Array.isArray(events)) { + console.error('Invalid events data received:', events); + return; + } + + const eventRows = events.map(event => { + if (!event || typeof event !== 'object') { + console.error('Invalid event data:', event); + return ''; + } + + // Determine badge color and text based on event type + let badgeClass = 'bg-secondary'; + let eventText = event.event_type || 'Unknown Event'; + + switch(event.event_type) { + case 'user_login': + badgeClass = 'bg-success'; + eventText = 'User Login'; + break; + case 'user_logout': + badgeClass = 'bg-secondary'; + eventText = 'User Logout'; + break; + case 'user_register': + badgeClass = 'bg-info'; + eventText = 'User Registration'; + break; + case 'user_update': + badgeClass = 'bg-primary'; + eventText = 'User Update'; + break; + case 'file_upload': + badgeClass = 'bg-success'; + eventText = 'File Upload'; + break; + case 'file_delete': + badgeClass = 'bg-danger'; + eventText = 'File Delete'; + break; + case 'file_download': + badgeClass = 'bg-info'; + eventText = 'File Download'; + break; + case 'file_restore': + badgeClass = 'bg-warning'; + eventText = 'File Restore'; + break; + case 'file_move': + badgeClass = 'bg-primary'; + eventText = 'File Move'; + break; + case 'file_rename': + badgeClass = 'bg-info'; + eventText = 'File Rename'; + break; + case 'file_star': + badgeClass = 'bg-warning'; + eventText = 'File Star'; + break; + case 'file_unstar': + badgeClass = 'bg-secondary'; + eventText = 'File Unstar'; + break; + case 'room_create': + badgeClass = 'bg-success'; + eventText = 'Room Create'; + break; + case 'room_delete': + badgeClass = 'bg-danger'; + eventText = 'Room Delete'; + break; + case 'room_update': + badgeClass = 'bg-primary'; + eventText = 'Room Update'; + break; + case 'room_join': + badgeClass = 'bg-info'; + eventText = 'Room Join'; + break; + case 'room_leave': + badgeClass = 'bg-secondary'; + eventText = 'Room Leave'; + break; + case 'conversation_create': + badgeClass = 'bg-success'; + eventText = 'Conversation Create'; + break; + case 'conversation_delete': + badgeClass = 'bg-danger'; + eventText = 'Conversation Delete'; + break; + case 'message_sent': + badgeClass = 'bg-primary'; + eventText = 'Message Sent'; + break; + case 'attachment_download': + badgeClass = 'bg-info'; + eventText = 'Attachment Download'; + break; + } + + const timestamp = event.timestamp ? new Date(event.timestamp).toLocaleString() : '-'; + const username = event.user?.username || 'Unknown User'; + const lastName = event.user?.last_name || ''; + const eventId = event.id || ''; + const ipAddress = event.ip_address || '-'; + + return ` + + ${timestamp} + ${eventText} + ${username} ${lastName} + + + + ${ipAddress} + + `; + }).join(''); + + eventsTableBody.innerHTML = eventRows; + + // Add event listeners to detail buttons + document.querySelectorAll('[data-event-id]').forEach(button => { + button.addEventListener('click', () => { + const eventId = button.dataset.eventId; + if (eventId) { + showEventDetails(eventId); + } + }); + }); + } + + /** + * Updates pagination controls + */ + function updatePagination(total) { + totalPages = total; + currentPageSpan.textContent = currentPage; + totalPagesSpan.textContent = totalPages; + + prevPageBtn.classList.toggle('disabled', currentPage === 1); + nextPageBtn.classList.toggle('disabled', currentPage === totalPages); + } + + /** + * Shows event details in modal + */ + async function showEventDetails(eventId) { + try { + const response = await fetch(`/api/events/${eventId}`); + const data = await response.json(); + + if (data.success) { + const event = data.event; + eventDetailsContent.textContent = JSON.stringify(event.details, null, 2); + } else { + console.error('Failed to load event details:', data.error); + } + } catch (error) { + console.error('Error loading event details:', error); + } + } + + /** + * Loads users for the user filter + */ + async function loadUsers() { + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + const userFilter = document.getElementById('userFilter'); + userFilter.innerHTML = ''; + + data.users.forEach(user => { + const option = document.createElement('option'); + option.value = user.id; + option.textContent = `${user.username} ${user.last_name || ''}`.trim(); + userFilter.appendChild(option); + }); + } catch (error) { + console.error('Error loading users:', error); + } + } + + // Event Listeners + applyFiltersBtn.addEventListener('click', () => { + currentFilters = { + eventType: eventTypeFilter.value, + dateRange: dateRangeFilter.value, + userId: userFilter.value + }; + currentPage = 1; + loadEvents(); + }); + + prevPageBtn.addEventListener('click', () => { + if (currentPage > 1) { + currentPage--; + loadEvents(); + } + }); + + nextPageBtn.addEventListener('click', () => { + if (currentPage < totalPages) { + currentPage++; + loadEvents(); + } + }); + + // Initialize + loadUsers(); + loadEvents(); +}); \ No newline at end of file diff --git a/static/js/file-grid.js b/static/js/file-grid.js index 36ffc80..95a99e2 100644 --- a/static/js/file-grid.js +++ b/static/js/file-grid.js @@ -354,6 +354,24 @@ function toggleStar(filename, path = '', roomId) { .then(r => r.json()) .then(res => { if (res.success) { + // Log the star/unstar event + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ + event_type: res.starred ? 'file_star' : 'file_unstar', + details: { + filename: filename, + path: path, + room_id: roomId, + timestamp: new Date().toISOString() + } + }) + }); + // Remove the file from the current view since it's no longer starred currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId)); renderFiles(currentFiles); @@ -394,6 +412,24 @@ function restoreFile(filename, path = '', roomId) { .then(r => r.json()) .then(res => { if (res.success) { + // Log the restore event + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ + event_type: 'file_restore', + details: { + filename: filename, + path: path, + room_id: roomId, + timestamp: new Date().toISOString() + } + }) + }); + // Remove the file from the current view since it's been restored currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId)); renderFiles(currentFiles); @@ -434,7 +470,7 @@ function permanentDeleteFile() { return; } - fetch(`/api/rooms/${roomId}/delete-permanent`, { + fetch(`/api/rooms/${roomId}/delete_permanent`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -445,39 +481,40 @@ function permanentDeleteFile() { path: path }) }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - // Check if the response is empty - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { - return response.json(); - } - return { success: true }; // If no JSON response, assume success - }) + .then(r => r.json()) .then(res => { if (res.success) { - // Remove the file from the current view since it's been deleted + // Log the permanent delete event + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ + event_type: 'file_delete_permanent', + details: { + filename: filename, + path: path, + room_id: roomId, + timestamp: new Date().toISOString() + } + }) + }); + + // Remove the file from the current view currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId)); renderFiles(currentFiles); + // Close the modal const modal = bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal')); - if (modal) { - modal.hide(); - } + modal.hide(); } else { - console.error('Failed to delete file:', res.error || 'Unknown error'); + console.error('Failed to delete file permanently:', res.error); } }) .catch(error => { - console.error('Error deleting file:', error); - // Show error to user - const modal = bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal')); - if (modal) { - modal.hide(); - } - // You might want to show an error message to the user here + console.error('Error deleting file permanently:', error); }); } diff --git a/static/js/room-members.js b/static/js/room-members.js index 198174c..49ef5bf 100644 --- a/static/js/room-members.js +++ b/static/js/room-members.js @@ -13,19 +13,95 @@ * - Auto-save functionality for permission changes * @function */ -$(document).ready(function() { - // Initialize Select2 for user selection +document.addEventListener('DOMContentLoaded', function() { + // Initialize Select2 $('.select2').select2({ - theme: 'bootstrap-5', - width: '100%', - placeholder: 'Search for a user...', - allowClear: true + theme: 'bootstrap-5' }); - - // Auto-submit permission form on checkbox change - document.querySelectorAll('.auto-save-perms-form input[type="checkbox"]').forEach(function(checkbox) { - checkbox.addEventListener('change', function() { - this.closest('form').submit(); + + // Log when a member is added to the room + const addMemberForm = document.querySelector('form[action*="/add_member"]'); + if (addMemberForm) { + addMemberForm.addEventListener('submit', function(e) { + const formData = new FormData(this); + const userId = formData.get('user_id'); + const roomId = this.action.split('/rooms/')[1].split('/add_member')[0]; + + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_join', + details: { + room_id: roomId, + user_id: userId, + timestamp: new Date().toISOString() + } + }) + }); + }); + } + + // Log when a member is removed from the room + const removeMemberForms = document.querySelectorAll('form[action*="/remove_member"]'); + removeMemberForms.forEach(form => { + form.addEventListener('submit', function(e) { + const roomId = this.action.split('/rooms/')[1].split('/remove_member')[0]; + const userId = this.action.split('/users/')[1]; + + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_leave', + details: { + room_id: roomId, + user_id: userId, + timestamp: new Date().toISOString() + } + }) + }); + }); + }); + + // Log when member permissions are updated + const permissionForms = document.querySelectorAll('.auto-save-perms-form'); + permissionForms.forEach(form => { + form.addEventListener('change', function(e) { + const roomId = this.action.split('/rooms/')[1].split('/update_member_permissions')[0]; + const userId = this.action.split('/users/')[1]; + const formData = new FormData(this); + + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_member_permissions_update', + details: { + room_id: roomId, + user_id: userId, + permissions: { + can_view: formData.get('can_view') === '1', + can_download: formData.get('can_download') === 'on', + can_upload: formData.get('can_upload') === 'on', + can_delete: formData.get('can_delete') === 'on', + can_rename: formData.get('can_rename') === 'on', + can_move: formData.get('can_move') === 'on', + can_share: formData.get('can_share') === 'on' + }, + timestamp: new Date().toISOString() + } + }) + }); }); }); }); \ No newline at end of file diff --git a/static/js/rooms-list.js b/static/js/rooms-list.js index 75068ad..271fa5c 100644 --- a/static/js/rooms-list.js +++ b/static/js/rooms-list.js @@ -46,4 +46,137 @@ document.addEventListener('DOMContentLoaded', function() { form.submit(); }); } + + // Log when rooms page is viewed + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_list_view', + details: { + timestamp: new Date().toISOString() + } + }) + }); + + // Log when a room is opened + const openRoomButtons = document.querySelectorAll('a[href*="/room/"]'); + openRoomButtons.forEach(button => { + button.addEventListener('click', function(e) { + const roomId = this.href.split('/room/')[1]; + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_open', + details: { + room_id: roomId, + timestamp: new Date().toISOString() + } + }) + }); + }); + }); + + // Log when a room is deleted + const deleteRoomForms = document.querySelectorAll('form[action*="/delete"]'); + deleteRoomForms.forEach(form => { + form.addEventListener('submit', function(e) { + const roomId = this.action.split('/rooms/')[1].split('/delete')[0]; + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_delete', + details: { + room_id: roomId, + timestamp: new Date().toISOString() + } + }) + }); + }); + }); + + // Log when a room is created + const createRoomForm = document.querySelector('form[action*="/create"]'); + if (createRoomForm) { + createRoomForm.addEventListener('submit', function(e) { + const formData = new FormData(this); + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_create', + details: { + room_name: formData.get('name'), + description: formData.get('description'), + timestamp: new Date().toISOString() + } + }) + }); + }); + } + + // Log when a room is edited + const editRoomForm = document.querySelector('form[action*="/edit"]'); + if (editRoomForm) { + editRoomForm.addEventListener('submit', function(e) { + const roomId = this.action.split('/rooms/')[1].split('/edit')[0]; + const formData = new FormData(this); + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_update', + details: { + room_id: roomId, + room_name: formData.get('name'), + description: formData.get('description'), + timestamp: new Date().toISOString() + } + }) + }); + }); + } + + // Log when room search is performed + if (searchInput) { + let searchTimeout; + searchInput.addEventListener('input', function(e) { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + if (this.value.length > 0) { + fetch('/api/events/log', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + event_type: 'room_search', + details: { + search_term: this.value, + timestamp: new Date().toISOString() + } + }) + }); + } + }, 500); + }); + } }); \ No newline at end of file diff --git a/templates/settings/settings.html b/templates/settings/settings.html index d75f4b9..5af4a76 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 %} @@ -36,6 +37,11 @@ Company Info +