user logging

This commit is contained in:
2025-05-29 22:33:05 +02:00
parent 5dbdd43785
commit 8f24e21d5d
9 changed files with 340 additions and 113 deletions

Binary file not shown.

View File

@@ -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 import log_event
from datetime import datetime
def require_password_change(f): def require_password_change(f):
@wraps(f) @wraps(f)
@@ -26,11 +28,26 @@ def init_routes(auth_bp):
user = User.query.filter_by(email=email).first() user = User.query.filter_by(email=email).first()
if not user or not user.check_password(password): if not user or not user.check_password(password):
# Log failed login attempt
log_event('user_login', {
'email': email,
'success': False,
'reason': 'invalid_credentials'
})
flash('Please check your login details and try again.', 'danger') flash('Please check your login details and try again.', 'danger')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
login_user(user, remember=remember) login_user(user, remember=remember)
# Log successful login
log_event('user_login', {
'user_id': user.id,
'email': email,
'success': True,
'remember': remember,
'using_default_password': password == 'changeme'
}, user.id)
# 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 +86,16 @@ def init_routes(auth_bp):
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
# Log user registration
log_event('user_register', {
'email': email,
'username': username,
'timestamp': datetime.utcnow().isoformat(),
'ip_address': request.remote_addr,
'user_agent': request.user_agent.string,
'registration_method': 'web_form'
}, new_user.id)
login_user(new_user) login_user(new_user)
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
@@ -77,6 +104,12 @@ def init_routes(auth_bp):
@auth_bp.route('/logout') @auth_bp.route('/logout')
@login_required @login_required
def logout(): def logout():
# Log logout event
log_event('user_logout', {
'user_id': current_user.id,
'email': current_user.email
}, current_user.id)
logout_user() logout_user()
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
@@ -98,6 +131,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('user_update', {
'user_id': current_user.id,
'email': current_user.email,
'update_type': 'password_change'
}, current_user.id)
flash('Password changed successfully!', 'success') flash('Password changed successfully!', 'success')
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))

View File

