diff --git a/app/__pycache__/config.cpython-39.pyc b/app/__pycache__/config.cpython-39.pyc new file mode 100644 index 0000000..60185c6 Binary files /dev/null and b/app/__pycache__/config.cpython-39.pyc differ diff --git a/app/__pycache__/database.cpython-39.pyc b/app/__pycache__/database.cpython-39.pyc new file mode 100644 index 0000000..93a2c49 Binary files /dev/null and b/app/__pycache__/database.cpython-39.pyc differ diff --git a/app/app.py b/app/app.py index 96ecd2d..d5a0641 100644 --- a/app/app.py +++ b/app/app.py @@ -1,531 +1,30 @@ -from flask import Flask, render_template, request, redirect, url_for, jsonify -import mysql.connector -import json +from flask import Flask, redirect, url_for, render_template +from views.index_views import index +from views.user_views import user +from views.group_views import group +from config import app_config +from database import init_app app = Flask(__name__) +app.config.from_object(app_config) -DB_CONFIG = { - 'host': '192.168.60.150', - 'user': 'user_92z0Kj', - 'password': '5B3UXZV8vyrB', - 'database': 'radius_NIaIuT' -} +init_app(app) -def get_db(): - try: - db = mysql.connector.connect(**DB_CONFIG) - return db - except mysql.connector.Error as err: - print(f"Database Connection Error: {err}") - return None - -@app.route('/', methods=['GET', 'POST']) -def index(): - sql_results = None - sql_error = None - total_users = 0 - total_groups = 0 - - db = get_db() - if db: - cursor = db.cursor(dictionary=True) - try: - cursor.execute("SELECT COUNT(DISTINCT username) as total FROM radcheck;") - total_users = cursor.fetchone()['total'] - - cursor.execute("SELECT COUNT(DISTINCT groupname) as total FROM radgroupreply;") - total_groups = cursor.fetchone()['total'] - - except mysql.connector.Error as err: - print(f"Error fetching counts: {err}") - - cursor.close() - db.close() - - return render_template('index.html', total_users=total_users, total_groups=total_groups, sql_results=sql_results, sql_error=sql_error) - -@app.route('/sql', methods=['POST']) -def sql(): - sql_results = None - sql_error = None - sql_query = request.form['query'] - - db = get_db() - if db: - try: - cursor = db.cursor(dictionary=True) - cursor.execute(sql_query) - sql_results = cursor.fetchall() - cursor.close() - db.close() - except mysql.connector.Error as err: - sql_error = str(err) - except Exception as e: - sql_error = str(e) - - total_users = 0 - total_groups = 0 - - db = get_db() - if db: - cursor = db.cursor(dictionary=True) - try: - cursor.execute("SELECT COUNT(DISTINCT username) as total FROM radcheck;") - total_users = cursor.fetchone()['total'] - - cursor.execute("SELECT COUNT(DISTINCT groupname) as total FROM radgroupreply;") - total_groups = cursor.fetchone()['total'] - - except mysql.connector.Error as err: - print(f"Error fetching counts: {err}") - - cursor.close() - db.close() - - return render_template('index.html', total_users=total_users, total_groups=total_groups, sql_results=sql_results, sql_error=sql_error) +app.register_blueprint(index) +app.register_blueprint(user, url_prefix='/user') +app.register_blueprint(group, url_prefix='/group') @app.route('/user_list') -def user_list(): - db = get_db() - if db is None: - return "Database connection failed", 500 - - cursor = db.cursor(dictionary=True) - try: - cursor.execute(""" - SELECT - r.username AS mac_address, - rd.description AS description, - ug.groupname AS vlan_id - FROM radcheck r - LEFT JOIN radusergroup ug ON r.username = ug.username - LEFT JOIN rad_description rd ON r.username = rd.username - """) - results = cursor.fetchall() - print("Results:", results) - - cursor.execute("SELECT groupname FROM radgroupcheck") - groups = cursor.fetchall() - groups = [{'groupname': row['groupname']} for row in groups] - print("Groups:", groups) - - 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 - -@app.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') - - print(f"Update request received: mac_address={mac_address}, description={description}, vlan_id={vlan_id}, new_mac_address={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: - print("Updating MAC address...") - # Update radcheck - cursor.execute(""" - UPDATE radcheck - SET username = %s, value = %s - WHERE username = %s - """, (new_mac_address, new_mac_address, mac_address)) - print(f"radcheck update affected {cursor.rowcount} rows.") - - # Update rad_description - cursor.execute(""" - UPDATE rad_description - SET username = %s, description = %s - WHERE username = %s - """, (new_mac_address, description, mac_address)) - print(f"rad_description update affected {cursor.rowcount} rows.") - - # Update radusergroup - cursor.execute(""" - UPDATE radusergroup - SET username = %s, groupname = %s - WHERE username = %s - """, (new_mac_address, vlan_id, mac_address)) - print(f"radusergroup update affected {cursor.rowcount} rows.") - - mac_address = new_mac_address - else: - print("Updating description and VLAN...") - # Update rad_description - cursor.execute(""" - UPDATE rad_description - SET description = %s - WHERE username = %s - """, (description, mac_address)) - print(f"rad_description update affected {cursor.rowcount} rows.") - - # Update radusergroup - cursor.execute(""" - UPDATE radusergroup - SET groupname = %s - WHERE username = %s - """, (vlan_id, mac_address)) - print(f"radusergroup update affected {cursor.rowcount} rows.") - - if cursor.rowcount > 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 0000000..39ea961 Binary files /dev/null and b/app/views/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/views/__pycache__/group_views.cpython-39.pyc b/app/views/__pycache__/group_views.cpython-39.pyc new file mode 100644 index 0000000..c15c7e7 Binary files /dev/null and b/app/views/__pycache__/group_views.cpython-39.pyc differ 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 0000000..0ca0b69 Binary files /dev/null and b/app/views/__pycache__/index_views.cpython-39.pyc differ diff --git a/app/views/__pycache__/user_views.cpython-39.pyc b/app/views/__pycache__/user_views.cpython-39.pyc new file mode 100644 index 0000000..27de276 Binary files /dev/null and b/app/views/__pycache__/user_views.cpython-39.pyc differ 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