Revert "Added events system"

This reverts commit f00d569db3.
This commit is contained in:
2025-05-29 14:45:52 +02:00
parent f00d569db3
commit 6d959ac253
24 changed files with 114 additions and 1186 deletions

Binary file not shown.

1
app.py
View File

@@ -25,7 +25,6 @@ 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)

View File

@@ -205,27 +205,46 @@ class MessageAttachment(db.Model):
return f'<MessageAttachment {self.name}>'
class EventType(Enum):
# User events
USER_LOGIN = 'user_login'
USER_LOGOUT = 'user_logout'
USER_REGISTER = 'user_register'
USER_CREATE = 'user_create'
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_DELETE = 'file_delete'
FILE_DOWNLOAD = 'file_download'
FILE_RESTORE = 'file_restore'
FILE_MOVE = 'file_move'
FILE_DELETE = 'file_delete'
FILE_RENAME = 'file_rename'
FILE_MOVE = 'file_move'
FILE_STAR = 'file_star'
FILE_UNSTAR = 'file_unstar'
ROOM_CREATE = 'room_create'
ROOM_DELETE = 'room_delete'
ROOM_UPDATE = 'room_update'
ROOM_JOIN = 'room_join'
ROOM_LEAVE = 'room_leave'
# Conversation events
CONVERSATION_CREATE = 'conversation_create'
CONVERSATION_UPDATE = 'conversation_update'
CONVERSATION_DELETE = 'conversation_delete'
MESSAGE_SENT = 'message_sent'
ATTACHMENT_DOWNLOAD = 'attachment_download'
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'
class Event(db.Model):
__tablename__ = 'events'

View File

@@ -2,8 +2,6 @@ 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)
@@ -33,13 +31,6 @@ 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')
@@ -78,13 +69,6 @@ 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'))
@@ -93,11 +77,6 @@ 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'))
@@ -119,14 +98,6 @@ 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'))

View File

@@ -1,9 +1,8 @@
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, EventType
from models import db, Conversation, User, Message, MessageAttachment
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
@@ -85,17 +84,6 @@ 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)
@@ -239,18 +227,6 @@ 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()
@@ -348,18 +324,6 @@ 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,
@@ -394,19 +358,6 @@ 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,

View File

@@ -1,8 +1,7 @@
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response
from flask_login import current_user, login_required
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, EventType
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings
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
@@ -280,14 +279,6 @@ 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'))
@@ -298,10 +289,6 @@ 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:
@@ -309,31 +296,14 @@ 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
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
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')
# Handle password change if provided
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
@@ -342,20 +312,9 @@ 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()
@@ -396,18 +355,11 @@ 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,
events=events)
site_settings=site_settings)
@main_bp.route('/settings/colors', methods=['POST'])
@login_required
@@ -578,105 +530,4 @@ 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')
@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/<int:event_id>')
@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]
})
return Response(css, mimetype='text/css')

View File

@@ -1,272 +0,0 @@
/**
* @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 `
<tr>
<td>${timestamp}</td>
<td><span class="badge ${badgeClass}">${eventText}</span></td>
<td>${username} ${lastName}</td>
<td>
<button class="btn btn-sm btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#eventDetailsModal"
data-event-id="${eventId}">
<i class="fas fa-info-circle"></i> View Details
</button>
</td>
<td>${ipAddress}</td>
</tr>
`;
}).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 = '<option value="">All Users</option>';
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();
});

View File

@@ -354,24 +354,6 @@ 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);
@@ -412,24 +394,6 @@ 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);
@@ -470,7 +434,7 @@ function permanentDeleteFile() {
return;
}
fetch(`/api/rooms/${roomId}/delete_permanent`, {
fetch(`/api/rooms/${roomId}/delete-permanent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -481,40 +445,39 @@ function permanentDeleteFile() {
path: path
})
})
.then(r => r.json())
.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(res => {
if (res.success) {
// 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
// Remove the file from the current view since it's been deleted
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'));
modal.hide();
if (modal) {
modal.hide();
}
} else {
console.error('Failed to delete file permanently:', res.error);
console.error('Failed to delete file:', res.error || 'Unknown error');
}
})
.catch(error => {
console.error('Error deleting file permanently:', 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
});
}

View File

@@ -13,95 +13,19 @@
* - Auto-save functionality for permission changes
* @function
*/
document.addEventListener('DOMContentLoaded', function() {
// Initialize Select2
$(document).ready(function() {
// Initialize Select2 for user selection
$('.select2').select2({
theme: 'bootstrap-5'
theme: 'bootstrap-5',
width: '100%',
placeholder: 'Search for a user...',
allowClear: true
});
// 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()
}
})
});
// 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();
});
});
});

