From 173c8c2c996c13fd6ebc30d817c85abba0a17077 Mon Sep 17 00:00:00 2001 From: Simon Cloutier Date: Tue, 1 Apr 2025 10:12:38 -0400 Subject: [PATCH] LOTS of changes --- app/__pycache__/config.cpython-39.pyc | Bin 0 -> 1403 bytes app/__pycache__/database.cpython-39.pyc | Bin 0 -> 1214 bytes app/app.py | 537 +----------------- app/config.py | 33 ++ app/database.py | 34 ++ app/requirements.txt | 5 +- app/templates/add_user.html | 19 - app/templates/base.html | 258 ++++++++- app/templates/edit_attribute.html | 22 - app/templates/edit_group.html | 26 - app/templates/edit_groupname.html | 16 - app/templates/edit_user.html | 20 - app/templates/group_list.html | 38 -- app/templates/group_list_nested.html | 507 ++++++----------- app/templates/index.html | 152 +++-- app/templates/stats.html | 100 ++++ app/templates/user_list.html | 364 ------------ app/templates/user_list_inline_edit.html | 419 ++++++-------- app/views/__init__.py | 3 + app/views/__pycache__/__init__.cpython-39.pyc | Bin 0 -> 234 bytes .../__pycache__/group_views.cpython-39.pyc | Bin 0 -> 5893 bytes .../__pycache__/index_views.cpython-39.pyc | Bin 0 -> 5320 bytes .../__pycache__/user_views.cpython-39.pyc | Bin 0 -> 7549 bytes app/views/group_views.py | 201 +++++++ app/views/index_views.py | 187 ++++++ app/views/user_views.py | 276 +++++++++ docker-compose.yml | 15 +- 27 files changed, 1548 insertions(+), 1684 deletions(-) create mode 100644 app/__pycache__/config.cpython-39.pyc create mode 100644 app/__pycache__/database.cpython-39.pyc create mode 100644 app/config.py create mode 100644 app/database.py delete mode 100644 app/templates/add_user.html delete mode 100644 app/templates/edit_attribute.html delete mode 100644 app/templates/edit_group.html delete mode 100644 app/templates/edit_groupname.html delete mode 100644 app/templates/edit_user.html delete mode 100644 app/templates/group_list.html create mode 100644 app/templates/stats.html delete mode 100644 app/templates/user_list.html create mode 100644 app/views/__init__.py create mode 100644 app/views/__pycache__/__init__.cpython-39.pyc create mode 100644 app/views/__pycache__/group_views.cpython-39.pyc create mode 100644 app/views/__pycache__/index_views.cpython-39.pyc create mode 100644 app/views/__pycache__/user_views.cpython-39.pyc create mode 100644 app/views/group_views.py create mode 100644 app/views/index_views.py create mode 100644 app/views/user_views.py diff --git a/app/__pycache__/config.cpython-39.pyc b/app/__pycache__/config.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60185c6b9882fcadda337e275d4fab923f40253a GIT binary patch literal 1403 zcmb7D%W~5&6!qJV^B_>(3t+(}3zD`>JB4AGI*D6M)1<@>P;G=K5+yWEj2(BTVOW(9 zX_tHtJ8fAp%s+OdLig6qTpZVPV0 zo(js-f{1d&K2{E8RGh_mQV?;86yYyQ%xH{9?*}{6xq6t;2YnHwaX1-_N7Xe{UTZo2 z2KL=X*A4K7yFI;5==(4oP96 zVShLt@1{{TjH259Vhs{W?emwjDX=P&*o~7FTHdA?;4QbycV+S-n>(pA9j~>GMIGr2 z^U&IrE6l=p6z)-snT_$@n509_3mAV&!{MxAVob(;Nb60v-dks8;QE2rUPt%*S1qU2 zc;Rkt8*JUC-6FLpDE*n89Q`BNBhh^Qz2I2a$;@~rVd7v*x;cB@J)*6 zIOp)E1Oi00Owwv}5G#=66O(HImaNTsLFTo-_y)>&hKcIf4%~6UUI4(!Sg$o;Q?}{0y72EfLVg+z;eU@nN3#SiouS3{*-1a z-#N=%5c#sRv@77mH}1AyGdnu{ literal 0 HcmV?d00001 diff --git a/app/__pycache__/database.cpython-39.pyc b/app/__pycache__/database.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93a2c49ac35a2b17ecf9af101581b9051df7872a GIT binary patch literal 1214 zcmZWoOK%e~5VpOKY@RKxga`)|jw_*3Ax@}5qz{A;eb5w@E<({JcEgsebGS$M$%>Z^mJ{TtF}mzW?m}vJm>AC$j^5=n4Gd zIS7gHcoCzkIf(a3&iL&3NLMpo_7*}2v3UQorsyrIX zuoVjC+uVj+g2l}~q_UPK32RF@w*&DxffePp)8sJfd_%klQzQP&-v_SlU&dI<`l=Kq zk!;ddQ>Wm()|Mi)1=XHp;JaMMGzhqEFmrEE=>Ri;X9|*6^vV^iiB$rha`i^q zx&gK@{R!z8a0$<2J_o)w25~v`MS70`@EQz>M!GT3n4IDv=pN>KgVGR>$QWU?kCF#A zl2(5gqf>&>;I_0W8ImvPD>=h@{Q&y;lzbvdVWOAOGI$I~lkwo*oAuS&TFk4_Dji7R4fM 0: - print("Database rows were modified.") - else: - print("Database rows were not modified.") - - db.commit() - db.autocommit = True - cursor.close() - print("Update successful") - return "success" - - except mysql.connector.Error as err: - db.rollback() - db.autocommit = True - cursor.close() - print(f"Database Error: {err}") - return str(err) - - except Exception as e: - db.rollback() - db.autocommit = True - cursor.close() - print(f"Exception: {e}") - return str(e) - - finally: - db.close() - else: - print("Database Connection Failed") - return "Database Connection Failed" - -@app.route('/delete_user/') -def delete_user(mac_address): - db = get_db() - if db: - cursor = db.cursor() - try: - db.autocommit = False - - cursor.execute("DELETE FROM rad_description WHERE username = %s", (mac_address,)) - cursor.execute("DELETE FROM radcheck WHERE username = %s", (mac_address,)) - cursor.execute("DELETE FROM radusergroup WHERE username = %s", (mac_address,)) - - db.commit() - db.autocommit = True - cursor.close() - db.close() - return redirect(url_for('user_list')) - except mysql.connector.Error as err: - print(f"Database Error: {err}") - db.rollback() - db.autocommit = True - cursor.close() - db.close() - return redirect(url_for('user_list')) - return "Database Connection Failed" +def legacy_user_list(): + return redirect(url_for('user.user_list')) @app.route('/groups') -def groups(): - db = get_db() - if db: - cursor = db.cursor() - try: - cursor.execute("SELECT DISTINCT groupname FROM radgroupcheck") - group_names = [row[0] for row in cursor.fetchall()] +def legacy_group_list(): + return redirect(url_for('group.groups')) - grouped_results = {} - for groupname in group_names: - cursor.execute("SELECT id, attribute, op, value FROM radgroupreply WHERE groupname = %s", (groupname,)) - attributes = cursor.fetchall() - grouped_results[groupname] = [{'id': row[0], 'attribute': row[1], 'op': row[2], 'value': row[3]} for row in attributes] - - cursor.close() - db.close() - return render_template('group_list_nested.html', grouped_results=grouped_results) - except mysql.connector.Error as err: - print(f"Database Error: {err}") - cursor.close() - db.close() - return render_template('group_list_nested.html', grouped_results={}) - return "Database Connection Failed" - -@app.route('/edit_groupname/', methods=['GET', 'POST']) -def edit_groupname(old_groupname): - db = get_db() - if db: - cursor = db.cursor(dictionary=True) - - if request.method == 'POST': - new_groupname = request.form['groupname'] - try: - db.autocommit = False - cursor.execute(""" - UPDATE radgroupreply - SET groupname = %s - WHERE groupname = %s - """, (new_groupname, old_groupname)) - - cursor.execute(""" - UPDATE radusergroup - SET groupname = %s - WHERE groupname = %s - """, (new_groupname, old_groupname)) - - db.commit() - db.autocommit = True - cursor.close() - db.close() - return redirect(url_for('groups')) - except mysql.connector.Error as err: - db.rollback() - db.autocommit = True - cursor.close() - db.close() - return f"Database Error: {err}" - else: - return render_template('edit_groupname.html', old_groupname=old_groupname) - return "Database Connection Failed" - -@app.route('/update_attribute', methods=['POST']) -def update_attribute(): - group_id = request.form['group_id'] - attribute = request.form['attribute'] - op = request.form['op'] - value = request.form['value'] - - db = get_db() - if db: - cursor = db.cursor() - try: - db.autocommit = False - cursor.execute(""" - UPDATE radgroupreply - SET attribute = %s, op = %s, value = %s - WHERE id = %s - """, (attribute, op, value, group_id)) - db.commit() - db.autocommit = True - cursor.close() - return "success" - except mysql.connector.Error as err: - db.rollback() - db.autocommit = True - cursor.close() - return str(err) - except Exception as e: - db.rollback() - db.autocommit = True - cursor.close() - return str(e) - finally: - db.close() - return "Database Connection Failed" - -@app.route('/add_attribute', methods=['POST']) -def add_attribute(): - groupname = request.form['groupname'] - attribute = request.form['attribute'] - op = request.form['op'] - value = request.form['value'] - - db = get_db() - if db: - cursor = db.cursor() - try: - cursor.execute(""" - INSERT INTO radgroupreply (groupname, attribute, op, value) - VALUES (%s, %s, %s, %s) - """, (groupname, attribute, op, value)) - db.commit() - cursor.close() - db.close() - return "success" - except mysql.connector.Error as err: - print(f"Database Error: {err}") - db.rollback() - cursor.close() - db.close() - return str(err) - return "Database Connection Failed" - -@app.route('/edit_attribute/', methods=['GET', 'POST']) -def edit_attribute(group_id): - db = get_db() - if db: - cursor = db.cursor(dictionary=True) - - if request.method == 'POST': - attribute = request.form['attribute'] - op = request.form['op'] - value = request.form['value'] - - try: - db.autocommit = False - cursor.execute(""" - UPDATE radgroupreply - SET attribute = %s, op = %s, value = %s - WHERE id = %s - """, (attribute, op, value, group_id)) - db.commit() - db.autocommit = True - cursor.close() - db.close() - return redirect(url_for('groups')) - except mysql.connector.Error as err: - db.rollback() - db.autocommit = True - cursor.close() - db.close() - return f"Database Error: {err}" - - else: - cursor.execute("SELECT * FROM radgroupreply WHERE id = %s", (group_id,)) - attribute_data = cursor.fetchone() - cursor.close() - db.close() - return render_template('edit_attribute.html', attribute_data=attribute_data) - return "Database Connection Failed" - -@app.route('/add_group', methods=['POST']) -def add_group(): - groupname = request.form['groupname'] - - db = get_db() - if db: - cursor = db.cursor() - try: - cursor.execute("INSERT INTO radgroupreply (groupname, attribute, op, value) VALUES (%s, '', '', '')", (groupname,)) - cursor.execute("INSERT INTO radusergroup (groupname, username) VALUES (%s, '')", (groupname,)) - cursor.execute("INSERT INTO radgroupcheck (groupname, attribute, op, value) VALUES (%s, 'Auth-Type', ':=', 'Accept')", (groupname,)) - db.commit() - cursor.close() - db.close() - return "success" - except mysql.connector.Error as err: - print(f"Database Error: {err}") - db.rollback() - cursor.close() - db.close() - return str(err) - return "Database Connection Failed" - -@app.route('/delete_group_rows/') -def delete_group_rows(groupname): - db = get_db() - if db: - cursor = db.cursor() - try: - cursor.execute("DELETE FROM radgroupreply WHERE groupname = %s", (groupname,)) - cursor.execute("DELETE FROM radusergroup WHERE groupname = %s", (groupname,)) - cursor.execute("DELETE FROM radgroupcheck WHERE groupname = %s", (groupname,)) - db.commit() - cursor.close() - db.close() - return redirect(url_for('groups')) - except mysql.connector.Error as err: - print(f"Database Error: {err}") - db.rollback() - cursor.close() - db.close() - return redirect(url_for('groups')) - return "Database Connection Failed" - -@app.route('/delete_group/') -def delete_group(group_id): - db = get_db() - if db: - cursor = db.cursor() - try: - cursor.execute("DELETE FROM radgroupreply WHERE id = %s", (group_id,)) - cursor.execute("DELETE FROM radgroupcheck WHERE id = %s", (group_id,)) - db.commit() - cursor.close() - db.close() - return redirect(url_for('groups')) - except mysql.connector.Error as err: - print(f"Database Error: {err}") - db.rollback() - cursor.close() - db.close() - return redirect(url_for('groups')) - return "Database Connection Failed" - -@app.route('/add_user', methods=['POST']) -def add_user(): - try: - data = request.get_json() - mac_address = data.get('mac_address') - description = data.get('description') - vlan_id = data.get('vlan_id') - - if not mac_address: - return jsonify({'success': False, 'message': 'MAC Address is required'}), 400 - - db = get_db() - if db is None: - return jsonify({'success': False, 'message': 'Database connection failed'}), 500 - - cursor = db.cursor() - try: - db.autocommit = False - - cursor.execute("SELECT username FROM radcheck WHERE username = %s", (mac_address,)) - if cursor.fetchone(): - cursor.close() - db.close() - return jsonify({'success': False, 'message': 'User with this MAC Address already exists'}), 400 - - cursor.execute(""" - INSERT INTO radcheck (username, attribute, op, value) - VALUES (%s, 'Cleartext-Password', ':=', %s) - """, (mac_address, mac_address)) - - cursor.execute(""" - INSERT INTO rad_description (username, description) - VALUES (%s, %s) - """, (mac_address, description)) - - cursor.execute(""" - INSERT INTO radusergroup (username, groupname) - VALUES (%s, %s) - """, (mac_address, vlan_id)) - - db.commit() - db.autocommit = True - cursor.close() - db.close() - return jsonify({'success': True, 'message': 'User added successfully'}) - - except mysql.connector.Error as err: - print(f"Database Error: {err}") - db.rollback() - db.autocommit = True - cursor.close() - db.close() - return jsonify({'success': False, 'message': f"Database error: {err}"}), 500 - - except Exception as e: - print(f"Error adding user: {e}") - db.rollback() - db.autocommit = True - cursor.close() - db.close() - return jsonify({'success': False, 'message': str(e)}), 500 - finally: - db.close() - except Exception as e: - return jsonify({'success': False, 'message': 'Unknown error'}), 500 +@app.route('/') +def index_redirect(): + return render_template('index.html') if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=8080) \ No newline at end of file diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..cc3ca04 --- /dev/null +++ b/app/config.py @@ -0,0 +1,33 @@ +import os + +class Config: + """Base configuration.""" + DEBUG = False + TESTING = False + SQLALCHEMY_TRACK_MODIFICATIONS = False + SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'default-insecure-key') + OUI_API_URL = os.getenv('OUI_API_URL', 'https://api.maclookup.app/v2/macs/{}') + OUI_API_KEY = os.getenv('OUI_API_KEY', '') + OUI_API_LIMIT_PER_SEC = int(os.getenv('OUI_API_LIMIT_PER_SEC', '2')) + OUI_API_DAILY_LIMIT = int(os.getenv('OUI_API_DAILY_LIMIT', '10000')) + +class DevelopmentConfig(Config): + """Development configuration.""" + DEBUG = True + MYSQL_HOST = os.getenv('MYSQL_HOST', '192.168.60.150') + MYSQL_USER = os.getenv('MYSQL_USER', 'user_92z0Kj') + MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD', '5B3UXZV8vyrB') + MYSQL_DATABASE = os.getenv('MYSQL_DATABASE', 'radius_NIaIuT') + +class ProductionConfig(Config): + """Production configuration.""" + MYSQL_HOST = os.getenv('MYSQL_HOST') + MYSQL_USER = os.getenv('MYSQL_USER') + MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD') + MYSQL_DATABASE = os.getenv('MYSQL_DATABASE') + +# Use the correct config based on environment +if os.getenv('FLASK_ENV') == 'production': + app_config = ProductionConfig +else: + app_config = DevelopmentConfig diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..6a7c3ce --- /dev/null +++ b/app/database.py @@ -0,0 +1,34 @@ +import mysql.connector +from flask import current_app, g +from mysql.connector import pooling + +# Optional: Use a pool if desired +def init_db_pool(): + return pooling.MySQLConnectionPool( + pool_name="mypool", + pool_size=5, + pool_reset_session=True, + host=current_app.config['MYSQL_HOST'], + user=current_app.config['MYSQL_USER'], + password=current_app.config['MYSQL_PASSWORD'], + database=current_app.config['MYSQL_DATABASE'] + ) + +def get_db(): + if 'db' not in g: + if 'db_pool' not in current_app.config: + current_app.config['db_pool'] = init_db_pool() + g.db = current_app.config['db_pool'].get_connection() + return g.db + +def close_db(e=None): + db = g.pop('db', None) + if db is not None: + try: + db.close() # returns connection to the pool + except Exception as err: + print(f"[DB Cleanup] Failed to close DB connection: {err}") + + +def init_app(app): + app.teardown_appcontext(close_db) diff --git a/app/requirements.txt b/app/requirements.txt index a754bdf..6465f07 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,2 +1,5 @@ Flask -mysql-connector-python \ No newline at end of file +mysql-connector-python +requests +BeautifulSoup4 +lxml \ No newline at end of file diff --git a/app/templates/add_user.html b/app/templates/add_user.html deleted file mode 100644 index 7c032c0..0000000 --- a/app/templates/add_user.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - Add User - - -

