diff --git a/app/__pycache__/app.cpython-39.pyc b/app/__pycache__/app.cpython-39.pyc index c8cda55..77c12e5 100644 Binary files a/app/__pycache__/app.cpython-39.pyc and b/app/__pycache__/app.cpython-39.pyc differ diff --git a/app/__pycache__/config.cpython-39.pyc b/app/__pycache__/config.cpython-39.pyc index 73aca10..cb89a6f 100644 Binary files a/app/__pycache__/config.cpython-39.pyc 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 index 93a2c49..e9c8cba 100644 Binary files a/app/__pycache__/database.cpython-39.pyc and b/app/__pycache__/database.cpython-39.pyc differ diff --git a/app/__pycache__/wsgi.cpython-39.pyc b/app/__pycache__/wsgi.cpython-39.pyc index fa54b13..0f350fc 100644 Binary files a/app/__pycache__/wsgi.cpython-39.pyc and b/app/__pycache__/wsgi.cpython-39.pyc differ diff --git a/app/app.py b/app/app.py index 968c73e..1162544 100644 --- a/app/app.py +++ b/app/app.py @@ -2,23 +2,15 @@ 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 as config_class -from database import init_app +from config import app_config -import logging, os, pytz + +import logging, os from logging.handlers import RotatingFileHandler -# Instantiate config class -app_config = config_class() - app = Flask(__name__) app.config.from_object(app_config) -app.config['TZ'] = pytz.timezone(app.config['APP_TIMEZONE']) -init_app(app) -app.config['TZ'] = pytz.timezone(app.config['APP_TIMEZONE']) - -# Logging if app.config.get('LOG_TO_FILE'): log_file = app.config.get('LOG_FILE_PATH', '/app/logs/app.log') os.makedirs(os.path.dirname(log_file), exist_ok=True) @@ -39,7 +31,8 @@ def legacy_user_list(): @app.route('/groups') def legacy_group_list(): - return redirect(url_for('group.groups')) + return redirect(url_for('group.group_list')) + @app.route('/') def index_redirect(): diff --git a/app/config.py b/app/config.py index 132adf8..afe5756 100644 --- a/app/config.py +++ b/app/config.py @@ -1,10 +1,16 @@ -import os, pytz +import os class Config: DEBUG = False TESTING = False - SQLALCHEMY_TRACK_MODIFICATIONS = False - SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'default-insecure-key') + SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'insecure-default-key') + + # Database connection info + DB_HOST = os.getenv('DB_HOST', 'localhost') + DB_PORT = int(os.getenv('DB_PORT', '3306')) + DB_USER = os.getenv('DB_USER', 'radiususer') + DB_PASSWORD = os.getenv('DB_PASSWORD', 'radiuspass') + DB_NAME = os.getenv('DB_NAME', 'radius') # Logging LOG_TO_FILE = os.getenv('LOG_TO_FILE', 'false').lower() == 'true' @@ -14,33 +20,18 @@ class Config: 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')) + OUI_API_DAILY_LIMIT = int(os.getenv('OUI_API_DAILY_LIMIT', '10000')) - # These get set in __init__ + # Timezone APP_TIMEZONE = os.getenv('APP_TIMEZONE', 'UTC') - TZ = pytz.timezone(APP_TIMEZONE) - - def __init__(self): - tz_name = os.getenv('APP_TIMEZONE', 'UTC') - self.APP_TIMEZONE = tz_name - self.TZ = pytz.timezone(tz_name) 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') + DEBUG = False -# Use the correct config based on environment +# Runtime selection if os.getenv('FLASK_ENV') == 'production': app_config = ProductionConfig else: diff --git a/app/database.py b/app/database.py index 6a7c3ce..8806f78 100644 --- a/app/database.py +++ b/app/database.py @@ -1,34 +1,20 @@ 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() + g.db = mysql.connector.connect( + host=current_app.config['DB_HOST'], + port=current_app.config['DB_PORT'], + user=current_app.config['DB_USER'], + password=current_app.config['DB_PASSWORD'], + database=current_app.config['DB_NAME'] + ) 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) + @app.teardown_appcontext + def close_connection(exception): + db = g.pop('db', None) + if db is not None: + db.close() diff --git a/app/db_interface.py b/app/db_interface.py new file mode 100644 index 0000000..e47dc2f --- /dev/null +++ b/app/db_interface.py @@ -0,0 +1,272 @@ +from flask import current_app +import mysql.connector +import datetime +import requests +import time +import os + +def get_connection(): + return mysql.connector.connect( + host=current_app.config['DB_HOST'], + user=current_app.config['DB_USER'], + password=current_app.config['DB_PASSWORD'], + database=current_app.config['DB_NAME'] + ) + + +def get_all_users(): + conn = get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT u.*, g.vlan_id AS group_vlan_id, g.description AS group_description, + mv.vendor_name + FROM users u + LEFT JOIN groups g ON u.vlan_id = g.vlan_id + LEFT JOIN mac_vendors mv + ON SUBSTRING(REPLACE(REPLACE(u.mac_address, ':', ''), '-', ''), 1, 6) = mv.mac_prefix + """) + users = cursor.fetchall() + cursor.close() + conn.close() + return users + + + +def get_all_groups(): + conn = get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT g.*, ( + SELECT COUNT(*) FROM users WHERE vlan_id = g.vlan_id + ) AS user_count + FROM groups g + ORDER BY g.vlan_id + """) + groups = cursor.fetchall() + cursor.close() + conn.close() + return groups + + + +def get_group_by_name(name): + conn = get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM groups WHERE name = %s", (name,)) + group = cursor.fetchone() + cursor.close() + conn.close() + return group + + +def add_group(vlan_id, description): + conn = get_connection() + cursor = conn.cursor() + cursor.execute("INSERT INTO groups (vlan_id, description) VALUES (%s, %s)", (vlan_id, description)) + conn.commit() + cursor.close() + conn.close() + + +def update_group_description(vlan_id, description): + conn = get_connection() + cursor = conn.cursor() + cursor.execute("UPDATE groups SET description = %s WHERE id = %s", (description, vlan_id)) + conn.commit() + cursor.close() + conn.close() + + +def delete_group(vlan_id): + conn = get_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM groups WHERE id = %s", (vlan_id,)) + conn.commit() + cursor.close() + conn.close() + + +def duplicate_group(vlan_id): + conn = get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT vlan_id, description FROM groups WHERE id = %s", (vlan_id,)) + group = cursor.fetchone() + + if group: + new_vlan_id = int(group['vlan_id']) + 1 # Auto-increment logic + new_description = f"{group['description']} Copy" if group['description'] else None + cursor.execute("INSERT INTO groups (vlan_id, description) VALUES (%s, %s)", (new_vlan_id, new_description)) + conn.commit() + + cursor.close() + conn.close() + + +def add_user(mac_address, description, vlan_id): + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "INSERT INTO users (mac_address, description, vlan_id) VALUES (%s, %s, %s)", + (mac_address.lower(), description, vlan_id) + ) + conn.commit() + cursor.close() + conn.close() + + +def update_user_description(mac_address, description): + conn = get_connection() + cursor = conn.cursor() + cursor.execute("UPDATE users SET description = %s WHERE mac_address = %s", (description, mac_address.lower())) + conn.commit() + cursor.close() + conn.close() + + +def update_user_vlan(mac_address, vlan_id): + conn = get_connection() + cursor = conn.cursor() + cursor.execute("UPDATE users SET vlan_id = %s WHERE mac_address = %s", (vlan_id, mac_address.lower())) + conn.commit() + cursor.close() + conn.close() + + +def delete_user(mac_address): + conn = get_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM users WHERE mac_address = %s", (mac_address.lower(),)) + conn.commit() + cursor.close() + conn.close() + + +def get_latest_auth_logs(result, limit=10): + conn = get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM auth_logs WHERE result = %s ORDER BY timestamp DESC LIMIT %s", + (result, limit) + ) + logs = cursor.fetchall() + cursor.close() + conn.close() + return logs + + +def get_vendor_info(mac): + conn = get_connection() + cursor = conn.cursor(dictionary=True) + prefix = mac.lower().replace(":", "").replace("-", "")[:6] + + cursor.execute("SELECT vendor FROM mac_vendors WHERE prefix = %s", (prefix,)) + row = cursor.fetchone() + cursor.close() + conn.close() + + return row['vendor'] if row else "Unknown Vendor" + + +def get_summary_counts(): + conn = get_connection() + cursor = conn.cursor(dictionary=True) + + cursor.execute("SELECT COUNT(*) AS count FROM users") + total_users = cursor.fetchone()['count'] + + cursor.execute("SELECT COUNT(*) AS count FROM groups") + total_groups = cursor.fetchone()['count'] + + cursor.close() + conn.close() + return total_users, total_groups + +def update_description(mac_address, description): + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "UPDATE users SET description = %s WHERE mac_address = %s", + (description, mac_address.lower()) + ) + conn.commit() + cursor.close() + conn.close() + +def update_vlan(mac_address, vlan_id): + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "UPDATE users SET vlan_id = %s WHERE mac_address = %s", + (vlan_id, mac_address.lower()) + ) + conn.commit() + cursor.close() + conn.close() + +def refresh_vendors(): + conn = get_connection() + cursor = conn.cursor(dictionary=True) + + # Fetch all MACs from users table that are missing vendor data + cursor.execute(""" + SELECT DISTINCT SUBSTRING(REPLACE(REPLACE(mac_address, ':', ''), '-', ''), 1, 6) AS mac_prefix + FROM users + WHERE NOT EXISTS ( + SELECT 1 FROM mac_vendors WHERE mac_prefix = SUBSTRING(REPLACE(REPLACE(users.mac_address, ':', ''), '-', ''), 1, 6) + ) + """) + prefixes = [row['mac_prefix'].lower() for row in cursor.fetchall()] + cursor.close() + + if not prefixes: + conn.close() + return + + url_template = current_app.config.get("OUI_API_URL", "https://api.maclookup.app/v2/macs/{}") + api_key = current_app.config.get("OUI_API_KEY", "") + rate_limit = int(current_app.config.get("OUI_API_LIMIT_PER_SEC", 2)) + daily_limit = int(current_app.config.get("OUI_API_DAILY_LIMIT", 10000)) + + headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} + + inserted = 0 + cursor = conn.cursor() + + for i, prefix in enumerate(prefixes): + if inserted >= daily_limit: + break + + try: + url = url_template.format(prefix) + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + vendor_name = data.get("company", "not found") + status = "found" + elif response.status_code == 404: + vendor_name = "not found" + status = "not_found" + else: + print(f"Error {response.status_code} for {prefix}") + continue # skip insert on unexpected status + + cursor.execute(""" + INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated) + VALUES (%s, %s, %s, NOW(), NOW()) + ON DUPLICATE KEY UPDATE + vendor_name = VALUES(vendor_name), + status = VALUES(status), + last_checked = NOW(), + last_updated = NOW() + """, (prefix, vendor_name, status)) + conn.commit() + inserted += 1 + + except Exception as e: + print(f"Error fetching vendor for {prefix}: {e}") + continue + + time.sleep(1.0 / rate_limit) + + cursor.close() + conn.close() diff --git a/app/templates/base.html b/app/templates/base.html index 2507dc5..a97dbe4 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,245 +1,152 @@
- - - - - - - - -| Group Name | -Attribute | -Op | -Value | +VLAN ID | +Description | +User Count | Actions |
|---|---|---|---|---|---|---|---|
| - | - | - - | -- | - - - | -|||
| - | + | {{ group.vlan_id }} | - - - - 🗑️ - + + | +{{ group.user_count }} | ++ | ||
| - | - | - - | -- | - - - 🗑️ - | -|||
{{ total_users }}
{{ total_groups }}
| {{ entry.username }} | +{{ entry.mac_address }} | {{ entry.description or '' }} | {{ entry.vendor }} | {{ entry.ago }} | @@ -30,7 +30,7 @@
| {{ entry.username }} | +{{ entry.mac_address }} | {{ entry.description or '' }} | {{ entry.vendor }} | {{ entry.ago }} | {% if entry.already_exists %} - Already exists in {{ entry.existing_vlan or 'unknown VLAN' }} + Already exists in VLAN {{ entry.existing_vlan or 'unknown' }} {% else %} |