View File

@@ -46,137 +46,4 @@ 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);
});
}
});

View File

@@ -4,7 +4,6 @@
{% 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 %}
@@ -37,11 +36,6 @@
<i class="fas fa-building me-2"></i>Company Info
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'events' %}active{% endif %}" id="events-tab" data-bs-toggle="tab" data-bs-target="#events" type="button" role="tab" aria-controls="events" aria-selected="{{ 'true' if active_tab == 'events' else 'false' }}">
<i class="fas fa-history me-2"></i>Event Log
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'security' %}active{% endif %}" id="security-tab" data-bs-toggle="tab" data-bs-target="#security" type="button" role="tab" aria-controls="security" aria-selected="{{ 'true' if active_tab == 'security' else 'false' }}">
<i class="fas fa-shield-alt me-2"></i>Security
@@ -66,11 +60,6 @@
{{ company_info_tab(site_settings, csrf_token) }}
</div>
<!-- Events Tab -->
<div class="tab-pane fade {% if active_tab == 'events' %}show active{% endif %}" id="events" role="tabpanel" aria-labelledby="events-tab">
{{ events_tab(events, csrf_token) }}
</div>
<!-- Security Tab -->
<div class="tab-pane fade {% if active_tab == 'security' %}show active{% endif %}" id="security" role="tabpanel" aria-labelledby="security-tab">
{{ security_tab() }}
@@ -91,6 +80,5 @@
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/settings.js', v=config.JS_VERSION) }}"></script>
<script src="{{ url_for('static', filename='js/events.js', v=config.JS_VERSION) }}"></script>
<script src="{{ url_for('static', filename='js/settings.js', v=config.CSS_VERSION) }}"></script>
{% endblock %}

View File