Add User

-
- -

- -

- -

- -
- Back to User List - - \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 946c9ec..eb8444a 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,35 +1,239 @@ - + - {% block title %}{% endblock %} - + body { + background-color: var(--bg); + color: var(--fg); + font-family: sans-serif; + margin: 0; + padding: 0; + } + + nav { + background-color: var(--th-bg); + padding: 10px; + display: flex; + align-items: center; + } + + nav a { + margin-right: 10px; + text-decoration: none; + padding: 5px 10px; + border: 1px solid #ccc; + border-radius: 5px; + color: var(--fg); + } + + nav a.active { + background-color: var(--cell-bg); + } + + .content { + padding: 20px; + } + + #theme-toggle { + margin-left: auto; + cursor: pointer; + padding: 5px 10px; + border-radius: 5px; + border: 1px solid #ccc; + background-color: var(--cell-bg); + color: var(--fg); + } + + .styled-table { + border-collapse: collapse; + width: 100%; + margin-bottom: 2rem; + background-color: var(--bg); + color: var(--fg); + transition: all 0.3s ease; + } + + .styled-table th, + .styled-table td { + border: 1px solid #444; + padding: 8px; + text-align: left; + } + + .styled-table input, + .styled-table select { + background-color: var(--cell-bg); + color: var(--fg); + border: 1px solid #666; + padding: 4px; + border-radius: 4px; + transition: background-color 0.3s; + } + + .styled-table input:focus, + .styled-table select:focus { + background-color: #555; + outline: none; + } + + .styled-table thead { + background-color: var(--th-bg); + position: sticky; + top: 0; + z-index: 1; + } + + .styled-table tbody tr:hover { + background-color: rgba(255, 255, 255, 0.05); + } + + .icon-button { + background-color: var(--cell-bg); + color: var(--fg); + border: 1px solid #666; + border-radius: 4px; + padding: 4px 8px; + margin: 2px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.2s, transform 0.2s; + } + + .icon-button:hover { + background-color: #555; + transform: scale(1.05); + } + + .icon-button.pulse { + animation: pulse 2s infinite; + } + + @keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(50, 205, 50, 0.4); } + 70% { box-shadow: 0 0 0 10px rgba(50, 205, 50, 0); } + 100% { box-shadow: 0 0 0 0 rgba(50, 205, 50, 0); } + } + + .page-title { + margin-bottom: 1rem; + color: var(--fg); + } + + .merged-cell { + border: none; + } + + .fade-in { + animation: fadeIn 0.5s ease-in; + } + + @keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } + } + + #toast-container { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + } + + .toast { + background-color: #444; + color: #fff; + padding: 10px 16px; + margin-top: 10px; + border-radius: 4px; + box-shadow: 0 0 10px rgba(0,0,0,0.2); + opacity: 0.95; + animation: fadein 0.5s, fadeout 0.5s 2.5s; + } + + @keyframes fadein { + from { opacity: 0; right: 0; } + to { opacity: 0.95; } + } + + @keyframes fadeout { + from { opacity: 0.95; } + to { opacity: 0; } + } + - + -
- {% block content %}{% endblock %} -
+
+ {% block content %}{% endblock %} +
+ +
+ + + + + - \ No newline at end of file + diff --git a/app/templates/edit_attribute.html b/app/templates/edit_attribute.html deleted file mode 100644 index 509de04..0000000 --- a/app/templates/edit_attribute.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - Edit Attribute - - -

Edit Attribute

- -
-
-

- -
-

- -
-

- - -
- - \ No newline at end of file diff --git a/app/templates/edit_group.html b/app/templates/edit_group.html deleted file mode 100644 index 1567d02..0000000 --- a/app/templates/edit_group.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - Edit Group - - -

Edit Group: {{ group.id }}

- -
-
-

- -
-

- -
-

- -
-

- - -
- - - \ No newline at end of file diff --git a/app/templates/edit_groupname.html b/app/templates/edit_groupname.html deleted file mode 100644 index 8a04ccf..0000000 --- a/app/templates/edit_groupname.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - Edit Group Name - - -

Edit Group Name: {{ old_groupname }}

- -
-
-

- - -
- - \ No newline at end of file diff --git a/app/templates/edit_user.html b/app/templates/edit_user.html deleted file mode 100644 index a2bc268..0000000 --- a/app/templates/edit_user.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - Edit User - - -

Edit User: {{ user.mac_address }}

- -
-
-

- -
-

- - -
- - - \ No newline at end of file diff --git a/app/templates/group_list.html b/app/templates/group_list.html deleted file mode 100644 index b97c11d..0000000 --- a/app/templates/group_list.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - Group List - - -

Group List

- - - - - - - - - - - - - - {% for group in results %} - - - - - - - - - {% endfor %} - -
IDGroup NameAttributeOpValueActions
{{ group.id }}{{ group.groupname }}{{ group.attribute }}{{ group.op }}{{ group.value }} - Edit | - Delete -
- - - \ No newline at end of file diff --git a/app/templates/group_list_nested.html b/app/templates/group_list_nested.html index 1aeb72c..46ebb36 100644 --- a/app/templates/group_list_nested.html +++ b/app/templates/group_list_nested.html @@ -1,364 +1,183 @@ {% extends 'base.html' %} - {% block title %}Group List{% endblock %} {% block content %} -

Group List

+

Group List

+ + + + + + + + + + + + + + + + + + + + {% for groupname, attributes in grouped_results.items() %} -
Group NameAttributeOpValueActions
+ + + + +
- - - - - - - - - - - - - - - - {% for attribute in attributes %} - - - - - - - - {% endfor %} - -
Group NameAttributesOpValueActions
- - - - - - - 🗑️ - -
- - - - - 🗑️ -
+ + + + + + + + 🗑️ + + + + {% for attribute in attributes %} + + + + + - - - - - - - - + -{% endblock %} \ No newline at end of file +window.onload = function () { + const scrollPosition = sessionStorage.getItem("scrollPosition"); + if (scrollPosition) { + window.scrollTo(0, parseInt(scrollPosition) - 100); + sessionStorage.removeItem("scrollPosition"); + } +}; + +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 5c94a9d..ec8c902 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,43 +1,125 @@ {% extends 'base.html' %} - {% block title %}FreeRADIUS Manager{% endblock %} {% block content %} -

FreeRADIUS Manager

+

FreeRADIUS Manager

-

Statistics:

-

Total Users: {{ total_users }}

-

Total Groups: {{ total_groups }}

+
+
+ Total Users +

{{ total_users }}

+
+
+ Total Groups +

{{ total_groups }}

+
+
-

SQL Query Tool:

-
-
- -
+

Recent Access Accepts

+
    + {% for entry in latest_accept %} +
  • + {{ entry.username }} + {% if entry.description %} ({{ entry.description }}){% endif %} + — {{ entry.ago }} +
  • + {% endfor %} +
- {% if sql_results %} -

Query Results:

- - - - {% for key in sql_results[0].keys() %} - - {% endfor %} - - - - {% for row in sql_results %} - - {% for value in row.values() %} - - {% endfor %} - - {% endfor %} - -
{{ key }}
{{ value }}
- {% endif %} +

Recent Access Rejects

+
    + {% for entry in latest_reject %} +
  • + {{ entry.username }} + {% if entry.description %} ({{ entry.description }}){% endif %} + — {{ entry.ago }} +
  • + {% endfor %} +
- {% if sql_error %} -

{{ sql_error }}

- {% endif %} -{% endblock %} \ No newline at end of file +
+ +

MAC Vendor Lookup

+
+ + +
+ +

+
+
+
+
+{% endblock %}
diff --git a/app/templates/stats.html b/app/templates/stats.html
new file mode 100644
index 0000000..efdb0aa
--- /dev/null
+++ b/app/templates/stats.html
@@ -0,0 +1,100 @@
+{% extends 'base.html' %}
+{% block title %}Authentication Stats{% endblock %}
+
+{% block content %}
+

Authentication Stats

+ +
+
+

Last Access-Accept Events

+ + + + + + + + + + + {% for entry in accept_entries %} + + + + + + + {% endfor %} + +
MAC AddressDescriptionVendorTime
{{ entry.username }}{{ entry.description or '' }}{{ entry.vendor }}{{ entry.ago }}
+
+ +
+

Last Access-Reject Events

+ + + + + + + + + + + + {% for entry in reject_entries %} + + + + + + + + {% endfor %} + +
MAC AddressDescriptionVendorTimeActions
{{ entry.username }}{{ entry.description or '' }}{{ entry.vendor }}{{ entry.ago }} + {% if entry.already_exists %} + Already exists in {{ entry.existing_vlan or 'unknown VLAN' }} + {% else %} +
+ + + +
+ {% endif %} +
+
+
+ + +{% endblock %} diff --git a/app/templates/user_list.html b/app/templates/user_list.html deleted file mode 100644 index 95ec148..0000000 --- a/app/templates/user_list.html +++ /dev/null @@ -1,364 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}User List{% endblock %} - -{% block content %} -

