comments added
This commit is contained in:
@@ -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()
|
||||
cursor.execute("DELETE FROM groups WHERE vlan_id = %s", (vlan_id,))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -128,9 +128,11 @@
|
||||
<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>
|
||||
</select>
|
||||
<button type="submit" title="Add">💾</button>
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -5,6 +5,7 @@ from db_interface import (
|
||||
get_vendor_info,
|
||||
get_latest_auth_logs,
|
||||
get_all_groups,
|
||||
lookup_mac_verbose,
|
||||
)
|
||||
import pytz
|
||||
|
||||
|
||||
Reference in New Issue
Block a user