@@ -1,165 +0,0 @@
{% macro events_tab(events, csrf_token) %}
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">Event Log</h5>
<div class="d-flex gap-2">
<select id="eventTypeFilter" class="form-select form-select-sm">
<option value="">All Event Types</option>
<option value="user_login">User Login</option>
<option value="user_logout">User Logout</option>
<option value="user_register">User Registration</option>
<option value="user_update">User Update</option>
<option value="file_upload">File Upload</option>
<option value="file_delete">File Delete</option>
<option value="file_download">File Download</option>
<option value="file_restore">File Restore</option>
<option value="file_move">File Move</option>
<option value="file_rename">File Rename</option>
<option value="file_star">File Star</option>
<option value="file_unstar">File Unstar</option>
<option value="file_delete_permanent">File Delete Permanent</option>
<option value="room_create">Room Create</option>
<option value="room_delete">Room Delete</option>
<option value="room_update">Room Update</option>
<option value="room_open">Room Open</option>
<option value="room_list_view">Room List View</option>
<option value="room_search">Room Search</option>
<option value="room_join">Room Join</option>
<option value="room_leave">Room Leave</option>
<option value="room_member_permissions_update">Room Member Permissions Update</option>
<option value="conversation_create">Conversation Create</option>
<option value="conversation_delete">Conversation Delete</option>
<option value="message_sent">Message Sent</option>
<option value="attachment_download">Attachment Download</option>
</select>
<select id="dateRangeFilter" class="form-select form-select-sm">
<option value="24h">Last 24 Hours</option>
<option value="7d">Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="all">All Time</option>
</select>
<select id="userFilter" class="form-select form-select-sm">
<option value="">All Users</option>
</select>
<button id="applyFilters" class="btn btn-primary btn-sm">Apply Filters</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Timestamp</th>
<th>Event Type</th>
<th>User</th>
<th>Details</th>
<th>IP Address</th>
</tr>
</thead>
<tbody id="eventsTableBody">
{% for event in events %}
<tr>
<td>{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>
{% if event.event_type == 'user_login' %}
<span class="badge bg-success">User Login</span>
{% elif event.event_type == 'user_logout' %}
<span class="badge bg-secondary">User Logout</span>
{% elif event.event_type == 'user_register' %}
<span class="badge bg-info">User Registration</span>
{% elif event.event_type == 'user_update' %}
<span class="badge bg-primary">User Update</span>
{% elif event.event_type == 'file_upload' %}
<span class="badge bg-success">File Upload</span>
{% elif event.event_type == 'file_delete' %}
<span class="badge bg-danger">File Delete</span>
{% elif event.event_type == 'file_download' %}
<span class="badge bg-info">File Download</span>
{% elif event.event_type == 'file_restore' %}
<span class="badge bg-warning">File Restore</span>
{% elif event.event_type == 'file_move' %}
<span class="badge bg-primary">File Move</span>
{% elif event.event_type == 'file_rename' %}
<span class="badge bg-info">File Rename</span>
{% elif event.event_type == 'file_star' %}
<span class="badge bg-warning">File Star</span>
{% elif event.event_type == 'file_unstar' %}
<span class="badge bg-secondary">File Unstar</span>
{% elif event.event_type == 'file_delete_permanent' %}
<span class="badge bg-danger">File Delete Permanent</span>
{% elif event.event_type == 'room_create' %}
<span class="badge bg-success">Room Create</span>
{% elif event.event_type == 'room_delete' %}
<span class="badge bg-danger">Room Delete</span>
{% elif event.event_type == 'room_update' %}
<span class="badge bg-primary">Room Update</span>
{% elif event.event_type == 'room_open' %}
<span class="badge bg-info">Room Open</span>
{% elif event.event_type == 'room_list_view' %}
<span class="badge bg-secondary">Room List View</span>
{% elif event.event_type == 'room_search' %}
<span class="badge bg-info">Room Search</span>
{% elif event.event_type == 'room_join' %}
<span class="badge bg-info">Room Join</span>
{% elif event.event_type == 'room_leave' %}
<span class="badge bg-secondary">Room Leave</span>
{% elif event.event_type == 'room_member_permissions_update' %}
<span class="badge bg-primary">Room Member Permissions Update</span>
{% elif event.event_type == 'conversation_create' %}
<span class="badge bg-success">Conversation Create</span>
{% elif event.event_type == 'conversation_delete' %}
<span class="badge bg-danger">Conversation Delete</span>
{% elif event.event_type == 'message_sent' %}
<span class="badge bg-primary">Message Sent</span>
{% elif event.event_type == 'attachment_download' %}
<span class="badge bg-info">Attachment Download</span>
{% else %}
<span class="badge bg-secondary">{{ event.event_type }}</span>
{% endif %}
</td>
<td>{{ event.user.username }} {% if event.user.last_name %}{{ event.user.last_name }}{% endif %}</td>
<td>
<button class="btn btn-sm btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#eventDetailsModal"
data-event-id="{{ event.id }}">
<i class="fas fa-info-circle"></i> View Details
</button>
</td>
<td>{{ event.ip_address or '-' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
<button id="prevPage" class="btn btn-outline-primary btn-sm">Previous</button>
<span class="mx-2">Page <span id="currentPage">1</span> of <span id="totalPages">1</span></span>
<button id="nextPage" class="btn btn-outline-primary btn-sm">Next</button>
</div>
</div>
</div>
</div>
<!-- Event Details Modal -->
<div class="modal fade" id="eventDetailsModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Event Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<pre id="eventDetailsContent" class="bg-light p-3 rounded"></pre>
</div>
</div>
</div>
</div>
{% endmacro %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/events.js', v=config.JS_VERSION) }}"></script>
{% endblock %}

View File

@@ -43,93 +43,4 @@
{% block extra_js %}
<script src="{{ url_for('static', filename='js/file-grid.js', v=config.CSS_VERSION) }}"></script>
<script src="{{ url_for('static', filename='js/starred.js', v=config.CSS_VERSION) }}"></script>
<script>
// Add event logging for starred actions
document.addEventListener('DOMContentLoaded', function() {
// Log when starred page is viewed
fetch('/api/events/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({
event_type: 'starred_view',
details: {
user_id: '{{ current_user.id }}',
timestamp: new Date().toISOString()
}
})
});
// Log when view is toggled
const gridViewBtn = document.getElementById('gridViewBtn');
const listViewBtn = document.getElementById('listViewBtn');
if (gridViewBtn && listViewBtn) {
gridViewBtn.addEventListener('click', function() {
logViewChange('grid');
});
listViewBtn.addEventListener('click', function() {
logViewChange('list');
});
}
function logViewChange(viewType) {
fetch('/api/events/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({
event_type: 'starred_view_change',
details: {
user_id: '{{ current_user.id }}',
view_type: viewType,
timestamp: new Date().toISOString()
}
})
});
}
// Log when search is performed
const searchInput = document.querySelector('.search-input');
if (searchInput) {
searchInput.addEventListener('input', debounce(function(e) {
if (e.target.value.length > 0) {
fetch('/api/events/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({
event_type: 'starred_search',
details: {
user_id: '{{ current_user.id }}',
search_term: e.target.value,
timestamp: new Date().toISOString()
}
})
});
}
}, 500));
}
// Debounce function to limit API calls
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
});
</script>
{% endblock %}