User List

- - {% for username, attributes in grouped_users.items() %} - - - - - - - - - - - - - - - - - {% for attribute in attributes %} - - - - - - - - {% endfor %} - -
User NameAttributesOpValueActions
- - - - - - - 🗑️ - -
- - - - - 🗑️ -
- {% endfor %} - - - - - - - - - - - - - - - - - - -
User NameAttributesOpValueActions
- - - -
- - -
- - -
- - - - -{% endblock %} \ No newline at end of file diff --git a/app/templates/user_list_inline_edit.html b/app/templates/user_list_inline_edit.html index 82b43d4..4a7dc87 100644 --- a/app/templates/user_list_inline_edit.html +++ b/app/templates/user_list_inline_edit.html @@ -1,262 +1,179 @@ {% extends 'base.html' %} - {% block title %}User List{% endblock %} {% block content %} -

User List

+

User List

- - - - - - - - - - - {% for user in results %} - - - - - - - {% endfor %} - - - - -
MAC AddressDescriptionVLAN IDActions
- - - - - - - - 🗑️ -
- -
+ + + + + + + + + + + + + + + + + + + - -
-
MAC Address + Vendor + + DescriptionGroupActions
(auto) + + + + +
- - - - - - - - - - - - - - -
MAC AddressDescriptionVLAN ID
- -
- -
- - -
- + {% for row in results %} + + + {{ row.vendor or 'Unknown Vendor' }} + + + + + + + + + 🗑️ + + + {% endfor %} + + - - - -{% endblock %} \ No newline at end of file +window.onload = function () { + const scroll = sessionStorage.getItem("scrollPosition"); + if (scroll) { + window.scrollTo(0, parseInt(scroll) - 100); + sessionStorage.removeItem("scrollPosition"); + } +}; + +{% endblock %} diff --git a/app/views/__init__.py b/app/views/__init__.py new file mode 100644 index 0000000..b0544e8 --- /dev/null +++ b/app/views/__init__.py @@ -0,0 +1,3 @@ +from .index_views import index # not index_views +from .user_views import user +from .group_views import group diff --git a/app/views/__pycache__/__init__.cpython-39.pyc b/app/views/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39ea96173c40abf10c70c3cf2fae93d3bd62ca90 GIT binary patch literal 234 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 literal 0 HcmV?d00001 diff --git a/app/views/__pycache__/index_views.cpython-39.pyc b/app/views/__pycache__/index_views.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ca0b692bcc3e9beffeea373ab8ab5c7edb13d44 GIT binary patch literal 5320 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 literal 0 HcmV?d00001 diff --git a/app/views/group_views.py b/app/views/group_views.py new file mode 100644 index 0000000..19b67c5 --- /dev/null +++ b/app/views/group_views.py @@ -0,0 +1,201 @@ +from flask import Blueprint, render_template, request, redirect, url_for, jsonify +from database import get_db +import mysql.connector + +group = Blueprint('group', __name__) + +@group.route('/groups') +def groups(): + db = get_db() + if db: + cursor = db.cursor() + try: + cursor.execute("SELECT DISTINCT groupname FROM radgroupcheck") + group_names = [row[0] for row in cursor.fetchall()] + grouped_results = {} + + for groupname in group_names: + cursor.execute("SELECT id, attribute, op, value FROM radgroupreply WHERE groupname = %s", (groupname,)) + attributes = cursor.fetchall() + grouped_results[groupname] = [ + {'id': row[0], 'attribute': row[1], 'op': row[2], 'value': row[3]} + for row in attributes + ] + + cursor.close() + db.close() + return render_template('group_list_nested.html', grouped_results=grouped_results) + + except mysql.connector.Error as err: + print(f"Database Error: {err}") + cursor.close() + db.close() + return render_template('group_list_nested.html', grouped_results={}) + return "Database Connection Failed" + +@group.route('/save_group', methods=['POST']) +def save_group(): + data = request.get_json() + groupname = data.get('groupname') + attributes = data.get('attributes') + + if not groupname or not attributes: + return jsonify({'error': 'Group name and attributes are required'}), 400 + + db = get_db() + cursor = db.cursor() + + try: + # Prevent duplicates + cursor.execute("SELECT 1 FROM radgroupcheck WHERE groupname = %s", (groupname,)) + if cursor.fetchone(): + return jsonify({'error': f'Group name "{groupname}" already exists'}), 400 + + # Insert baseline group rule + cursor.execute(""" + INSERT INTO radgroupcheck (groupname, attribute, op, value) + VALUES (%s, 'Auth-Type', ':=', 'Accept') + """, (groupname,)) + + # Insert attributes + for attr in attributes: + cursor.execute(""" + INSERT INTO radgroupreply (groupname, attribute, op, value) + VALUES (%s, %s, %s, %s) + """, (groupname, attr['attribute'], attr['op'], attr['value'])) + + db.commit() + cursor.close() + db.close() + return jsonify({'success': True}) + + except Exception as e: + db.rollback() + cursor.close() + db.close() + return jsonify({'error': str(e)}), 500 + +@group.route('/update_group_name', methods=['POST']) +def update_group_name(): + old_groupname = request.form.get('oldGroupName') + new_groupname = request.form.get('newGroupName') + + if not old_groupname or not new_groupname: + return jsonify({'error': 'Both old and new group names are required'}), 400 + + db = get_db() + cursor = db.cursor() + try: + cursor.execute("UPDATE radgroupcheck SET groupname=%s WHERE groupname=%s", (new_groupname, old_groupname)) + cursor.execute("UPDATE radgroupreply SET groupname=%s WHERE groupname=%s", (new_groupname, old_groupname)) + cursor.execute("UPDATE radusergroup SET groupname=%s WHERE groupname=%s", (new_groupname, old_groupname)) + db.commit() + cursor.close() + db.close() + return jsonify({'success': True}), 200 + except Exception as e: + db.rollback() + cursor.close() + db.close() + return jsonify({'error': str(e)}), 500 + +@group.route('/update_attribute', methods=['POST']) +def update_attribute(): + attribute_id = request.form.get('attributeId') + attribute = request.form.get('attribute') + op = request.form.get('op') + value = request.form.get('value') + + if not attribute_id or not attribute or not op or not value: + return jsonify({'error': 'All fields are required'}), 400 + + db = get_db() + cursor = db.cursor() + try: + cursor.execute(""" + UPDATE radgroupreply + SET attribute=%s, op=%s, value=%s + WHERE id=%s + """, (attribute, op, value, attribute_id)) + db.commit() + cursor.close() + db.close() + return jsonify({'success': True}), 200 + except Exception as e: + db.rollback() + cursor.close() + db.close() + return jsonify({'error': str(e)}), 500 + +@group.route('/delete_group_rows/') +def delete_group_rows(groupname): + db = get_db() + if db: + cursor = db.cursor() + try: + cursor.execute("DELETE FROM radgroupreply WHERE groupname = %s", (groupname,)) + cursor.execute("DELETE FROM radusergroup WHERE groupname = %s", (groupname,)) + cursor.execute("DELETE FROM radgroupcheck WHERE groupname = %s", (groupname,)) + db.commit() + cursor.close() + db.close() + return redirect(url_for('group.groups')) + except mysql.connector.Error as err: + db.rollback() + cursor.close() + db.close() + return redirect(url_for('group.groups')) + return "Database Connection Failed" + +@group.route('/delete_group/') +def delete_group(group_id): + db = get_db() + if db: + cursor = db.cursor() + try: + cursor.execute("DELETE FROM radgroupreply WHERE id = %s", (group_id,)) + cursor.execute("DELETE FROM radgroupcheck WHERE id = %s", (group_id,)) + db.commit() + cursor.close() + db.close() + return redirect(url_for('group.groups')) + except mysql.connector.Error as err: + db.rollback() + cursor.close() + db.close() + return redirect(url_for('group.groups')) + return "Database Connection Failed" + +@group.route('/duplicate_group', methods=['POST']) +def duplicate_group(): + groupname = request.form.get('groupname') + if not groupname: + return jsonify({'error': 'Group name is required'}), 400 + + db = get_db() + cursor = db.cursor() + + try: + cursor.execute("SELECT attribute, op, value FROM radgroupreply WHERE groupname = %s", (groupname,)) + attributes = cursor.fetchall() + if not attributes: + return jsonify({'error': f'Group "{groupname}" not found or has no attributes'}), 404 + + new_groupname = f"Copy of {groupname}" + count = 1 + while True: + cursor.execute("SELECT 1 FROM radgroupcheck WHERE groupname = %s", (new_groupname,)) + if not cursor.fetchone(): + break + count += 1 + new_groupname = f"Copy of {groupname} ({count})" + + attr_list = [{'attribute': row[0], 'op': row[1], 'value': row[2]} for row in attributes] + cursor.close() + db.close() + return jsonify({'new_groupname': new_groupname, 'attributes': attr_list}) + + except Exception as e: + cursor.close() + db.close() + return jsonify({'error': str(e)}), 500 diff --git a/app/views/index_views.py b/app/views/index_views.py new file mode 100644 index 0000000..d00336d --- /dev/null +++ b/app/views/index_views.py @@ -0,0 +1,187 @@ +from flask import Blueprint, render_template, request, jsonify +from database import get_db +from datetime import datetime +import requests + +index = Blueprint('index', __name__) +OUI_API_URL = 'https://api.maclookup.app/v2/macs/{}' + + +def time_ago(dt): + now = datetime.now() + diff = now - dt + seconds = int(diff.total_seconds()) + if seconds < 60: + return f"{seconds}s ago" + elif seconds < 3600: + return f"{seconds//60}m{seconds%60}s ago" + elif seconds < 86400: + return f"{seconds//3600}h{(seconds%3600)//60}m ago" + else: + return f"{seconds//86400}d{(seconds%86400)//3600}h ago" + + +def lookup_vendor(mac): + prefix = mac.replace(":", "").replace("-", "").upper()[:6] + db = get_db() + cursor = db.cursor(dictionary=True) + + # Try local DB first + cursor.execute("SELECT vendor_name FROM mac_vendor_cache WHERE mac_prefix = %s", (prefix,)) + result = cursor.fetchone() + + if result and result['vendor_name'] != "Unknown Vendor": + return {"source": "local", "prefix": prefix, "vendor": result['vendor_name']} + + # Try API fallback + try: + api_url = OUI_API_URL.format(mac) + r = requests.get(api_url, timeout=3) + if r.status_code == 200: + data = r.json() + vendor = data.get("company", "Unknown Vendor") + + # Save to DB + cursor.execute(""" + INSERT INTO mac_vendor_cache (mac_prefix, vendor_name, last_updated) + VALUES (%s, %s, NOW()) + ON DUPLICATE KEY UPDATE vendor_name = VALUES(vendor_name), last_updated = NOW() + """, (prefix, vendor)) + db.commit() + return {"source": "api", "prefix": prefix, "vendor": vendor, "raw": data} + else: + return {"source": "api", "prefix": prefix, "error": f"API returned status {r.status_code}", "raw": r.text} + except Exception as e: + return {"source": "api", "prefix": prefix, "error": str(e)} + finally: + cursor.close() + + +@index.route('/') +def homepage(): + db = get_db() + latest_accept = [] + latest_reject = [] + total_users = 0 + total_groups = 0 + + if db: + cursor = db.cursor(dictionary=True) + + cursor.execute("SELECT COUNT(*) AS count FROM radcheck") + total_users = cursor.fetchone()['count'] + + cursor.execute("SELECT COUNT(DISTINCT groupname) AS count FROM radgroupcheck") + total_groups = cursor.fetchone()['count'] + + cursor.execute(""" + SELECT p.username, d.description, p.reply, p.authdate + FROM radpostauth p + LEFT JOIN rad_description d ON p.username = d.username + WHERE p.reply = 'Access-Accept' + ORDER BY p.authdate DESC LIMIT 5 + """) + latest_accept = cursor.fetchall() + for row in latest_accept: + row['ago'] = time_ago(row['authdate']) + + cursor.execute(""" + SELECT p.username, d.description, p.reply, p.authdate + FROM radpostauth p + LEFT JOIN rad_description d ON p.username = d.username + WHERE p.reply = 'Access-Reject' + ORDER BY p.authdate DESC LIMIT 5 + """) + latest_reject = cursor.fetchall() + for row in latest_reject: + row['ago'] = time_ago(row['authdate']) + + cursor.close() + db.close() + + return render_template('index.html', + total_users=total_users, + total_groups=total_groups, + latest_accept=latest_accept, + latest_reject=latest_reject) + + +@index.route('/stats') +def stats(): + db = get_db() + accept_entries = [] + reject_entries = [] + available_groups = [] + + if db: + cursor = db.cursor(dictionary=True) + + # Fetch available VLANs + cursor.execute("SELECT DISTINCT groupname FROM radgroupcheck ORDER BY groupname") + available_groups = [row['groupname'] for row in cursor.fetchall()] + + # Get existing users and map to group + cursor.execute(""" + SELECT r.username, g.groupname + FROM radcheck r + LEFT JOIN radusergroup g ON r.username = g.username + """) + existing_user_map = { + row['username'].replace(":", "").replace("-", "").upper(): row['groupname'] + for row in cursor.fetchall() + } + + # Access-Reject entries + cursor.execute(""" + SELECT p.username, d.description, p.reply, p.authdate + FROM radpostauth p + LEFT JOIN rad_description d ON p.username = d.username + WHERE p.reply = 'Access-Reject' + ORDER BY p.authdate DESC LIMIT 25 + """) + reject_entries = cursor.fetchall() + for row in reject_entries: + normalized = row['username'].replace(":", "").replace("-", "").upper() + row['vendor'] = lookup_vendor(row['username'])['vendor'] + row['ago'] = time_ago(row['authdate']) + + if normalized in existing_user_map: + row['already_exists'] = True + row['existing_vlan'] = existing_user_map[normalized] + else: + row['already_exists'] = False + row['existing_vlan'] = None + print(f"⚠ Not found in radcheck: {row['username']} → {normalized}") + + # Access-Accept entries + cursor.execute(""" + SELECT p.username, d.description, p.reply, p.authdate + FROM radpostauth p + LEFT JOIN rad_description d ON p.username = d.username + WHERE p.reply = 'Access-Accept' + ORDER BY p.authdate DESC LIMIT 25 + """) + accept_entries = cursor.fetchall() + for row in accept_entries: + row['vendor'] = lookup_vendor(row['username'])['vendor'] + row['ago'] = time_ago(row['authdate']) + + cursor.close() + db.close() + + return render_template('stats.html', + accept_entries=accept_entries, + reject_entries=reject_entries, + available_groups=available_groups) + + + + + +@index.route('/lookup_mac', methods=['POST']) +def lookup_mac(): + mac = request.form.get('mac', '').strip() + if not mac: + return jsonify({"error": "MAC address is required"}), 400 + + return jsonify(lookup_vendor(mac)) diff --git a/app/views/user_views.py b/app/views/user_views.py new file mode 100644 index 0000000..14590f8 --- /dev/null +++ b/app/views/user_views.py @@ -0,0 +1,276 @@ +from flask import Blueprint, render_template, request, redirect, url_for, jsonify, flash +from database import get_db +import mysql.connector, os, time, requests + +user = Blueprint('user', __name__) # ✅ Blueprint name = "user" + +@user.route('/user_list') +def user_list(): + db = get_db() + if db is None: + return "Database connection failed", 500 + + cursor = db.cursor(dictionary=True) + try: + # Get user info + cursor.execute(""" + SELECT + r.username AS mac_address, + rd.description AS description, + ug.groupname AS vlan_id, + mvc.vendor_name AS vendor + FROM radcheck r + LEFT JOIN radusergroup ug ON r.username = ug.username + LEFT JOIN rad_description rd ON r.username = rd.username + LEFT JOIN mac_vendor_cache mvc ON UPPER(REPLACE(REPLACE(r.username, ':', ''), '-', '')) LIKE CONCAT(mvc.mac_prefix, '%') + """) + results = cursor.fetchall() + + # Get available groups + cursor.execute("SELECT groupname FROM radgroupcheck") + groups = [{'groupname': row['groupname']} for row in cursor.fetchall()] + + cursor.close() + db.close() + return render_template('user_list_inline_edit.html', results=results, groups=groups) + + except mysql.connector.Error as e: + print(f"Database error: {e}") + cursor.close() + db.close() + return "Database error", 500 + + +@user.route('/update_user', methods=['POST']) +def update_user(): + mac_address = request.form['mac_address'] + description = request.form['description'] + vlan_id = request.form['vlan_id'] + new_mac_address = request.form.get('new_mac_address') + + db = get_db() + if db: + cursor = db.cursor() + try: + db.autocommit = False + + if new_mac_address and new_mac_address != mac_address: + cursor.execute(""" + UPDATE radcheck + SET username = %s, value = %s + WHERE username = %s + """, (new_mac_address, new_mac_address, mac_address)) + + cursor.execute(""" + UPDATE rad_description + SET username = %s, description = %s + WHERE username = %s + """, (new_mac_address, description, mac_address)) + + cursor.execute(""" + UPDATE radusergroup + SET username = %s, groupname = %s + WHERE username = %s + """, (new_mac_address, vlan_id, mac_address)) + else: + cursor.execute(""" + UPDATE rad_description + SET description = %s + WHERE username = %s + """, (description, mac_address)) + + cursor.execute(""" + UPDATE radusergroup + SET groupname = %s + WHERE username = %s + """, (vlan_id, mac_address)) + + db.commit() + db.autocommit = True + cursor.close() + return "success" + + except Exception as e: + db.rollback() + db.autocommit = True + cursor.close() + return str(e) + finally: + db.close() + return "Database Connection Failed" + + +@user.route('/delete_user/') +def delete_user(mac_address): + db = get_db() + if db: + cursor = db.cursor() + try: + db.autocommit = False + cursor.execute("DELETE FROM rad_description WHERE username = %s", (mac_address,)) + cursor.execute("DELETE FROM radcheck WHERE username = %s", (mac_address,)) + cursor.execute("DELETE FROM radusergroup WHERE username = %s", (mac_address,)) + db.commit() + cursor.close() + db.close() + return redirect(url_for('user.user_list')) + except mysql.connector.Error as err: + print(f"Database Error: {err}") + db.rollback() + cursor.close() + db.close() + return redirect(url_for('user.user_list')) + return "Database Connection Failed" + + +@user.route('/add_user', methods=['POST']) +def add_user(): + try: + data = request.get_json() + mac_address = data.get('mac_address') + description = data.get('description') + vlan_id = data.get('vlan_id') + + if not mac_address: + return jsonify({'success': False, 'message': 'MAC Address is required'}), 400 + + db = get_db() + if db is None: + return jsonify({'success': False, 'message': 'Database connection failed'}), 500 + + cursor = db.cursor() + try: + db.autocommit = False + + cursor.execute("SELECT username FROM radcheck WHERE username = %s", (mac_address,)) + if cursor.fetchone(): + return jsonify({'success': False, 'message': 'User already exists'}), 400 + + cursor.execute(""" + INSERT INTO radcheck (username, attribute, op, value) + VALUES (%s, 'Cleartext-Password', ':=', %s) + """, (mac_address, mac_address)) + + cursor.execute(""" + INSERT INTO rad_description (username, description) + VALUES (%s, %s) + """, (mac_address, description)) + + cursor.execute(""" + INSERT INTO radusergroup (username, groupname) + VALUES (%s, %s) + """, (mac_address, vlan_id)) + + db.commit() + db.autocommit = True + cursor.close() + db.close() + return jsonify({'success': True, 'message': 'User added successfully'}) + + except Exception as e: + db.rollback() + db.autocommit = True + cursor.close() + db.close() + return jsonify({'success': False, 'message': str(e)}), 500 + except Exception: + return jsonify({'success': False, 'message': 'Unknown error'}), 500 + +@user.route('/add_from_reject', methods=['POST']) +def add_from_reject(): + username = request.form.get('username') + groupname = request.form.get('groupname') + + if not username or not groupname: + flash("Missing MAC address or group", "error") + return redirect(url_for('index.stats')) + + db = get_db() + cursor = db.cursor() + try: + db.autocommit = False + + # Check if already exists + cursor.execute("SELECT username FROM radcheck WHERE username = %s", (username,)) + if cursor.fetchone(): + flash(f"{username} already exists", "info") + else: + cursor.execute(""" + INSERT INTO radcheck (username, attribute, op, value) + VALUES (%s, 'Cleartext-Password', ':=', %s) + """, (username, username)) + + cursor.execute(""" + INSERT INTO rad_description (username, description) + VALUES (%s, '') + """, (username,)) + + cursor.execute(""" + INSERT INTO radusergroup (username, groupname) + VALUES (%s, %s) + """, (username, groupname)) + + db.commit() + flash(f"{username} added to group {groupname}", "success") + + except Exception as e: + db.rollback() + flash(f"Error: {str(e)}", "error") + finally: + db.autocommit = True + cursor.close() + db.close() + + return redirect(url_for('index.stats')) + +@user.route('/refresh_vendors', methods=['POST']) +def refresh_vendors(): + db = get_db() + cursor = db.cursor(dictionary=True) + + api_url = os.getenv('MACLOOKUP_API_URL', 'https://api.maclookup.app/v2/macs/{}').strip('"') + api_key = os.getenv('MACLOOKUP_API_KEY', '').strip('"') + limit = int(os.getenv('MACLOOKUP_RATE_LIMIT', 2)) + headers = {'Authorization': f'Bearer {api_key}'} if api_key else {} + + cursor.execute(""" + SELECT r.username + FROM radcheck r + LEFT JOIN mac_vendor_cache m ON UPPER(REPLACE(REPLACE(r.username, ':', ''), '-', '')) LIKE CONCAT(m.mac_prefix, '%') + WHERE m.vendor_name IS NULL OR m.vendor_name = 'Unknown Vendor' + LIMIT 5 + """) + entries = cursor.fetchall() + + if not entries: + cursor.close() + db.close() + return jsonify({"success": True, "updated": 0, "remaining": False}) + + updated = 0 + for entry in entries: + mac = entry['username'] + prefix = mac.replace(':', '').replace('-', '').upper()[:6] + + try: + r = requests.get(api_url.format(mac), headers=headers, timeout=3) + if r.status_code == 200: + data = r.json() + vendor = data.get("company", "not found") + + cursor.execute(""" + INSERT INTO mac_vendor_cache (mac_prefix, vendor_name, last_updated) + VALUES (%s, %s, NOW()) + ON DUPLICATE KEY UPDATE vendor_name = VALUES(vendor_name), last_updated = NOW() + """, (prefix, vendor)) + db.commit() + updated += 1 + except Exception as e: + print(f"Error for {mac}: {e}") + + time.sleep(1 / limit) + + cursor.close() + db.close() + + return jsonify({"success": True, "updated": updated, "remaining": True}) diff --git a/docker-compose.yml b/docker-compose.yml index 2226796..d9760b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ ---- +version: '3.8' services: app: @@ -10,6 +10,16 @@ services: environment: - FLASK_APP=app.py - FLASK_ENV=development + - MYSQL_HOST=192.168.60.150 + - MYSQL_USER=user_92z0Kj + - MYSQL_PASSWORD=5B3UXZV8vyrB + - MYSQL_DATABASE=radius_NIaIuT + - FLASK_SECRET_KEY=default-insecure-key + - PYTHONPATH=/app + - MACLOOKUP_RATE_LIMIT=2 + - MACLOOKUP_API_KEY="" # if using a key later + - MACLOOKUP_API_URL="https://api.maclookup.app/v2/macs/{}" + restart: no nginx: build: @@ -18,4 +28,5 @@ services: ports: - "8080:80" depends_on: - - app \ No newline at end of file + - app + restart: always