Added events system
This commit is contained in:
Binary file not shown.
1
app.py
1
app.py
@@ -25,6 +25,7 @@ def create_app():
|
|||||||
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secure-secret-key-here')
|
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['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['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
|
# Initialize extensions
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|||||||
41
models.py
41
models.py
@@ -205,46 +205,27 @@ class MessageAttachment(db.Model):
|
|||||||
return f'<MessageAttachment {self.name}>'
|
return f'<MessageAttachment {self.name}>'
|
||||||
|
|
||||||
class EventType(Enum):
|
class EventType(Enum):
|
||||||
# User events
|
|
||||||
USER_LOGIN = 'user_login'
|
USER_LOGIN = 'user_login'
|
||||||
USER_LOGOUT = 'user_logout'
|
USER_LOGOUT = 'user_logout'
|
||||||
USER_CREATE = 'user_create'
|
USER_REGISTER = 'user_register'
|
||||||
USER_UPDATE = 'user_update'
|
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_UPLOAD = 'file_upload'
|
||||||
FILE_DOWNLOAD = 'file_download'
|
|
||||||
FILE_DELETE = 'file_delete'
|
FILE_DELETE = 'file_delete'
|
||||||
FILE_RENAME = 'file_rename'
|
FILE_DOWNLOAD = 'file_download'
|
||||||
|
FILE_RESTORE = 'file_restore'
|
||||||
FILE_MOVE = 'file_move'
|
FILE_MOVE = 'file_move'
|
||||||
|
FILE_RENAME = 'file_rename'
|
||||||
FILE_STAR = 'file_star'
|
FILE_STAR = 'file_star'
|
||||||
FILE_UNSTAR = 'file_unstar'
|
FILE_UNSTAR = 'file_unstar'
|
||||||
|
ROOM_CREATE = 'room_create'
|
||||||
# Conversation events
|
ROOM_DELETE = 'room_delete'
|
||||||
|
ROOM_UPDATE = 'room_update'
|
||||||
|
ROOM_JOIN = 'room_join'
|
||||||
|
ROOM_LEAVE = 'room_leave'
|
||||||
CONVERSATION_CREATE = 'conversation_create'
|
CONVERSATION_CREATE = 'conversation_create'
|
||||||
CONVERSATION_UPDATE = 'conversation_update'
|
|
||||||
CONVERSATION_DELETE = 'conversation_delete'
|
CONVERSATION_DELETE = 'conversation_delete'
|
||||||
CONVERSATION_MEMBER_ADD = 'conversation_member_add'
|
MESSAGE_SENT = 'message_sent'
|
||||||
CONVERSATION_MEMBER_REMOVE = 'conversation_member_remove'
|
ATTACHMENT_DOWNLOAD = 'attachment_download'
|
||||||
|
|
||||||
# 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):
|
class Event(db.Model):
|
||||||
__tablename__ = 'events'
|
__tablename__ = 'events'
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 flask_login import login_user, logout_user, login_required, current_user
|
||||||
from models import db, User
|
from models import db, User
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from utils.event_logger import log_event
|
||||||
|
from models import EventType
|
||||||
|
|
||||||
def require_password_change(f):
|
def require_password_change(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
@@ -31,6 +33,13 @@ def init_routes(auth_bp):
|
|||||||
|
|
||||||
login_user(user, remember=remember)
|
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
|
# Check if user is using default password
|
||||||
if password == 'changeme':
|
if password == 'changeme':
|
||||||
flash('Please change your password before continuing.', 'warning')
|
flash('Please change your password before continuing.', 'warning')
|
||||||
@@ -69,6 +78,13 @@ def init_routes(auth_bp):
|
|||||||
db.session.add(new_user)
|
db.session.add(new_user)
|
||||||
db.session.commit()
|
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)
|
login_user(new_user)
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
@@ -77,6 +93,11 @@ def init_routes(auth_bp):
|
|||||||
@auth_bp.route('/logout')
|
@auth_bp.route('/logout')
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout():
|
||||||
|
# Log logout before actually logging out
|
||||||
|
log_event(
|
||||||
|
event_type=EventType.USER_LOGOUT,
|
||||||
|
user_id=current_user.id
|
||||||
|
)
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
@@ -98,6 +119,14 @@ def init_routes(auth_bp):
|
|||||||
|
|
||||||
current_user.set_password(new_password)
|
current_user.set_password(new_password)
|
||||||
db.session.commit()
|
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')
|
flash('Password changed successfully!', 'success')
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
|
||||||
from flask_login import login_required, current_user
|
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 forms import ConversationForm
|
||||||
from routes.auth import require_password_change
|
from routes.auth import require_password_change
|
||||||
|
from utils.event_logger import log_event
|
||||||
import os
|
import os
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -84,6 +85,17 @@ def create_conversation():
|
|||||||
db.session.add(conversation)
|
db.session.add(conversation)
|
||||||
db.session.commit()
|
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')
|
flash('Conversation created successfully!', 'success')
|
||||||
return redirect(url_for('conversations.conversations'))
|
return redirect(url_for('conversations.conversations'))
|
||||||
return render_template('conversations/create_conversation.html', form=form)
|
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)
|
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
|
# Delete all messages in the conversation
|
||||||
Message.query.filter_by(conversation_id=conversation_id).delete()
|
Message.query.filter_by(conversation_id=conversation_id).delete()
|
||||||
|
|
||||||
@@ -324,6 +348,18 @@ def send_message(conversation_id):
|
|||||||
|
|
||||||
db.session.commit()
|
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
|
# Prepare message data for response
|
||||||
message_data = {
|
message_data = {
|
||||||
'id': message.id,
|
'id': message.id,
|
||||||
@@ -358,6 +394,19 @@ def download_attachment(message_id, attachment_index):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
attachment = message.attachments[attachment_index]
|
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(
|
return send_file(
|
||||||
attachment.path,
|
attachment.path,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
|
|||||||
155
routes/main.py
155
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 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 routes.auth import require_password_change
|
||||||
|
from utils.event_logger import log_event
|
||||||
import os
|
import os
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from sqlalchemy import func, case, literal_column, text
|
from sqlalchemy import func, case, literal_column, text
|
||||||
@@ -279,6 +280,14 @@ def init_routes(main_bp):
|
|||||||
os.remove(old_picture_path)
|
os.remove(old_picture_path)
|
||||||
current_user.profile_picture = None
|
current_user.profile_picture = None
|
||||||
db.session.commit()
|
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')
|
flash('Profile picture removed successfully!', 'success')
|
||||||
return redirect(url_for('main.profile'))
|
return redirect(url_for('main.profile'))
|
||||||
|
|
||||||
@@ -289,6 +298,10 @@ def init_routes(main_bp):
|
|||||||
if existing_user:
|
if existing_user:
|
||||||
flash('A user with this email already exists.', 'error')
|
flash('A user with this email already exists.', 'error')
|
||||||
return render_template('profile/profile.html')
|
return render_template('profile/profile.html')
|
||||||
|
|
||||||
|
# Track changes for event logging
|
||||||
|
changes = {}
|
||||||
|
|
||||||
# Handle profile picture upload
|
# Handle profile picture upload
|
||||||
file = request.files.get('profile_picture')
|
file = request.files.get('profile_picture')
|
||||||
if file and file.filename:
|
if file and file.filename:
|
||||||
@@ -296,14 +309,31 @@ def init_routes(main_bp):
|
|||||||
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||||
file.save(file_path)
|
file.save(file_path)
|
||||||
current_user.profile_picture = filename
|
current_user.profile_picture = filename
|
||||||
|
changes['profile_picture'] = True
|
||||||
|
|
||||||
# Update user information
|
# Update user information
|
||||||
|
if current_user.username != request.form.get('first_name'):
|
||||||
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')
|
current_user.last_name = request.form.get('last_name')
|
||||||
|
changes['last_name'] = True
|
||||||
|
if current_user.email != new_email:
|
||||||
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')
|
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')
|
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')
|
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')
|
current_user.notes = request.form.get('notes')
|
||||||
|
changes['notes'] = True
|
||||||
|
|
||||||
# Handle password change if provided
|
# Handle password change if provided
|
||||||
new_password = request.form.get('new_password')
|
new_password = request.form.get('new_password')
|
||||||
confirm_password = request.form.get('confirm_password')
|
confirm_password = request.form.get('confirm_password')
|
||||||
@@ -312,9 +342,20 @@ def init_routes(main_bp):
|
|||||||
flash('Passwords do not match.', 'error')
|
flash('Passwords do not match.', 'error')
|
||||||
return render_template('profile/profile.html')
|
return render_template('profile/profile.html')
|
||||||
current_user.set_password(new_password)
|
current_user.set_password(new_password)
|
||||||
|
changes['password'] = True
|
||||||
flash('Password updated successfully.', 'success')
|
flash('Password updated successfully.', 'success')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.session.commit()
|
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')
|
flash('Profile updated successfully!', 'success')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
@@ -355,11 +396,18 @@ def init_routes(main_bp):
|
|||||||
|
|
||||||
site_settings = SiteSettings.get_settings()
|
site_settings = SiteSettings.get_settings()
|
||||||
active_tab = request.args.get('tab', 'colors')
|
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',
|
return render_template('settings/settings.html',
|
||||||
primary_color=site_settings.primary_color,
|
primary_color=site_settings.primary_color,
|
||||||
secondary_color=site_settings.secondary_color,
|
secondary_color=site_settings.secondary_color,
|
||||||
active_tab=active_tab,
|
active_tab=active_tab,
|
||||||
site_settings=site_settings)
|
site_settings=site_settings,
|
||||||
|
events=events)
|
||||||
|
|
||||||
@main_bp.route('/settings/colors', methods=['POST'])
|
@main_bp.route('/settings/colors', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -531,3 +579,104 @@ def init_routes(main_bp):
|
|||||||
logger.info(f"[Dynamic Colors] Cache version: {site_settings.updated_at.timestamp()}")
|
logger.info(f"[Dynamic Colors] Cache version: {site_settings.updated_at.timestamp()}")
|
||||||
|
|
||||||
return Response(css, mimetype='text/css')
|
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]
|
||||||
|
})
|
||||||
272
static/js/events.js
Normal file
272
static/js/events.js
Normal file
@@ -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 `
|
||||||
|
<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();
|
||||||
|
});
|
||||||
@@ -354,6 +354,24 @@ function toggleStar(filename, path = '', roomId) {
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.success) {
|
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
|
// 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));
|
currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
|
||||||
renderFiles(currentFiles);
|
renderFiles(currentFiles);
|
||||||
@@ -394,6 +412,24 @@ function restoreFile(filename, path = '', roomId) {
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.success) {
|
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
|
// 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));
|
currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
|
||||||
renderFiles(currentFiles);
|
renderFiles(currentFiles);
|
||||||
@@ -434,7 +470,7 @@ function permanentDeleteFile() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(`/api/rooms/${roomId}/delete-permanent`, {
|
fetch(`/api/rooms/${roomId}/delete_permanent`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -445,39 +481,40 @@ function permanentDeleteFile() {
|
|||||||
path: path
|
path: path
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(r => r.json())
|
||||||
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 => {
|
.then(res => {
|
||||||
if (res.success) {
|
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));
|
currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
|
||||||
renderFiles(currentFiles);
|
renderFiles(currentFiles);
|
||||||
|
|
||||||
// Close the modal
|
// Close the modal
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal'));
|
const modal = bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal'));
|
||||||
if (modal) {
|
|
||||||
modal.hide();
|
modal.hide();
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to delete file:', res.error || 'Unknown error');
|
console.error('Failed to delete file permanently:', res.error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error deleting file:', error);
|
console.error('Error deleting file permanently:', 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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,19 +13,95 @@
|
|||||||
* - Auto-save functionality for permission changes
|
* - Auto-save functionality for permission changes
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
$(document).ready(function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize Select2 for user selection
|
// Initialize Select2
|
||||||
$('.select2').select2({
|
$('.select2').select2({
|
||||||
theme: 'bootstrap-5',
|
theme: 'bootstrap-5'
|
||||||
width: '100%',
|
|
||||||
placeholder: 'Search for a user...',
|
|
||||||
allowClear: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-submit permission form on checkbox change
|
// Log when a member is added to the room
|
||||||
document.querySelectorAll('.auto-save-perms-form input[type="checkbox"]').forEach(function(checkbox) {
|
const addMemberForm = document.querySelector('form[action*="/add_member"]');
|
||||||
checkbox.addEventListener('change', function() {
|
if (addMemberForm) {
|
||||||
this.closest('form').submit();
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -46,4 +46,137 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
form.submit();
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
{% from "settings/tabs/company_info.html" import company_info_tab %}
|
{% from "settings/tabs/company_info.html" import company_info_tab %}
|
||||||
{% from "settings/tabs/security.html" import security_tab %}
|
{% from "settings/tabs/security.html" import security_tab %}
|
||||||
{% from "settings/tabs/debugging.html" import debugging_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 %}
|
{% from "settings/components/reset_colors_modal.html" import reset_colors_modal %}
|
||||||
|
|
||||||
{% block title %}Settings - DocuPulse{% endblock %}
|
{% block title %}Settings - DocuPulse{% endblock %}
|
||||||
@@ -36,6 +37,11 @@
|
|||||||
<i class="fas fa-building me-2"></i>Company Info
|
<i class="fas fa-building me-2"></i>Company Info
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</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">
|
<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' }}">
|
<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
|
<i class="fas fa-shield-alt me-2"></i>Security
|
||||||
@@ -60,6 +66,11 @@
|
|||||||
{{ company_info_tab(site_settings, csrf_token) }}
|
{{ company_info_tab(site_settings, csrf_token) }}
|
||||||
</div>
|
</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 -->
|
<!-- Security Tab -->
|
||||||
<div class="tab-pane fade {% if active_tab == 'security' %}show active{% endif %}" id="security" role="tabpanel" aria-labelledby="security-tab">
|
<div class="tab-pane fade {% if active_tab == 'security' %}show active{% endif %}" id="security" role="tabpanel" aria-labelledby="security-tab">
|
||||||
{{ security_tab() }}
|
{{ security_tab() }}
|
||||||
@@ -80,5 +91,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="{{ url_for('static', filename='js/settings.js', v=config.CSS_VERSION) }}"></script>
|
<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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
165
templates/settings/tabs/events.html
Normal file
165
templates/settings/tabs/events.html
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -43,4 +43,93 @@
|
|||||||
{% block extra_js %}
|
{% 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/file-grid.js', v=config.CSS_VERSION) }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/starred.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 %}
|
{% endblock %}
|
||||||
@@ -49,4 +49,66 @@
|
|||||||
{% block extra_js %}
|
{% 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/file-grid.js', v=config.CSS_VERSION) }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/trash.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 %}
|
{% endblock %}
|
||||||
18
utils/__init__.py
Normal file
18
utils/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
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'
|
||||||
|
]
|
||||||
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/event_logger.cpython-313.pyc
Normal file
BIN
utils/__pycache__/event_logger.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/helpers.cpython-313.pyc
Normal file
BIN
utils/__pycache__/helpers.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/permissions.cpython-313.pyc
Normal file
BIN
utils/__pycache__/permissions.cpython-313.pyc
Normal file
Binary file not shown.
@@ -1,39 +1,3 @@
|
|||||||
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):
|
def clean_path(path):
|
||||||
"""
|
"""
|
||||||
Clean a file path by removing leading/trailing slashes and normalizing separators.
|
Clean a file path by removing leading/trailing slashes and normalizing separators.
|
||||||
35
utils/permissions.py
Normal file
35
utils/permissions.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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)
|
||||||
Reference in New Issue
Block a user