View File

@@ -49,66 +49,4 @@
{% block extra_js %}
<script src="{{ url_for('static', filename='js/file-grid.js', v=config.CSS_VERSION) }}"></script>
<script src="{{ url_for('static', filename='js/trash.js', v=config.CSS_VERSION) }}"></script>
<script>
// Add event logging for trash actions
document.addEventListener('DOMContentLoaded', function() {
// Log when trash page is viewed
fetch('/api/events/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({
event_type: 'trash_view',
details: {
user_id: '{{ current_user.id }}',
timestamp: new Date().toISOString()
}
})
});
// Log when empty trash is clicked
const emptyTrashBtn = document.querySelector('.header-button');
if (emptyTrashBtn) {
emptyTrashBtn.addEventListener('click', function() {
fetch('/api/events/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({
event_type: 'trash_empty_click',
details: {
user_id: '{{ current_user.id }}',
timestamp: new Date().toISOString()
}
})
});
});
}
// Log when trash is actually emptied
const confirmEmptyTrashBtn = document.getElementById('confirmEmptyTrash');
if (confirmEmptyTrashBtn) {
confirmEmptyTrashBtn.addEventListener('click', function() {
fetch('/api/events/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({
event_type: 'trash_emptied',
details: {
user_id: '{{ current_user.id }}',
timestamp: new Date().toISOString()
}
})
});
});
}
});
</script>
{% endblock %}

View File

@@ -1,3 +1,39 @@
from flask_login import current_user
from models import RoomMemberPermission
from datetime import datetime, timezone
def user_has_permission(room, perm_name):
"""
Check if the current user has a specific permission in a room.
Args:
room: Room object
perm_name: Name of the permission to check (e.g., 'can_view', 'can_upload')
Returns:
bool: True if user has permission, False otherwise
"""
# Admin users have all permissions
if current_user.is_admin:
return True
# Check if user is a member of the room
if current_user not in room.members:
return False
# Get user's permissions for this room
permission = RoomMemberPermission.query.filter_by(
room_id=room.id,
user_id=current_user.id
).first()
# If no specific permissions are set, user only has view access
if not permission:
return perm_name == 'can_view'
# Check the specific permission
return getattr(permission, perm_name, False)
def clean_path(path):
"""
Clean a file path by removing leading/trailing slashes and normalizing separators.

View File

@@ -1,18 +0,0 @@
"""
Utils package for DocuPulse.
Contains utility functions for various features including event logging.
"""
from .permissions import user_has_permission
from .helpers import clean_path, timeago
from .event_logger import log_event, get_user_events, get_room_events, get_recent_events
__all__ = [
'user_has_permission',
'clean_path',
'timeago',
'log_event',
'get_user_events',
'get_room_events',
'get_recent_events'
]

View File

@@ -1,35 +0,0 @@
from flask_login import current_user
from models import RoomMemberPermission
from datetime import datetime, timezone
def user_has_permission(room, perm_name):
"""
Check if the current user has a specific permission in a room.
Args:
room: Room object
perm_name: Name of the permission to check (e.g., 'can_view', 'can_upload')
Returns:
bool: True if user has permission, False otherwise
"""
# Admin users have all permissions
if current_user.is_admin:
return True
# Check if user is a member of the room
if current_user not in room.members:
return False
# Get user's permissions for this room
permission = RoomMemberPermission.query.filter_by(
room_id=room.id,
user_id=current_user.id
).first()
# If no specific permissions are set, user only has view access
if not permission:
return perm_name == 'can_view'
# Check the specific permission
return getattr(permission, perm_name, False)