user logging
This commit is contained in:
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 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'))
|
||||||
|
|
||||||
|
|||||||
101
routes/main.py
101
routes/main.py
@@ -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,6 +628,9 @@ 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()
|
||||||
|
|
||||||
|
# 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',
|
return render_template('settings/tabs/events.html',
|
||||||
events=events.items,
|
events=events.items,
|
||||||
total_pages=total_pages,
|
total_pages=total_pages,
|
||||||
@@ -588,6 +641,19 @@ def init_routes(main_bp):
|
|||||||
users=users,
|
users=users,
|
||||||
csrf_token=session.get('csrf_token'))
|
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,
|
||||||
|
total_pages=total_pages,
|
||||||
|
current_page=page,
|
||||||
|
users=users,
|
||||||
|
csrf_token=session.get('csrf_token'))
|
||||||
|
|
||||||
@main_bp.route('/api/events/<int:event_id>')
|
@main_bp.route('/api/events/<int:event_id>')
|
||||||
@login_required
|
@login_required
|
||||||
def get_event_details(event_id):
|
def get_event_details(event_id):
|
||||||
@@ -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)
|
||||||
@@ -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.';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,25 +23,87 @@ 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 => {
|
||||||
|
const targetId = tab.getAttribute('data-bs-target').substring(1);
|
||||||
|
if (targetId === tabId) {
|
||||||
|
tab.classList.add('active');
|
||||||
|
} else {
|
||||||
tab.classList.remove('active');
|
tab.classList.remove('active');
|
||||||
tab.setAttribute('aria-selected', 'false');
|
}
|
||||||
});
|
|
||||||
tabContent.forEach(content => {
|
|
||||||
content.classList.remove('show', 'active');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Activate the selected tab
|
// Update active state of tab panes
|
||||||
const selectedTab = document.querySelector(`#${tabId}-tab`);
|
document.querySelectorAll('.tab-pane').forEach(pane => {
|
||||||
const selectedContent = document.getElementById(tabId);
|
if (pane.id === tabId) {
|
||||||
if (selectedTab && selectedContent) {
|
pane.classList.add('show', 'active');
|
||||||
selectedTab.classList.add('active');
|
} else {
|
||||||
selectedTab.setAttribute('aria-selected', 'true');
|
pane.classList.remove('show', 'active');
|
||||||
selectedContent.classList.add('show', 'active');
|
|
||||||
// Save to localStorage
|
|
||||||
localStorage.setItem('settingsActiveTab', tabId);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If switching to events tab, fetch events data
|
||||||
|
if (tabId === 'events') {
|
||||||
|
fetchEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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,6 +61,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="eventsTableBody">
|
<tbody id="eventsTableBody">
|
||||||
|
{% if events %}
|
||||||
{% for event in events %}
|
{% for event in events %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
<td>{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||||
@@ -118,7 +122,7 @@
|
|||||||
<span class="badge bg-secondary">{{ event.event_type }}</span>
|
<span class="badge bg-secondary">{{ event.event_type }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ event.user.username }} {% if event.user.last_name %}{{ event.user.last_name }}{% endif %}</td>
|
<td>{{ event.user.username if event.user else 'Unknown' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-secondary"
|
<button class="btn btn-sm btn-outline-secondary"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@@ -130,15 +134,20 @@
|
|||||||
<td>{{ event.ip_address or '-' }}</td>
|
<td>{{ event.ip_address or '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% 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 %}
|
||||||
Reference in New Issue
Block a user