@@ -1,4 +1,4 @@
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, jsonify, session
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, Event from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event
from routes.auth import require_password_change from routes.auth import require_password_change
@@ -9,7 +9,6 @@ from datetime import datetime, timedelta
import logging import logging
import sys import sys
import time import time
from flask import session
# Set up logging to show in console # Set up logging to show in console
logging.basicConfig( logging.basicConfig(
@@ -356,11 +355,62 @@ 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 data if events tab is active
events = None
total_pages = 1
current_page = 1
users = []
if active_tab == 'events':
# Get filter parameters
event_type = request.args.get('event_type')
date_range = request.args.get('date_range', '7d')
user_id = request.args.get('user_id')
page = request.args.get('page', 1, type=int)
per_page = 50
# Calculate date range
end_date = datetime.utcnow()
if date_range == '24h':
start_date = end_date - timedelta(days=1)
elif date_range == '7d':
start_date = end_date - timedelta(days=7)
elif date_range == '30d':
start_date = end_date - timedelta(days=30)
else:
start_date = None
# Build query
query = Event.query
if event_type:
query = query.filter_by(event_type=event_type)
if start_date:
query = query.filter(Event.timestamp >= start_date)
if user_id:
query = query.filter_by(user_id=user_id)
# Get total count for pagination
total_events = query.count()
total_pages = (total_events + per_page - 1) // per_page
# Get paginated events
events = query.order_by(Event.timestamp.desc()).paginate(page=page, per_page=per_page)
# Get all users for filter dropdown
users = User.query.order_by(User.username).all()
return render_template('settings/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.items if events else None,
total_pages=total_pages,
current_page=current_page,
users=users,
csrf_token=session.get('csrf_token'))
@main_bp.route('/settings/colors', methods=['POST']) @main_bp.route('/settings/colors', methods=['POST'])
@login_required @login_required
@@ -578,13 +628,29 @@ def init_routes(main_bp):
# Get all users for filter dropdown # Get all users for filter dropdown
users = User.query.order_by(User.username).all() users = User.query.order_by(User.username).all()
return render_template('settings/tabs/events.html', # Check if this is an AJAX request
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
logger.info(f"Processing AJAX request for events. Found {len(events.items)} events")
return render_template('settings/tabs/events.html',
events=events.items,
total_pages=total_pages,
current_page=page,
event_type=event_type,
date_range=date_range,
user_id=user_id,
users=users,
csrf_token=session.get('csrf_token'))
# For full page requests, render the full settings page
site_settings = SiteSettings.get_settings()
return render_template('settings/settings.html',
primary_color=site_settings.primary_color,
secondary_color=site_settings.secondary_color,
active_tab='events',
site_settings=site_settings,
events=events.items, events=events.items,
total_pages=total_pages, total_pages=total_pages,
current_page=page, current_page=page,
event_type=event_type,
date_range=date_range,
user_id=user_id,
users=users, users=users,
csrf_token=session.get('csrf_token')) csrf_token=session.get('csrf_token'))
@@ -595,16 +661,33 @@ def init_routes(main_bp):
return jsonify({'error': 'Unauthorized'}), 403 return jsonify({'error': 'Unauthorized'}), 403
event = Event.query.get_or_404(event_id) event = Event.query.get_or_404(event_id)
return jsonify({ logger.info(f"Raw event object: {event}")
logger.info(f"Event details type: {type(event.details)}")
logger.info(f"Event details value: {event.details}")
# Convert details to dict if it's a string
details = event.details
if isinstance(details, str):
try:
import json
details = json.loads(details)
except json.JSONDecodeError:
details = {'raw_details': details}
# Return the raw event data
response_data = {
'id': event.id, 'id': event.id,
'event_type': event.event_type, 'event_type': event.event_type,
'timestamp': event.timestamp.isoformat(),
'user': { 'user': {
'id': event.user.id, 'id': event.user.id,
'username': event.user.username, 'username': event.user.username,
'last_name': event.user.last_name 'last_name': event.user.last_name
}, } if event.user else None,
'timestamp': event.timestamp.isoformat(),
'details': event.details,
'ip_address': event.ip_address, 'ip_address': event.ip_address,
'user_agent': event.user_agent 'user_agent': event.user_agent,
}) 'details': details
}
logger.info(f"Sending response: {response_data}")
return jsonify(response_data)

View File

@@ -41,15 +41,47 @@ document.addEventListener('DOMContentLoaded', function() {
// Function to load event details // Function to load event details
function loadEventDetails(eventId) { function loadEventDetails(eventId) {
console.log('Loading details for event:', eventId);
fetch(`/api/events/${eventId}`) fetch(`/api/events/${eventId}`)
.then(response => response.json()) .then(response => {
console.log('Response status:', response.status);
return response.json();
})
.then(data => { .then(data => {
const formattedDetails = JSON.stringify(data.details, null, 2); console.log('Received event data:', data);
eventDetailsContent.textContent = formattedDetails;
// Format the details for display
const formattedDetails = {
'Event ID': data.id,
'Event Type': data.event_type,
'Timestamp': new Date(data.timestamp).toLocaleString(),
'User': data.user ? `${data.user.username} (${data.user.last_name})` : 'N/A',
'IP Address': data.ip_address || 'N/A',
'User Agent': data.user_agent || 'N/A'
};
// Handle details separately
if (data.details) {
if (typeof data.details === 'object') {
formattedDetails['Details'] = JSON.stringify(data.details, null, 2);
} else {
formattedDetails['Details'] = data.details;
}
} else {
formattedDetails['Details'] = 'No additional details';
}
// Convert to formatted string
const detailsText = Object.entries(formattedDetails)
.map(([key, value]) => `${key}: ${value}`)
.join('\n\n');
console.log('Formatted details:', detailsText);
eventDetailsContent.textContent = detailsText;
}) })
.catch(error => { .catch(error => {
console.error('Error loading event details:', error); console.error('Error loading event details:', error);
eventDetailsContent.textContent = 'Error loading event details'; eventDetailsContent.textContent = 'Error loading event details. Please try again.';
}); });
} }

View File

@@ -13,9 +13,8 @@ document.addEventListener('DOMContentLoaded', function() {
const secondaryColorInput = document.getElementById('secondaryColor'); const secondaryColorInput = document.getElementById('secondaryColor');
const colorSettingsForm = document.getElementById('colorSettingsForm'); const colorSettingsForm = document.getElementById('colorSettingsForm');
// Tab persistence // Get all tab buttons
const settingsTabs = document.querySelectorAll('#settingsTabs button[data-bs-toggle="tab"]'); const settingsTabs = document.querySelectorAll('[data-bs-toggle="tab"]');
const tabContent = document.querySelectorAll('.tab-pane');
/** /**
* Activates a specific settings tab and updates the UI accordingly. * Activates a specific settings tab and updates the UI accordingly.
@@ -24,27 +23,89 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {string} tabId - The ID of the tab to activate * @param {string} tabId - The ID of the tab to activate
*/ */
function activateTab(tabId) { function activateTab(tabId) {
// Remove active class from all tabs and content // Update URL without reloading the page
const url = new URL(window.location.href);
url.searchParams.set('tab', tabId);
window.history.pushState({}, '', url);
// Save active tab to localStorage
localStorage.setItem('settingsActiveTab', tabId);
// Update active state of tabs
settingsTabs.forEach(tab => { settingsTabs.forEach(tab => {
tab.classList.remove('active'); const targetId = tab.getAttribute('data-bs-target').substring(1);
tab.setAttribute('aria-selected', 'false'); if (targetId === tabId) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
}); });
tabContent.forEach(content => {
content.classList.remove('show', 'active'); // Update active state of tab panes
document.querySelectorAll('.tab-pane').forEach(pane => {
if (pane.id === tabId) {
pane.classList.add('show', 'active');
} else {
pane.classList.remove('show', 'active');
}
}); });
// Activate the selected tab // If switching to events tab, fetch events data
const selectedTab = document.querySelector(`#${tabId}-tab`); if (tabId === 'events') {
const selectedContent = document.getElementById(tabId); fetchEvents();
if (selectedTab && selectedContent) {
selectedTab.classList.add('active');
selectedTab.setAttribute('aria-selected', 'true');
selectedContent.classList.add('show', 'active');
// Save to localStorage
localStorage.setItem('settingsActiveTab', tabId);
} }
} }
/**
* Fetches events data from the server and updates the events table
* @function
*/
function fetchEvents() {
const url = new URL(window.location.href);
const eventType = url.searchParams.get('event_type') || '';
const dateRange = url.searchParams.get('date_range') || '7d';
const userId = url.searchParams.get('user_id') || '';
const page = url.searchParams.get('page') || 1;
// Show loading state
const eventsTableBody = document.getElementById('eventsTableBody');
if (eventsTableBody) {
eventsTableBody.innerHTML = '<tr><td colspan="5" class="text-center">Loading events...</td></tr>';
}
// Fetch events data
fetch(`/settings/events?event_type=${eventType}&date_range=${dateRange}&user_id=${userId}&page=${page}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.text())
.then(html => {
// Extract the events table content from the response
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newEventsTable = doc.getElementById('eventsTableBody');
console.log('Found table body:', newEventsTable); // Debug log
if (newEventsTable && eventsTableBody) {
eventsTableBody.innerHTML = newEventsTable.innerHTML;
console.log('Updated table content'); // Debug log
} else {
console.error('Could not find events table in response');
if (eventsTableBody) {
eventsTableBody.innerHTML = '<tr><td colspan="5" class="text-center">Error: Could not load events</td></tr>';
}
}
})
.catch(error => {
console.error('Error fetching events:', error);
if (eventsTableBody) {
eventsTableBody.innerHTML = '<tr><td colspan="5" class="text-center">Error loading events</td></tr>';
}
});
}
// Add click event listeners to tabs // Add click event listeners to tabs
settingsTabs.forEach(tab => { settingsTabs.forEach(tab => {
tab.addEventListener('click', (e) => { tab.addEventListener('click', (e) => {

View File

@@ -73,7 +73,7 @@
<!-- Events Tab --> <!-- Events Tab -->
<div class="tab-pane fade {% if active_tab == 'events' %}show active{% endif %}" id="events" role="tabpanel" aria-labelledby="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) }} {{ events_tab(events, csrf_token, users) }}
</div> </div>
<!-- Debugging Tab --> <!-- Debugging Tab -->
@@ -92,4 +92,5 @@
{% 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.CSS_VERSION) }}"></script>
<script src="{{ url_for('static', filename='js/events.js', v=config.CSS_VERSION) }}"></script>
{% endblock %} {% endblock %}

View File

@@ -1,4 +1,4 @@
{% macro events_tab(events, csrf_token) %} {% macro events_tab(events, csrf_token, users) %}
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
@@ -41,6 +41,9 @@
</select> </select>
<select id="userFilter" class="form-select form-select-sm"> <select id="userFilter" class="form-select form-select-sm">
<option value="">All Users</option> <option value="">All Users</option>
{% for user in users %}
<option value="{{ user.id }}">{{ user.username }}</option>
{% endfor %}
</select> </select>
<button id="applyFilters" class="btn btn-primary btn-sm">Apply Filters</button> <button id="applyFilters" class="btn btn-primary btn-sm">Apply Filters</button>
</div> </div>
@@ -58,87 +61,93 @@
</tr> </tr>
</thead> </thead>
<tbody id="eventsTableBody"> <tbody id="eventsTableBody">
{% for event in events %} {% if events %}
<tr> {% for event in events %}
<td>{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td> <tr>
<td> <td>{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
{% if event.event_type == 'user_login' %} <td>
<span class="badge bg-success">User Login</span> {% if event.event_type == 'user_login' %}
{% elif event.event_type == 'user_logout' %} <span class="badge bg-success">User Login</span>
<span class="badge bg-secondary">User Logout</span> {% elif event.event_type == 'user_logout' %}
{% elif event.event_type == 'user_register' %} <span class="badge bg-secondary">User Logout</span>
<span class="badge bg-info">User Registration</span> {% elif event.event_type == 'user_register' %}
{% elif event.event_type == 'user_update' %} <span class="badge bg-info">User Registration</span>
<span class="badge bg-primary">User Update</span> {% elif event.event_type == 'user_update' %}
{% elif event.event_type == 'file_upload' %} <span class="badge bg-primary">User Update</span>
<span class="badge bg-success">File Upload</span> {% elif event.event_type == 'file_upload' %}
{% elif event.event_type == 'file_delete' %} <span class="badge bg-success">File Upload</span>
<span class="badge bg-danger">File Delete</span> {% elif event.event_type == 'file_delete' %}
{% elif event.event_type == 'file_download' %} <span class="badge bg-danger">File Delete</span>
<span class="badge bg-info">File Download</span> {% elif event.event_type == 'file_download' %}
{% elif event.event_type == 'file_restore' %} <span class="badge bg-info">File Download</span>
<span class="badge bg-warning">File Restore</span> {% elif event.event_type == 'file_restore' %}
{% elif event.event_type == 'file_move' %} <span class="badge bg-warning">File Restore</span>
<span class="badge bg-primary">File Move</span> {% elif event.event_type == 'file_move' %}
{% elif event.event_type == 'file_rename' %} <span class="badge bg-primary">File Move</span>
<span class="badge bg-info">File Rename</span> {% elif event.event_type == 'file_rename' %}
{% elif event.event_type == 'file_star' %} <span class="badge bg-info">File Rename</span>
<span class="badge bg-warning">File Star</span> {% elif event.event_type == 'file_star' %}
{% elif event.event_type == 'file_unstar' %} <span class="badge bg-warning">File Star</span>
<span class="badge bg-secondary">File Unstar</span> {% elif event.event_type == 'file_unstar' %}
{% elif event.event_type == 'file_delete_permanent' %} <span class="badge bg-secondary">File Unstar</span>
<span class="badge bg-danger">File Delete Permanent</span> {% elif event.event_type == 'file_delete_permanent' %}
{% elif event.event_type == 'room_create' %} <span class="badge bg-danger">File Delete Permanent</span>
<span class="badge bg-success">Room Create</span> {% elif event.event_type == 'room_create' %}
{% elif event.event_type == 'room_delete' %} <span class="badge bg-success">Room Create</span>
<span class="badge bg-danger">Room Delete</span> {% elif event.event_type == 'room_delete' %}
{% elif event.event_type == 'room_update' %} <span class="badge bg-danger">Room Delete</span>
<span class="badge bg-primary">Room Update</span> {% elif event.event_type == 'room_update' %}
{% elif event.event_type == 'room_open' %} <span class="badge bg-primary">Room Update</span>
<span class="badge bg-info">Room Open</span> {% elif event.event_type == 'room_open' %}
{% elif event.event_type == 'room_list_view' %} <span class="badge bg-info">Room Open</span>
<span class="badge bg-secondary">Room List View</span> {% elif event.event_type == 'room_list_view' %}
{% elif event.event_type == 'room_search' %} <span class="badge bg-secondary">Room List View</span>
<span class="badge bg-info">Room Search</span> {% elif event.event_type == 'room_search' %}
{% elif event.event_type == 'room_join' %} <span class="badge bg-info">Room Search</span>
<span class="badge bg-info">Room Join</span> {% elif event.event_type == 'room_join' %}
{% elif event.event_type == 'room_leave' %} <span class="badge bg-info">Room Join</span>
<span class="badge bg-secondary">Room Leave</span> {% elif event.event_type == 'room_leave' %}
{% elif event.event_type == 'room_member_permissions_update' %} <span class="badge bg-secondary">Room Leave</span>
<span class="badge bg-primary">Room Member Permissions Update</span> {% elif event.event_type == 'room_member_permissions_update' %}
{% elif event.event_type == 'conversation_create' %} <span class="badge bg-primary">Room Member Permissions Update</span>
<span class="badge bg-success">Conversation Create</span> {% elif event.event_type == 'conversation_create' %}
{% elif event.event_type == 'conversation_delete' %} <span class="badge bg-success">Conversation Create</span>
<span class="badge bg-danger">Conversation Delete</span> {% elif event.event_type == 'conversation_delete' %}
{% elif event.event_type == 'message_sent' %} <span class="badge bg-danger">Conversation Delete</span>
<span class="badge bg-primary">Message Sent</span> {% elif event.event_type == 'message_sent' %}
{% elif event.event_type == 'attachment_download' %} <span class="badge bg-primary">Message Sent</span>
<span class="badge bg-info">Attachment Download</span> {% elif event.event_type == 'attachment_download' %}
{% else %} <span class="badge bg-info">Attachment Download</span>
<span class="badge bg-secondary">{{ event.event_type }}</span> {% else %}
{% endif %} <span class="badge bg-secondary">{{ event.event_type }}</span>
</td> {% endif %}
<td>{{ event.user.username }} {% if event.user.last_name %}{{ event.user.last_name }}{% endif %}</td> </td>
<td> <td>{{ event.user.username if event.user else 'Unknown' }}</td>
<button class="btn btn-sm btn-outline-secondary" <td>
data-bs-toggle="modal" <button class="btn btn-sm btn-outline-secondary"
data-bs-target="#eventDetailsModal" data-bs-toggle="modal"
data-event-id="{{ event.id }}"> data-bs-target="#eventDetailsModal"
<i class="fas fa-info-circle"></i> View Details data-event-id="{{ event.id }}">
</button> <i class="fas fa-info-circle"></i> View Details
</td> </button>
<td>{{ event.ip_address or '-' }}</td> </td>
</tr> <td>{{ event.ip_address or '-' }}</td>
{% endfor %} </tr>
{% endfor %}
{% else %}
<tr>
<td colspan="5" class="text-center">No events found</td>
</tr>
{% endif %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="d-flex justify-content-between align-items-center mt-3"> <div class="d-flex justify-content-between align-items-center mt-3">
<div> <div>
<button id="prevPage" class="btn btn-outline-primary btn-sm">Previous</button> <button id="prevPage" class="btn btn-outline-primary btn-sm" {% if current_page == 1 %}disabled{% endif %}>Previous</button>
<span class="mx-2">Page <span id="currentPage">1</span> of <span id="totalPages">1</span></span> <span class="mx-2">Page <span id="currentPage">{{ current_page }}</span> of <span id="totalPages">{{ total_pages }}</span></span>
<button id="nextPage" class="btn btn-outline-primary btn-sm">Next</button> <button id="nextPage" class="btn btn-outline-primary btn-sm" {% if current_page == total_pages %}disabled{% endif %}>Next</button>
</div> </div>
</div> </div>
</div> </div>
@@ -160,6 +169,6 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% block extra_js %} {% block content %}
<script src="{{ url_for('static', filename='js/events.js', v=config.JS_VERSION) }}"></script> {{ events_tab(events, csrf_token, users) }}
{% endblock %} {% endblock %}