comments added

This commit is contained in:
2025-04-06 16:18:20 -04:00
parent 2e511ca428
commit f027d9105d
9 changed files with 237 additions and 42 deletions

View File

@@ -1,4 +1,4 @@
from flask import current_app
from flask import current_app, request, redirect, url_for, flash
import mysql.connector
from datetime import datetime, timedelta, timezone
import requests
@@ -7,8 +7,8 @@ import os
import pytz
from db_connection import get_connection
def get_all_users():
"""Retrieve all users with associated group and vendor information."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
@@ -26,10 +26,8 @@ def get_all_users():
conn.close()
return users
def get_all_groups():
"""Retrieve all groups along with user count for each group."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
@@ -44,19 +42,8 @@ def get_all_groups():
conn.close()
return available_groups
def get_group_by_name(name):
conn = get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM groups WHERE name = %s", (name,))
group = cursor.fetchone()
cursor.close()
conn.close()
return group
def add_group(vlan_id, description):
"""Insert a new group with a specified VLAN ID and description."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("INSERT INTO groups (vlan_id, description) VALUES (%s, %s)", (vlan_id, description))
@@ -64,8 +51,8 @@ def add_group(vlan_id, description):
cursor.close()
conn.close()
def update_group_description(vlan_id, description):
"""Update the description for a given MAC address in the users table."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("UPDATE groups SET description = %s WHERE vlan_id = %s", (description, vlan_id))
@@ -73,17 +60,24 @@ def update_group_description(vlan_id, description):
cursor.close()
conn.close()
def delete_group(vlan_id):
def delete_group(vlan_id, force_delete=False):
"""Delete a group, and optionally its associated users if force_delete=True."""
conn = get_connection()
cursor = conn.cursor()
try:
if force_delete:
cursor.execute("DELETE FROM users WHERE vlan_id = %s", (vlan_id,))
cursor.execute("DELETE FROM groups WHERE vlan_id = %s", (vlan_id,))
conn.commit()
except mysql.connector.IntegrityError as e:
print(f"❌ Cannot delete group '{vlan_id}': it is still in use. Error: {e}")
raise
finally:
cursor.close()
conn.close()
def duplicate_group(vlan_id):
"""Create a duplicate of a group with an incremented VLAN ID."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT vlan_id, description FROM groups WHERE vlan_id = %s", (vlan_id,))
@@ -98,8 +92,8 @@ def duplicate_group(vlan_id):
cursor.close()
conn.close()
def add_user(mac_address, description, vlan_id):
"""Insert a new user with MAC address, description, and VLAN assignment."""
print(f"→ Adding to DB: mac={mac_address}, desc={description}, vlan={vlan_id}")
conn = get_connection()
cursor = conn.cursor()
@@ -111,8 +105,8 @@ def add_user(mac_address, description, vlan_id):
cursor.close()
conn.close()
def update_user_description(mac_address, description):
"""Update the description field of a user identified by MAC address."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("UPDATE users SET description = %s WHERE mac_address = %s", (description, mac_address.lower()))
@@ -120,8 +114,8 @@ def update_user_description(mac_address, description):
cursor.close()
conn.close()
def update_user_vlan(mac_address, vlan_id):
"""Update the VLAN ID for a given MAC address in the users table."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("UPDATE users SET vlan_id = %s WHERE mac_address = %s", (vlan_id, mac_address.lower()))
@@ -129,8 +123,8 @@ def update_user_vlan(mac_address, vlan_id):
cursor.close()
conn.close()
def delete_user(mac_address):
"""Remove a user from the database by their MAC address."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM users WHERE mac_address = %s", (mac_address.lower(),))
@@ -138,8 +132,8 @@ def delete_user(mac_address):
cursor.close()
conn.close()
def get_latest_auth_logs(reply_type=None, limit=5, time_range=None, offset=0):
"""Retrieve recent authentication logs filtered by reply type and time range."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
@@ -181,6 +175,7 @@ def get_latest_auth_logs(reply_type=None, limit=5, time_range=None, offset=0):
return logs
def count_auth_logs(reply_type=None, time_range=None):
"""Count the number of authentication logs matching a reply type and time."""
conn = get_connection()
cursor = conn.cursor()
@@ -218,6 +213,7 @@ def count_auth_logs(reply_type=None, time_range=None):
return count
def get_summary_counts():
"""Return total counts of users and groups from the database."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
@@ -232,6 +228,7 @@ def get_summary_counts():
return total_users, total_groups
def update_description(mac_address, description):
"""Update the description for a given MAC address in the users table."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute(
@@ -243,6 +240,7 @@ def update_description(mac_address, description):
conn.close()
def update_vlan(mac_address, vlan_id):
"""Update the VLAN ID for a given MAC address in the users table."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute(
@@ -254,6 +252,7 @@ def update_vlan(mac_address, vlan_id):
conn.close()
def refresh_vendors():
"""Fetch and cache vendor info for unknown MAC prefixes using the API."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
@@ -328,6 +327,7 @@ def refresh_vendors():
conn.close()
def lookup_mac_verbose(mac):
"""Look up vendor info for a MAC with verbose output, querying API if needed."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
output = []
@@ -382,6 +382,7 @@ def lookup_mac_verbose(mac):
return "\n".join(output)
def get_user_by_mac(mac_address):
"""Retrieve a user record from the database by MAC address."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
@@ -394,6 +395,7 @@ def get_user_by_mac(mac_address):
return user
def get_known_mac_vendors():
"""Fetch all known MAC prefixes and their vendor info from the local database."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT mac_prefix, vendor_name, status FROM mac_vendors")
@@ -410,6 +412,7 @@ def get_known_mac_vendors():
}
def get_vendor_info(mac, insert_if_found=True):
"""Get vendor info for a MAC address, optionally inserting into the database."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
prefix = mac.lower().replace(":", "").replace("-", "")[:6]
@@ -522,3 +525,45 @@ def get_vendor_info(mac, insert_if_found=True):
cursor.close()
conn.close()
def delete_group_route():
"""Handle deletion of a group and optionally its users via form POST."""
vlan_id = request.form.get("group_id")
force = request.form.get("force_delete") == "true"
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM users WHERE vlan_id = %s", (vlan_id,))
user_count = cursor.fetchone()[0]
if user_count > 0 and not force:
conn.close()
flash("Group has users. Please confirm deletion or reassign users.", "error")
return redirect(url_for("group.group_list"))
try:
if force:
cursor.execute("DELETE FROM users WHERE vlan_id = %s", (vlan_id,))
cursor.execute("DELETE FROM groups WHERE vlan_id = %s", (vlan_id,))
conn.commit()
flash(f"Group {vlan_id} and associated users deleted." if force else f"Group {vlan_id} deleted.", "success")
except mysql.connector.IntegrityError as e:
flash(f"Cannot delete group {vlan_id}: it is still in use. Error: {e}", "error")
except Exception as e:
flash(f"Error deleting group: {e}", "error")
finally:
cursor.close()
conn.close()
return redirect(url_for("group.group_list"))
def get_users_by_vlan_id(vlan_id):
"""Fetch users assigned to a specific VLAN ID."""
conn = get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT mac_address, description FROM users WHERE vlan_id = %s", (vlan_id,))
users = cursor.fetchall()
cursor.close()
conn.close()
return users

View File

@@ -317,3 +317,88 @@ form.inline-form {
background-color: var(--accent);
color: black;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, 0.4);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
.modal-card {
background: var(--card-bg);
color: var(--fg);
padding: 2rem;
border-radius: 10px;
max-width: 500px;
width: 90%;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.6);
}
.modal-actions {
margin-top: 1.5rem;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.modal-actions button,
.modal-actions form button {
padding: 0.5rem 1rem;
font-weight: bold;
cursor: pointer;
border-radius: 5px;
border: none;
}
.modal-actions button {
background-color: #ccc;
color: black;
}
.modal-actions button.danger {
background-color: var(--error);
color: white;
}
.modal {
position: fixed;
z-index: 10000;
left: 0; top: 0;
width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex; align-items: center; justify-content: center;
}
.modal-content {
background: var(--card-bg);
padding: 1.5rem;
border-radius: 8px;
color: var(--fg);
width: 500px;
max-height: 70vh;
overflow-y: auto;
box-shadow: 0 0 15px rgba(0,0,0,0.3);
}
.modal-actions {
margin-top: 1rem;
display: flex;
justify-content: space-between;
}
.user-list {
margin-top: 1rem;
max-height: 200px;
overflow-y: auto;
border: 1px solid #555;
padding: 0.5rem;
font-size: 0.9rem;
background: var(--cell-bg);
}

View File

@@ -36,7 +36,7 @@
<input type="hidden" name="description" value="{{ group.description }}">
<button type="submit" title="Save">💾</button>
</form>
<form method="POST" action="{{ url_for('group.delete_group_route') }}" class="preserve-scroll" style="display:inline;" onsubmit="return confirm('Delete this group?');">
<form method="POST" action="{{ url_for('group.delete_group_route_handler') }}" class="preserve-scroll delete-group-form" data-user-count="{{ group.user_count }}" style="display:inline;">
<input type="hidden" name="group_id" value="{{ group.vlan_id }}">
<button type="submit"></button>
</form>
@@ -46,6 +46,22 @@
</tbody>
</table>
<!-- Modal for confirm delete -->
<div id="confirmModal" class="modal" style="display: none;">
<div class="modal-content">
<p>This group has users assigned. What would you like to do?</p>
<div id="userList" class="user-list"></div>
<div class="modal-actions">
<button onclick="closeModal()">Cancel</button>
<form id="confirmDeleteForm" method="POST" action="{{ url_for('group.delete_group_route_handler') }}">
<input type="hidden" name="group_id" id="modalGroupId">
<input type="hidden" name="force_delete" value="true">
<button type="submit" class="danger">Delete Group and Users</button>
</form>
</div>
</div>
</div>
<script>
document.querySelectorAll('form.preserve-scroll').forEach(form => {
form.addEventListener('submit', () => {
@@ -59,5 +75,47 @@
localStorage.removeItem('scrollY');
}
});
document.querySelectorAll('.delete-group-form').forEach(form => {
form.addEventListener('submit', function (e) {
const userCount = parseInt(this.dataset.userCount);
const groupId = this.querySelector('[name="group_id"]').value;
if (userCount > 0) {
e.preventDefault();
fetch('{{ url_for("group.get_users_for_group") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ vlan_id: groupId })
})
.then(resp => resp.json())
.then(users => {
const userListDiv = document.getElementById('userList');
userListDiv.innerHTML = '';
if (users.length > 0) {
const list = document.createElement('ul');
users.forEach(user => {
const item = document.createElement('li');
item.textContent = `${user.mac_address}${user.description || 'No description'}`;
list.appendChild(item);
});
userListDiv.appendChild(list);
} else {
userListDiv.textContent = 'No users found in this group.';
}
document.getElementById('modalGroupId').value = groupId;
document.getElementById('confirmModal').style.display = 'flex';
});
} else {
if (!confirm('Delete this group?')) e.preventDefault();
}
});
});
function closeModal() {
document.getElementById('confirmModal').style.display = 'none';
}
</script>
{% endblock %}

View File

@@ -128,7 +128,9 @@
<select name="group_id" required>
<option value="">Assign to VLAN</option>
{% for group in available_groups %}
<option value="{{ group.vlan_id }}">VLAN {{ group.vlan_id }}</option>
<option value="{{ group.vlan_id }}">
VLAN {{ group.vlan_id }}{% if group.description %} - {{ group.description }}{% endif %}
</option>
{% endfor %}
</select>
<button type="submit" title="Add">💾</button>

View File

@@ -10,7 +10,7 @@
<select name="group_id" required>
<option value="">Assign to VLAN</option>
{% for group in available_groups %}
<option value="{{ group.vlan_id }}">VLAN {{ group.vlan_id }}</option>
<option value="{{ group.vlan_id }}">VLAN {{ group.vlan_id }}{% if group.description %} - {{ group.description }}{% endif %}</option>
{% endfor %}
</select>
<button type="submit"> Add</button>
@@ -46,7 +46,7 @@
<select name="group_id" onchange="this.form.submit()">
{% for group in available_groups %}
<option value="{{ group.vlan_id }}" {% if group.vlan_id == entry.vlan_id %}selected{% endif %}>
VLAN {{ group.vlan_id }}
VLAN {{ group.vlan_id }}{% if group.description %} - {{ group.description }}{% endif %}
</option>
{% endfor %}
</select>

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, render_template, request, redirect, url_for
from db_interface import get_all_groups, add_group, update_group_description, delete_group
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from db_interface import get_all_groups, add_group, update_group_description, delete_group_route, get_users_by_vlan_id
group = Blueprint('group', __name__, url_prefix='/group')
@@ -27,7 +27,11 @@ def update_description_route():
@group.route('/delete', methods=['POST'])
def delete_group_route():
group_id = request.form['group_id']
delete_group(group_id)
return redirect(url_for('group.group_list'))
def delete_group_route_handler():
return delete_group_route()
@group.route('/get_users_for_group', methods=['POST'])
def get_users_for_group():
vlan_id = request.form.get('vlan_id')
users = get_users_by_vlan_id(vlan_id)
return jsonify(users)

View File

@@ -5,6 +5,7 @@ from db_interface import (
get_vendor_info,
get_latest_auth_logs,
get_all_groups,
lookup_mac_verbose,
)
import pytz