From f027d9105dd87f49d5d928f8da6e21088af20deb Mon Sep 17 00:00:00 2001 From: Simon Cloutier Date: Sun, 6 Apr 2025 16:18:20 -0400 Subject: [PATCH] comments added --- app/db_interface.py | 105 +++++++++++++----- app/static/styles.css | 87 ++++++++++++++- app/templates/group_list.html | 60 +++++++++- app/templates/stats.html | 6 +- app/templates/user_list.html | 4 +- .../__pycache__/group_views.cpython-39.pyc | Bin 1358 -> 1595 bytes .../__pycache__/index_views.cpython-39.pyc | Bin 2084 -> 2098 bytes app/views/group_views.py | 16 ++- app/views/index_views.py | 1 + 9 files changed, 237 insertions(+), 42 deletions(-) diff --git a/app/db_interface.py b/app/db_interface.py index d55588e..00c9ed8 100644 --- a/app/db_interface.py +++ b/app/db_interface.py @@ -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 \ No newline at end of file diff --git a/app/static/styles.css b/app/static/styles.css index 69f8129..2f8097a 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -316,4 +316,89 @@ form.inline-form { font-weight: bold; background-color: var(--accent); color: black; -} \ No newline at end of file +} + +.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); +} diff --git a/app/templates/group_list.html b/app/templates/group_list.html index 3835df2..c0d95f3 100644 --- a/app/templates/group_list.html +++ b/app/templates/group_list.html @@ -36,7 +36,7 @@ -
+
@@ -46,6 +46,22 @@ + + + {% endblock %} diff --git a/app/templates/stats.html b/app/templates/stats.html index c474090..645661d 100644 --- a/app/templates/stats.html +++ b/app/templates/stats.html @@ -128,9 +128,11 @@ + {% else %} diff --git a/app/templates/user_list.html b/app/templates/user_list.html index 95fe078..5c59865 100644 --- a/app/templates/user_list.html +++ b/app/templates/user_list.html @@ -10,7 +10,7 @@ @@ -46,7 +46,7 @@ diff --git a/app/views/__pycache__/group_views.cpython-39.pyc b/app/views/__pycache__/group_views.cpython-39.pyc index 258d8bf26cdf2e011c68e4449d75a5ff01b1107e..1ddd6172f37292106e5a4ea663f652e3d87db73d 100644 GIT binary patch literal 1595 zcmb_c%W~T`6a_$1q^PGYIkD69mC1%1>jyfWOtV)qldQPG&<14FB_tN0oLb|o)UE#^ zUgkft=m&I#nXbC(HcPK^FDOfN+hsX}OW+a$&N=s7WxZZZ;Q8~9&u72;g#3kz<`cl; z1q|~Ef*^txMA4KgmNMm~p7K*)1!zJwC$gYr7U$muk1j}xsaMyQk6wkNgHbU%Tk)k zMp}wo%hSq+OI`5ULfc?gWaiw47iKZfXIDvJ`)9J^SyAvay;zpUwlg8>xjkH#0uHQK zTu5`O^RmhpbGt8OA-7-Phj8r}4_X?j4L`Z!mqj+`xv+sdcGa4=cS3E9P)aRl`LB0$ za*BNJ9|{wwBY6qK>_e!?n!G0!74#Y^bHhLZZ@L<|qj-^<>iE1;#Ybv~*=3d&*-5bh zYZ9j0aDetO>_PZQ^ax&eN0Y29CzrW=XKF~?&5z3~8}H5yVXHwXgte$XfcMIufH2m7 z{p!tIU=b==oiBuWfSE&gLzROAA%A0Y>rePDtK@0Pu%m3K|9>6%ee`BfnNHCw)$fn?a-JbqfM0aOgI;gOIxuK#wsw*cO?) z;`JbDA_#=Vv`=HIzlHf~G}$kI< zgW(_a?v2}kUGv0BKIHj5WLDv%_VWX7@PVKsU&1h`D7q1)ngdW10j4B~1$*|iLmvF@`67;v`-!(l)rHa?=a6g&kkT2pY74_-q5u&FC~ z5;$u#l*96BEbqN30@tSv0uKA=Kj5G~jy+*r8d3cm*3;xs{YUb%Mu2sbp&NJA(bz%m zO4}7D9R8=Io@J-9?n>uWS5BuR!m|H$Wi#mh=kzJ-dl8KTFm<>YUY~ve9`G6HNOGFg6)yBO*Q4ZnS zL*y4I5<=n`cn2OJw;roXya1QZY^2DR_VeuT(d@s!|Ac>oXb?q#py&4b-{{#Q>ciFI zd4C211UEF*xvaK8TmU)Z+2QqW(BX|{ss3*G8#mJnV zpVdF%YwA;raTqb-w=hZlLLNX`kL9nYigl0CB4pBR(QE0Vo(@gIvPHLk?d(CX{@|E* z+pOMa>=3e+ak<`QJYcjL&53;$9x|E(x842OBND&(`zyyo+N>X-iY>pTHRZNSi~(U+ zd=r(tpd>+5PUWJU!yG6G=h)a|tR=5uiGN8Vt3u0ox$$-TBC+DvFaqsf)sNcz<2vL_;@EhD?qzoy%b14+E%l?@8@6POh_a^)idPC213_URWH9B8;&*0mt z^YZn?ja)E6M#sZ?U48D}aL*b4m|$4YLY$)i+z< ztM_IHn(GU*2Jf0=)6{5l+@ZKdXcJy&WF{tOSf?~cgEae7GLh5aNIutDx~*X_2V8KO z3uRUS_ILilu44Qadg?oW^rlLcJe}d^DRLd7tI=lIBfW?+osVb#U*v2awWp7z`KPin z6d+_aV1K<^~+HF@=Y8@YuAEd`*b{u~l tPGlUTM;d)m#xa@CBz8&Y6R7eZzjaC*>bLbcd}`UuVRp?1$M?ap?*abIXA=Mb delta 513 zcmZ8dJx}965cT*g#%DVQg2*9GAv*D4K%xO8BtTF>byPH#PF7$xxj@WD;}|IDphY6u zKL8R91r6!>A!+IP0aWZlae*zK-h8~7H~SRchkniXT}{5N?rWpo^?$)lcVYVvXsDst z@?wtxTFk%H;{a_eTxoHU1-BtOcYs6aUTK#Q53}GX#2@H20R3wn11x^zM=-=;sTl1( z?}I5yu+!s^0R-Yj-yfflOJft8^?JXMu|s}>*@H$Ev1iPIFV2kxsP^uSGHh05BN-AR zi)Iv`RWPR@lF&DZpHW4HA26kI&Oe^AcCEpt#hY1yMKNJLHkVYz=6R=`cOsii6`FNU zDU}qpvJN%dWJw;6v^YP`+eunGOimdc@Qg(|eGVX&<*m;5jqZQNvZ&glu-aR(E%?1A z#|_y2>XYVWbwm6u{InH|O5&oh+Z>a$?ROp>L5i&uCF4Ruw4GfRTaw e*Heypi*17J$kch0;6d4TbXRxE4!A)8uJaB;ta2d$ diff --git a/app/views/group_views.py b/app/views/group_views.py index 0c263e8..d7a237a 100644 --- a/app/views/group_views.py +++ b/app/views/group_views.py @@ -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) \ No newline at end of file diff --git a/app/views/index_views.py b/app/views/index_views.py index 0e4865f..ba7ef02 100644 --- a/app/views/index_views.py +++ b/app/views/index_views.py @@ -5,6 +5,7 @@ from db_interface import ( get_vendor_info, get_latest_auth_logs, get_all_groups, + lookup_mac_verbose, ) import pytz