getting there
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
17
app/app.py
17
app/app.py
@@ -2,23 +2,15 @@ from flask import Flask, redirect, url_for, render_template
|
|||||||
from views.index_views import index
|
from views.index_views import index
|
||||||
from views.user_views import user
|
from views.user_views import user
|
||||||
from views.group_views import group
|
from views.group_views import group
|
||||||
from config import app_config as config_class
|
from config import app_config
|
||||||
from database import init_app
|
|
||||||
|
|
||||||
import logging, os, pytz
|
|
||||||
|
import logging, os
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
# Instantiate config class
|
|
||||||
app_config = config_class()
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(app_config)
|
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'):
|
if app.config.get('LOG_TO_FILE'):
|
||||||
log_file = app.config.get('LOG_FILE_PATH', '/app/logs/app.log')
|
log_file = app.config.get('LOG_FILE_PATH', '/app/logs/app.log')
|
||||||
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
||||||
@@ -39,7 +31,8 @@ def legacy_user_list():
|
|||||||
|
|
||||||
@app.route('/groups')
|
@app.route('/groups')
|
||||||
def legacy_group_list():
|
def legacy_group_list():
|
||||||
return redirect(url_for('group.groups'))
|
return redirect(url_for('group.group_list'))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index_redirect():
|
def index_redirect():
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import os, pytz
|
import os
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
TESTING = False
|
TESTING = False
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'insecure-default-key')
|
||||||
SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'default-insecure-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
|
# Logging
|
||||||
LOG_TO_FILE = os.getenv('LOG_TO_FILE', 'false').lower() == 'true'
|
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_URL = os.getenv('OUI_API_URL', 'https://api.maclookup.app/v2/macs/{}')
|
||||||
OUI_API_KEY = os.getenv('OUI_API_KEY', '')
|
OUI_API_KEY = os.getenv('OUI_API_KEY', '')
|
||||||
OUI_API_LIMIT_PER_SEC = int(os.getenv('OUI_API_LIMIT_PER_SEC', '2'))
|
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')
|
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):
|
class DevelopmentConfig(Config):
|
||||||
"""Development configuration."""
|
|
||||||
DEBUG = True
|
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):
|
class ProductionConfig(Config):
|
||||||
"""Production configuration."""
|
DEBUG = False
|
||||||
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
|
# Runtime selection
|
||||||
if os.getenv('FLASK_ENV') == 'production':
|
if os.getenv('FLASK_ENV') == 'production':
|
||||||
app_config = ProductionConfig
|
app_config = ProductionConfig
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,34 +1,20 @@
|
|||||||
import mysql.connector
|
import mysql.connector
|
||||||
from flask import current_app, g
|
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():
|
def get_db():
|
||||||
if 'db' not in g:
|
if 'db' not in g:
|
||||||
if 'db_pool' not in current_app.config:
|
g.db = mysql.connector.connect(
|
||||||
current_app.config['db_pool'] = init_db_pool()
|
host=current_app.config['DB_HOST'],
|
||||||
g.db = current_app.config['db_pool'].get_connection()
|
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
|
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):
|
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()
|
||||||
|
|||||||
272
app/db_interface.py
Normal file
272
app/db_interface.py
Normal file
@@ -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()
|
||||||
@@ -1,245 +1,152 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
|
<meta charset="UTF-8">
|
||||||
<meta name="description" content="FreeRADIUS Web Manager">
|
<title>{% block title %}RadMac{% endblock %}</title>
|
||||||
<meta name="author" content="Simon Cloutier">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta property="og:title" content="FreeRADIUS Manager">
|
<style>
|
||||||
<meta property="og:description" content="Manage FreeRADIUS MAC authentication visually">
|
:root {
|
||||||
<meta property="og:type" content="website">
|
--bg: #121212;
|
||||||
<meta charset="UTF-8" />
|
--fg: #f0f0f0;
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
--accent: #4caf50;
|
||||||
<title>{% block title %}FreeRADIUS Manager{% endblock %}</title>
|
--cell-bg: #1e1e1e;
|
||||||
<style>
|
--card-bg: #2c2c2c;
|
||||||
:root {
|
}
|
||||||
--bg: #ffffff;
|
|
||||||
--fg: #000000;
|
|
||||||
--cell-bg: #f5f5f5;
|
|
||||||
--th-bg: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme="light"] {
|
||||||
--bg: #121212;
|
--bg: #f8f9fa;
|
||||||
--fg: #e0e0e0;
|
--fg: #212529;
|
||||||
--cell-bg: #1e1e1e;
|
--accent: #28a745;
|
||||||
--th-bg: #2c2c2c;
|
--cell-bg: #ffffff;
|
||||||
}
|
--card-bg: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font-family: sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 1rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
background-color: var(--th-bg);
|
background-color: var(--card-bg);
|
||||||
padding: 10px;
|
padding: 1rem;
|
||||||
display: flex;
|
border-bottom: 1px solid #666;
|
||||||
align-items: center;
|
display: flex;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
nav a {
|
nav .links a {
|
||||||
margin-right: 10px;
|
margin-right: 1rem;
|
||||||
text-decoration: none;
|
color: var(--fg);
|
||||||
padding: 5px 10px;
|
text-decoration: none;
|
||||||
border: 1px solid #ccc;
|
font-weight: bold;
|
||||||
border-radius: 5px;
|
}
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a.active {
|
nav .links a:hover {
|
||||||
background-color: var(--cell-bg);
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
nav .right {
|
||||||
padding: 20px;
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
#theme-toggle {
|
button#theme-toggle {
|
||||||
margin-left: auto;
|
background: none;
|
||||||
cursor: pointer;
|
border: 1px solid var(--fg);
|
||||||
padding: 5px 10px;
|
padding: 4px 8px;
|
||||||
border-radius: 5px;
|
color: var(--fg);
|
||||||
border: 1px solid #ccc;
|
cursor: pointer;
|
||||||
background-color: var(--cell-bg);
|
border-radius: 4px;
|
||||||
color: var(--fg);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.styled-table {
|
h1, h2, h3 {
|
||||||
border-collapse: collapse;
|
color: var(--fg);
|
||||||
width: 100%;
|
}
|
||||||
margin-bottom: 2rem;
|
|
||||||
background-color: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.styled-table th,
|
.toast {
|
||||||
.styled-table td {
|
position: fixed;
|
||||||
border: 1px solid #444;
|
bottom: 20px;
|
||||||
padding: 8px;
|
right: 20px;
|
||||||
text-align: left;
|
background-color: var(--accent);
|
||||||
}
|
color: white;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
.styled-table input,
|
.toast.show {
|
||||||
.styled-table select {
|
opacity: 1;
|
||||||
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,
|
table.styled-table {
|
||||||
.styled-table select:focus {
|
border-collapse: collapse;
|
||||||
background-color: #555;
|
width: 100%;
|
||||||
outline: none;
|
margin-top: 1rem;
|
||||||
}
|
background-color: var(--cell-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.styled-table thead {
|
.styled-table th, .styled-table td {
|
||||||
background-color: var(--th-bg);
|
padding: 8px 12px;
|
||||||
position: sticky;
|
border: 1px solid #555;
|
||||||
top: 0;
|
}
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.styled-table tbody tr:hover {
|
.styled-table th {
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: var(--card-bg);
|
||||||
}
|
color: var(--fg);
|
||||||
|
}
|
||||||
.icon-button {
|
</style>
|
||||||
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; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/" {% if request.path == '/' %}class="active"{% endif %}>Home</a>
|
<div class="links">
|
||||||
<a href="/user/user_list" {% if request.path.startswith('/user') %}class="active"{% endif %}>User List</a>
|
<a href="/">Home</a>
|
||||||
<a href="/group/groups" {% if request.path.startswith('/group') %}class="active"{% endif %}>Group List</a>
|
<a href="/user_list">Users</a>
|
||||||
<a href="/stats" {% if request.path.startswith('/stats') %}class="active"{% endif %}>Stats</a>
|
<a href="/group">Groups</a>
|
||||||
<button id="theme-toggle" onclick="toggleTheme()">🌓</button>
|
<a href="/stats">Stats</a>
|
||||||
</nav>
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<button id="theme-toggle">🌓 Theme</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="content">
|
{% block content %}
|
||||||
{% block content %}{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="toast-container"></div>
|
<div id="toast" class="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function showToast(message) {
|
// Theme toggle logic
|
||||||
const toast = document.createElement('div');
|
const toggleBtn = document.getElementById('theme-toggle');
|
||||||
toast.textContent = message;
|
const userPref = localStorage.getItem('theme');
|
||||||
toast.style.cssText = `
|
|
||||||
position: fixed;
|
|
||||||
bottom: 1rem;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background: var(--cell-bg, #333);
|
|
||||||
color: var(--fg, #fff);
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
|
||||||
z-index: 9999;
|
|
||||||
font-weight: bold;
|
|
||||||
`;
|
|
||||||
document.body.appendChild(toast);
|
|
||||||
setTimeout(() => toast.remove(), 3000);
|
|
||||||
}
|
|
||||||
function toggleTheme() {
|
|
||||||
const current = document.body.dataset.theme || 'light';
|
|
||||||
const next = current === 'light' ? 'dark' : 'light';
|
|
||||||
document.body.dataset.theme = next;
|
|
||||||
localStorage.setItem('theme', next);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = () => {
|
if (userPref) {
|
||||||
document.body.dataset.theme = localStorage.getItem('theme') || 'light';
|
document.documentElement.setAttribute('data-theme', userPref);
|
||||||
};
|
}
|
||||||
</script>
|
|
||||||
|
toggleBtn.addEventListener('click', () => {
|
||||||
<script>
|
const current = document.documentElement.getAttribute('data-theme');
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
const next = current === 'light' ? 'dark' : 'light';
|
||||||
{% if messages %}
|
document.documentElement.setAttribute('data-theme', next);
|
||||||
{% for category, message in messages %}
|
localStorage.setItem('theme', next);
|
||||||
showToast("{{ message }}");
|
});
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
// Toast display function
|
||||||
{% endwith %}
|
function showToast(message, duration = 3000) {
|
||||||
</script>
|
const toast = document.getElementById('toast');
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.classList.add('show');
|
||||||
|
setTimeout(() => toast.classList.remove('show'), duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make toast function globally available
|
||||||
|
window.showToast = showToast;
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,183 +1,52 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}Group List{% endblock %}
|
{% block title %}VLAN Groups{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="page-title">Group List</h1>
|
<h1 class="page-title">VLAN Groups</h1>
|
||||||
|
|
||||||
<table class="styled-table fade-in">
|
<form method="POST" action="{{ url_for('group.add_group_route') }}" style="margin-bottom: 1rem;">
|
||||||
|
<input type="text" name="vlan_id" placeholder="VLAN ID" required pattern="[0-9]+" style="width: 80px;">
|
||||||
|
<input type="text" name="description" placeholder="Group Description">
|
||||||
|
<button type="submit">➕ Add Group</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table class="styled-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Group Name</th>
|
<th>VLAN ID</th>
|
||||||
<th>Attribute</th>
|
<th>Description</th>
|
||||||
<th>Op</th>
|
<th>User Count</th>
|
||||||
<th>Value</th>
|
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="group-body">
|
<tbody>
|
||||||
<!-- New Group Entry Row -->
|
{% for group in groups %}
|
||||||
<tr class="new-row">
|
|
||||||
<td rowspan="1"><input type="text" id="new-groupname" placeholder="New group" /></td>
|
|
||||||
<td><input type="text" class="new-attribute" placeholder="Attribute"></td>
|
|
||||||
<td>
|
|
||||||
<select class="new-op">
|
|
||||||
<option value="">Op</option>
|
|
||||||
<option value="=">=</option>
|
|
||||||
<option value="!=">!=</option>
|
|
||||||
<option value=">">></option>
|
|
||||||
<option value="<"><</option>
|
|
||||||
<option value=">=">>=</option>
|
|
||||||
<option value="<="><=</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td><input type="text" class="new-value" placeholder="Value"></td>
|
|
||||||
<td>
|
|
||||||
<button class="icon-button pulse" onclick="saveNewGroup()" title="Save Group">💾</button>
|
|
||||||
<button class="icon-button" onclick="addAttributeRow()" title="Add Attribute">➕</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{% for groupname, attributes in grouped_results.items() %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><input type="text" id="groupname-{{ groupname }}" value="{{ groupname }}" disabled></td>
|
<td>{{ group.vlan_id }}</td>
|
||||||
<td colspan="3" class="merged-cell"></td>
|
|
||||||
<td>
|
<td>
|
||||||
<button class="icon-button" onclick="enableEdit('{{ groupname }}')" title="Edit">✏️</button>
|
<form method="POST" action="{{ url_for('group.update_description_route') }}" class="inline-form">
|
||||||
<button class="icon-button" onclick="updateGroupName('{{ groupname }}')" title="Save">💾</button>
|
<input type="hidden" name="group_id" value="{{ group.vlan_id }}">
|
||||||
<button class="icon-button" onclick="location.reload()" title="Cancel">❌</button>
|
<input type="text" name="description" value="{{ group.description or '' }}">
|
||||||
<a class="icon-button" href="{{ url_for('group.delete_group_rows', groupname=groupname) }}" onclick="saveScrollPosition()" title="Delete Group">🗑️</a>
|
<button type="submit" title="Save">💾</button>
|
||||||
<button class="icon-button" onclick="duplicateToNewGroup('{{ groupname }}')" title="Duplicate">📄</button>
|
</form>
|
||||||
|
</td>
|
||||||
|
<td>{{ group.user_count }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="{{ url_for('group.delete_group_route') }}" onsubmit="return confirm('Delete this group?');">
|
||||||
|
<input type="hidden" name="group_id" value="{{ group.vlan_id }}">
|
||||||
|
<button type="submit">❌</button>
|
||||||
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% for attribute in attributes %}
|
|
||||||
<tr>
|
|
||||||
<td class="merged-cell"></td>
|
|
||||||
<td><input type="text" id="attribute-{{ attribute.id }}" value="{{ attribute.attribute }}"></td>
|
|
||||||
<td>
|
|
||||||
<select id="op-{{ attribute.id }}">
|
|
||||||
<option value="=" {% if attribute.op == '=' %}selected{% endif %}>=</option>
|
|
||||||
<option value="!=" {% if attribute.op == '!=' %}selected{% endif %}>!=</option>
|
|
||||||
<option value=">" {% if attribute.op == '>' %}selected{% endif %}>></option>
|
|
||||||
<option value="<" {% if attribute.op == '<' %}selected{% endif %}><</option>
|
|
||||||
<option value=">=" {% if attribute.op == '>=' %}selected{% endif %}>>=</option>
|
|
||||||
<option value="<=" {% if attribute.op == '<=' %}selected{% endif %}><=</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td><input type="text" id="value-{{ attribute.id }}" value="{{ attribute.value }}"></td>
|
|
||||||
<td>
|
|
||||||
<button class="icon-button" onclick="updateAttribute('{{ attribute.id }}')" title="Save">💾</button>
|
|
||||||
<button class="icon-button" onclick="location.reload()" title="Reset">❌</button>
|
|
||||||
<a class="icon-button" href="{{ url_for('group.delete_group', group_id=attribute.id) }}" onclick="saveScrollPosition()" title="Delete">🗑️</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<script>
|
<style>
|
||||||
function enableEdit(groupname) {
|
form.inline-form {
|
||||||
const input = document.getElementById(`groupname-${groupname}`);
|
display: inline-flex;
|
||||||
input.disabled = false;
|
gap: 4px;
|
||||||
input.focus();
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
function saveScrollPosition() {
|
|
||||||
sessionStorage.setItem("scrollPosition", window.scrollY);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addAttributeRow() {
|
|
||||||
const table = document.getElementById("group-body");
|
|
||||||
const row = document.createElement("tr");
|
|
||||||
row.classList.add("new-attribute-row");
|
|
||||||
row.innerHTML = `
|
|
||||||
<td class="merged-cell"></td>
|
|
||||||
<td><input type="text" class="new-attribute" placeholder="Attribute"></td>
|
|
||||||
<td>
|
|
||||||
<select class="new-op">
|
|
||||||
<option value="">Op</option>
|
|
||||||
<option value="=">=</option>
|
|
||||||
<option value="!=">!=</option>
|
|
||||||
<option value=">">></option>
|
|
||||||
<option value="<"><</option>
|
|
||||||
<option value=">=">>=</option>
|
|
||||||
<option value="<="><=</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td><input type="text" class="new-value" placeholder="Value"></td>
|
|
||||||
<td><button class="icon-button" onclick="this.closest('tr').remove()" title="Remove">🗑️</button></td>
|
|
||||||
`;
|
|
||||||
table.insertBefore(row, table.querySelector(".new-row").nextSibling);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveNewGroup() {
|
|
||||||
const groupname = document.getElementById("new-groupname").value;
|
|
||||||
const attributes = [];
|
|
||||||
const attrInputs = document.querySelectorAll(".new-attribute");
|
|
||||||
|
|
||||||
attrInputs.forEach((attrInput, index) => {
|
|
||||||
const attribute = attrInput.value;
|
|
||||||
const op = document.querySelectorAll(".new-op")[index].value;
|
|
||||||
const value = document.querySelectorAll(".new-value")[index].value;
|
|
||||||
|
|
||||||
if (attribute && op && value) {
|
|
||||||
attributes.push({ attribute, op, value });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!groupname || attributes.length === 0) {
|
|
||||||
showToast("Group name and at least one attribute required.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch("/group/save_group", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ groupname, attributes })
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
showToast("Group saved.");
|
|
||||||
setTimeout(() => location.reload(), 800);
|
|
||||||
} else {
|
|
||||||
showToast("Error: " + data.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function duplicateToNewGroup(groupname) {
|
|
||||||
fetch("/group/duplicate_group", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
||||||
body: `groupname=${groupname}`
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
document.getElementById("new-groupname").value = data.new_groupname;
|
|
||||||
|
|
||||||
const oldAttrRows = document.querySelectorAll(".new-attribute-row");
|
|
||||||
oldAttrRows.forEach(row => row.remove());
|
|
||||||
|
|
||||||
data.attributes.forEach(attr => {
|
|
||||||
addAttributeRow();
|
|
||||||
const index = document.querySelectorAll(".new-attribute").length - 1;
|
|
||||||
document.querySelectorAll(".new-attribute")[index].value = attr.attribute;
|
|
||||||
document.querySelectorAll(".new-op")[index].value = attr.op;
|
|
||||||
document.querySelectorAll(".new-value")[index].value = attr.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("new-groupname").scrollIntoView({ behavior: 'smooth' });
|
|
||||||
showToast("Fields populated from duplicated group.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = function () {
|
|
||||||
const scrollPosition = sessionStorage.getItem("scrollPosition");
|
|
||||||
if (scrollPosition) {
|
|
||||||
window.scrollTo(0, parseInt(scrollPosition) - 100);
|
|
||||||
sessionStorage.removeItem("scrollPosition");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}FreeRADIUS Manager{% endblock %}
|
{% block title %}RadMac{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="page-title">FreeRADIUS Manager</h1>
|
<h1 class="page-title">RadMac</h1>
|
||||||
|
|
||||||
<div class="stats-cards">
|
<div class="stats-cards">
|
||||||
<div class="card neutral">
|
<div class="card neutral">
|
||||||
<strong>Total Users</strong>
|
<strong>Total MAC Addresses</strong>
|
||||||
<p>{{ total_users }}</p>
|
<p>{{ total_users }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card neutral">
|
<div class="card neutral">
|
||||||
<strong>Total Groups</strong>
|
<strong>Total VLAN Groups</strong>
|
||||||
<p>{{ total_groups }}</p>
|
<p>{{ total_groups }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Recent Access Accepts</h2>
|
<h2>Recent Access-Accept</h2>
|
||||||
<ul class="event-list green">
|
<ul class="event-list green">
|
||||||
{% for entry in latest_accept %}
|
{% for entry in latest_accept %}
|
||||||
<li>
|
<li>
|
||||||
<strong>{{ entry.username }}</strong>
|
<strong>{{ entry.mac_address }}</strong>
|
||||||
{% if entry.description %} ({{ entry.description }}){% endif %}
|
{% if entry.description %} ({{ entry.description }}){% endif %}
|
||||||
— {{ entry.ago }}
|
— {{ entry.ago }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Recent Access Rejects</h2>
|
<h2>Recent Access-Reject</h2>
|
||||||
<ul class="event-list red">
|
<ul class="event-list red">
|
||||||
{% for entry in latest_reject %}
|
{% for entry in latest_reject %}
|
||||||
<li>
|
<li>
|
||||||
<strong>{{ entry.username }}</strong>
|
<strong>{{ entry.mac_address }}</strong>
|
||||||
{% if entry.description %} ({{ entry.description }}){% endif %}
|
{% if entry.description %} ({{ entry.description }}){% endif %}
|
||||||
— {{ entry.ago }}
|
— {{ entry.ago }}
|
||||||
</li>
|
</li>
|
||||||
@@ -74,7 +74,6 @@ document.getElementById('mac-lookup-form').addEventListener('submit', function(e
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-cards {
|
.stats-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -101,7 +100,6 @@ document.getElementById('mac-lookup-form').addEventListener('submit', function(e
|
|||||||
}
|
}
|
||||||
.event-list.green li { color: #4caf50; }
|
.event-list.green li { color: #4caf50; }
|
||||||
.event-list.red li { color: #ff4d4d; }
|
.event-list.red li { color: #ff4d4d; }
|
||||||
|
|
||||||
#mac-lookup-form input {
|
#mac-lookup-form input {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<div class="stats-container">
|
<div class="stats-container">
|
||||||
<div class="card success-card">
|
<div class="card success-card">
|
||||||
<h2>Last Access-Accept Events</h2>
|
<h2>Recent Access-Accept</h2>
|
||||||
<table class="styled-table small-table">
|
<table class="styled-table small-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for entry in accept_entries %}
|
{% for entry in accept_entries %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ entry.username }}</td>
|
<td>{{ entry.mac_address }}</td>
|
||||||
<td>{{ entry.description or '' }}</td>
|
<td>{{ entry.description or '' }}</td>
|
||||||
<td>{{ entry.vendor }}</td>
|
<td>{{ entry.vendor }}</td>
|
||||||
<td>{{ entry.ago }}</td>
|
<td>{{ entry.ago }}</td>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card error-card">
|
<div class="card error-card">
|
||||||
<h2>Last Access-Reject Events</h2>
|
<h2>Recent Access-Reject</h2>
|
||||||
<table class="styled-table small-table">
|
<table class="styled-table small-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -44,20 +44,20 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for entry in reject_entries %}
|
{% for entry in reject_entries %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ entry.username }}</td>
|
<td>{{ entry.mac_address }}</td>
|
||||||
<td>{{ entry.description or '' }}</td>
|
<td>{{ entry.description or '' }}</td>
|
||||||
<td>{{ entry.vendor }}</td>
|
<td>{{ entry.vendor }}</td>
|
||||||
<td>{{ entry.ago }}</td>
|
<td>{{ entry.ago }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if entry.already_exists %}
|
{% if entry.already_exists %}
|
||||||
<span style="color: limegreen;">Already exists in {{ entry.existing_vlan or 'unknown VLAN' }}</span>
|
<span style="color: limegreen;">Already exists in VLAN {{ entry.existing_vlan or 'unknown' }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="POST" action="/user/add_from_reject" style="display: flex; gap: 4px;">
|
<form method="POST" action="/user/add_from_reject" style="display: flex; gap: 4px;">
|
||||||
<input type="hidden" name="username" value="{{ entry.username }}">
|
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
||||||
<select name="groupname" required>
|
<select name="group_id" required>
|
||||||
<option value="">Select VLAN</option>
|
<option value="">Select VLAN</option>
|
||||||
{% for group in available_groups %}
|
{% for group in available_groups %}
|
||||||
<option value="{{ group }}">{{ group }}</option>
|
<option value="{{ group.id }}">VLAN {{ group.vlan_id }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" title="Add User">💾</button>
|
<button type="submit" title="Add User">💾</button>
|
||||||
|
|||||||
@@ -1,61 +1,62 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}User List{% endblock %}
|
{% block title %}MAC Address List{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="page-title">User List</h1>
|
<h1 class="page-title">MAC Address List</h1>
|
||||||
|
|
||||||
<table class="styled-table fade-in">
|
<form id="add-user-form" method="POST" action="{{ url_for('user.add') }}">
|
||||||
|
<input type="text" name="mac_address" placeholder="MAC address (12 hex characters)" required maxlength="12">
|
||||||
|
<input type="text" name="description" placeholder="Description (optional)">
|
||||||
|
<select name="group_id" required>
|
||||||
|
<option value="">Assign to VLAN</option>
|
||||||
|
{% for group in groups %}
|
||||||
|
<option value="{{ group.id }}">VLAN {{ group.vlan_id }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button type="submit">➕ Add</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table class="styled-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>MAC Address</th>
|
<th>MAC Address</th>
|
||||||
<th>
|
|
||||||
Vendor
|
|
||||||
<button class="icon-button" onclick="refreshVendors(this)" title="Refresh Vendor">🔄</button>
|
|
||||||
</th>
|
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Group</th>
|
<th>Vendor <button id="refresh-vendors" title="Refresh unknown vendors">🔄</button></th>
|
||||||
|
<th>VLAN</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="user-body">
|
<tbody>
|
||||||
<!-- New User Row -->
|
{% for entry in users %}
|
||||||
<tr class="new-row">
|
|
||||||
<td><input type="text" id="new-mac" placeholder="MAC address"></td>
|
|
||||||
<td><em>(auto)</em></td>
|
|
||||||
<td><input type="text" id="new-description" placeholder="Description"></td>
|
|
||||||
<td>
|
|
||||||
<select id="new-vlan">
|
|
||||||
<option value="">-- Select Group --</option>
|
|
||||||
{% for group in groups %}
|
|
||||||
<option value="{{ group.groupname }}">{{ group.groupname }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button class="icon-button pulse" onclick="addUser()" title="Save User">💾</button>
|
|
||||||
<button class="icon-button" onclick="clearUserFields()" title="Reset">❌</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{% for row in results %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><input type="text" value="{{ row.mac_address }}" id="mac-{{ loop.index }}" disabled></td>
|
<td>{{ entry.mac_address }}</td>
|
||||||
<td>{{ row.vendor or 'Unknown Vendor' }}</td>
|
|
||||||
<td><input type="text" value="{{ row.description }}" id="desc-{{ loop.index }}"></td>
|
|
||||||
<td>
|
<td>
|
||||||
<select id="vlan-{{ loop.index }}">
|
<form method="POST" action="{{ url_for('user.update_description_route') }}" class="inline-form">
|
||||||
{% for group in groups %}
|
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
||||||
<option value="{{ group.groupname }}" {% if group.groupname == row.vlan_id %}selected{% endif %}>
|
<input type="text" name="description" value="{{ entry.description or '' }}">
|
||||||
{{ group.groupname }}
|
<button type="submit" title="Save">💾</button>
|
||||||
</option>
|
</form>
|
||||||
{% endfor %}
|
</td>
|
||||||
</select>
|
<td>{% if entry.vendor_name %}
|
||||||
|
{{ entry.vendor_name }}
|
||||||
|
{% else %}
|
||||||
|
<em>Unknown</em>
|
||||||
|
{% endif %}</td>
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="{{ url_for('user.update_vlan_route') }}" class="inline-form">
|
||||||
|
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
||||||
|
<select name="group_id" onchange="this.form.submit()">
|
||||||
|
{% for group in groups %}
|
||||||
|
<option value="{{ group.id }}" {% if group.vlan_id == entry.vlan_id %}selected{% endif %}>VLAN {{ group.vlan_id }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="icon-button" onclick="enableUserEdit({{ loop.index }})" title="Edit">✏️</button>
|
<form method="POST" action="{{ url_for('user.delete') }}">
|
||||||
<button class="icon-button" onclick="updateUser({{ loop.index }}, '{{ row.mac_address }}')" title="Save">💾</button>
|
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
||||||
<button class="icon-button" onclick="location.reload()" title="Cancel">❌</button>
|
<button type="submit" onclick="return confirm('Delete this MAC address?')">❌</button>
|
||||||
<a class="icon-button" href="/user/delete_user/{{ row.mac_address }}" onclick="saveScrollPosition()" title="Delete">🗑️</a>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -63,117 +64,22 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function enableUserEdit(index) {
|
document.getElementById('refresh-vendors').addEventListener('click', function () {
|
||||||
const input = document.getElementById(`mac-${index}`);
|
fetch("{{ url_for('user.refresh') }}", { method: "POST" })
|
||||||
input.disabled = false;
|
.then(res => res.json())
|
||||||
input.focus();
|
.then(data => {
|
||||||
}
|
alert("Vendor refresh complete.");
|
||||||
|
window.location.reload();
|
||||||
function clearUserFields() {
|
})
|
||||||
document.getElementById("new-mac").value = "";
|
.catch(err => alert("Error: " + err));
|
||||||
document.getElementById("new-description").value = "";
|
});
|
||||||
document.getElementById("new-vlan").selectedIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addUser() {
|
|
||||||
const mac = document.getElementById("new-mac").value;
|
|
||||||
const desc = document.getElementById("new-description").value;
|
|
||||||
const vlan = document.getElementById("new-vlan").value;
|
|
||||||
|
|
||||||
if (!mac || !vlan) {
|
|
||||||
showToast("MAC address and group are required.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch("/user/add_user", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ mac_address: mac, description: desc, vlan_id: vlan })
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
showToast("User added.");
|
|
||||||
setTimeout(() => location.reload(), 800);
|
|
||||||
} else {
|
|
||||||
showToast("Error: " + data.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUser(index, originalMac) {
|
|
||||||
const macInput = document.getElementById(`mac-${index}`);
|
|
||||||
const desc = document.getElementById(`desc-${index}`).value;
|
|
||||||
const vlan = document.getElementById(`vlan-${index}`).value;
|
|
||||||
|
|
||||||
fetch("/user/update_user", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
||||||
body: `mac_address=${originalMac}&new_mac_address=${macInput.value}&description=${desc}&vlan_id=${vlan}`
|
|
||||||
})
|
|
||||||
.then(res => res.text())
|
|
||||||
.then(data => {
|
|
||||||
if (data === "success") {
|
|
||||||
showToast("User updated.");
|
|
||||||
setTimeout(() => location.reload(), 800);
|
|
||||||
} else {
|
|
||||||
showToast("Update failed: " + data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// function refreshVendors() {
|
|
||||||
// showToast("Refreshing vendor info...");
|
|
||||||
// fetch("/user/refresh_vendors", {
|
|
||||||
// method: "POST"
|
|
||||||
// })
|
|
||||||
// .then(res => res.json())
|
|
||||||
// .then(data => {
|
|
||||||
// showToast(data.message || "Refreshed.");
|
|
||||||
// setTimeout(() => location.reload(), 1200);
|
|
||||||
// })
|
|
||||||
// .catch(() => showToast("Failed to refresh vendor info."));
|
|
||||||
// }
|
|
||||||
|
|
||||||
function refreshVendors(btn) {
|
|
||||||
btn.disabled = true;
|
|
||||||
showToast("Refreshing vendor info...");
|
|
||||||
|
|
||||||
function refreshCycle() {
|
|
||||||
fetch("/user/refresh_vendors", { method: "POST" })
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
showToast(`Updated ${data.updated} vendors`);
|
|
||||||
if (data.remaining) {
|
|
||||||
setTimeout(refreshCycle, 1500); // Pause before next batch
|
|
||||||
} else {
|
|
||||||
showToast("Vendor refresh complete.");
|
|
||||||
setTimeout(() => location.reload(), 1000);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showToast("Refresh failed: " + data.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
showToast("Error during vendor refresh.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshCycle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveScrollPosition() {
|
|
||||||
sessionStorage.setItem("scrollPosition", window.scrollY);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = function () {
|
|
||||||
const scroll = sessionStorage.getItem("scrollPosition");
|
|
||||||
if (scroll) {
|
|
||||||
window.scrollTo(0, parseInt(scroll) - 100);
|
|
||||||
sessionStorage.removeItem("scrollPosition");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form.inline-form {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,201 +1,33 @@
|
|||||||
from flask import Blueprint, render_template, request, redirect, url_for, jsonify
|
from flask import Blueprint, render_template, request, redirect, url_for
|
||||||
from database import get_db
|
from db_interface import get_all_groups, add_group, update_group_description, delete_group
|
||||||
import mysql.connector
|
|
||||||
|
|
||||||
group = Blueprint('group', __name__)
|
group = Blueprint('group', __name__, url_prefix='/group')
|
||||||
|
|
||||||
@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:
|
@group.route('/')
|
||||||
cursor.execute("SELECT id, attribute, op, value FROM radgroupreply WHERE groupname = %s", (groupname,))
|
def group_list():
|
||||||
attributes = cursor.fetchall()
|
groups = get_all_groups()
|
||||||
grouped_results[groupname] = [
|
return render_template('group_list.html', groups=groups)
|
||||||
{'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.html', grouped_results=grouped_results)
|
|
||||||
|
|
||||||
except mysql.connector.Error as err:
|
@group.route('/add', methods=['POST'])
|
||||||
print(f"Database Error: {err}")
|
def add_group_route():
|
||||||
cursor.close()
|
vlan_id = request.form['vlan_id']
|
||||||
db.close()
|
desc = request.form.get('description', '')
|
||||||
return render_template('group_list.html', grouped_results={})
|
add_group(vlan_id, desc)
|
||||||
return "Database Connection Failed"
|
return redirect(url_for('group.group_list'))
|
||||||
|
|
||||||
@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:
|
@group.route('/update_description', methods=['POST'])
|
||||||
return jsonify({'error': 'Group name and attributes are required'}), 400
|
def update_description_route():
|
||||||
|
group_id = request.form['group_id']
|
||||||
|
desc = request.form.get('description', '')
|
||||||
|
update_group_description(group_id, desc)
|
||||||
|
return redirect(url_for('group.group_list'))
|
||||||
|
|
||||||
db = get_db()
|
|
||||||
cursor = db.cursor()
|
|
||||||
|
|
||||||
try:
|
@group.route('/delete', methods=['POST'])
|
||||||
# Prevent duplicates
|
def delete_group_route():
|
||||||
cursor.execute("SELECT 1 FROM radgroupcheck WHERE groupname = %s", (groupname,))
|
group_id = request.form['group_id']
|
||||||
if cursor.fetchone():
|
delete_group(group_id)
|
||||||
return jsonify({'error': f'Group name "{groupname}" already exists'}), 400
|
return redirect(url_for('group.group_list'))
|
||||||
|
|
||||||
# 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/<groupname>')
|
|
||||||
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/<int:group_id>')
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
from flask import Blueprint, render_template, request, jsonify, current_app
|
from flask import Blueprint, render_template, request, jsonify, current_app
|
||||||
from database import get_db
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import requests, pytz
|
from db_interface import (
|
||||||
|
get_connection,
|
||||||
|
get_vendor_info,
|
||||||
|
get_all_groups,
|
||||||
|
get_latest_auth_logs,
|
||||||
|
)
|
||||||
|
import pytz
|
||||||
|
|
||||||
index = Blueprint('index', __name__)
|
index = Blueprint('index', __name__)
|
||||||
OUI_API_URL = 'https://api.maclookup.app/v2/macs/{}'
|
|
||||||
|
|
||||||
|
|
||||||
import pytz # make sure it's imported if not already
|
|
||||||
|
|
||||||
def time_ago(dt):
|
def time_ago(dt):
|
||||||
if not dt:
|
if not dt:
|
||||||
return "n/a"
|
return "n/a"
|
||||||
|
|
||||||
local_tz = current_app.config.get('TZ', pytz.utc)
|
# Use configured timezone
|
||||||
|
tz_name = current_app.config.get('APP_TIMEZONE', 'UTC')
|
||||||
|
local_tz = pytz.timezone(tz_name)
|
||||||
|
|
||||||
# If the DB datetime is naive, assume it's already in local server time
|
|
||||||
if dt.tzinfo is None:
|
if dt.tzinfo is None:
|
||||||
server_tz = pytz.timezone('America/Toronto') # Or your DB server's real timezone
|
dt = dt.replace(tzinfo=pytz.utc)
|
||||||
dt = server_tz.localize(dt)
|
|
||||||
|
|
||||||
# Convert to the app's configured timezone (from .env)
|
|
||||||
dt = dt.astimezone(local_tz)
|
dt = dt.astimezone(local_tz)
|
||||||
now = datetime.now(local_tz)
|
now = datetime.now(local_tz)
|
||||||
diff = now - dt
|
diff = now - dt
|
||||||
@@ -29,91 +30,21 @@ def time_ago(dt):
|
|||||||
if seconds < 60:
|
if seconds < 60:
|
||||||
return f"{seconds}s ago"
|
return f"{seconds}s ago"
|
||||||
elif seconds < 3600:
|
elif seconds < 3600:
|
||||||
return f"{seconds//60}m{seconds%60}s ago"
|
return f"{seconds // 60}m{seconds % 60}s ago"
|
||||||
elif seconds < 86400:
|
elif seconds < 86400:
|
||||||
return f"{seconds//3600}h{(seconds%3600)//60}m ago"
|
return f"{seconds // 3600}h{(seconds % 3600) // 60}m ago"
|
||||||
else:
|
else:
|
||||||
return f"{seconds//86400}d{(seconds%86400)//3600}h ago"
|
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('/')
|
@index.route('/')
|
||||||
def homepage():
|
def homepage():
|
||||||
db = get_db()
|
total_users, total_groups = get_summary_counts()
|
||||||
latest_accept = []
|
latest_accept = get_latest_auth_logs('Access-Accept', limit=5)
|
||||||
latest_reject = []
|
latest_reject = get_latest_auth_logs('Access-Reject', limit=5)
|
||||||
total_users = 0
|
|
||||||
total_groups = 0
|
|
||||||
|
|
||||||
if db:
|
for row in latest_accept + latest_reject:
|
||||||
cursor = db.cursor(dictionary=True)
|
row['ago'] = time_ago(row['timestamp'])
|
||||||
|
|
||||||
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',
|
return render_template('index.html',
|
||||||
total_users=total_users,
|
total_users=total_users,
|
||||||
@@ -124,66 +55,15 @@ def homepage():
|
|||||||
|
|
||||||
@index.route('/stats')
|
@index.route('/stats')
|
||||||
def stats():
|
def stats():
|
||||||
db = get_db()
|
accept_entries = get_latest_auth_logs('Access-Accept', limit=25)
|
||||||
accept_entries = []
|
reject_entries = get_latest_auth_logs('Access-Reject', limit=25)
|
||||||
reject_entries = []
|
available_groups = get_all_groups()
|
||||||
available_groups = []
|
|
||||||
|
|
||||||
if db:
|
for entry in accept_entries + reject_entries:
|
||||||
cursor = db.cursor(dictionary=True)
|
entry['ago'] = time_ago(entry['timestamp'])
|
||||||
|
entry['vendor'] = get_vendor_info(entry['mac_address'])
|
||||||
# Fetch available VLANs
|
entry['already_exists'] = entry.get('vlan_id') is not None
|
||||||
cursor.execute("SELECT DISTINCT groupname FROM radgroupcheck ORDER BY groupname")
|
entry['existing_vlan'] = entry.get('vlan_id') if entry['already_exists'] else None
|
||||||
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',
|
return render_template('stats.html',
|
||||||
accept_entries=accept_entries,
|
accept_entries=accept_entries,
|
||||||
@@ -191,13 +71,24 @@ def stats():
|
|||||||
available_groups=available_groups)
|
available_groups=available_groups)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@index.route('/lookup_mac', methods=['POST'])
|
@index.route('/lookup_mac', methods=['POST'])
|
||||||
def lookup_mac():
|
def lookup_mac():
|
||||||
mac = request.form.get('mac', '').strip()
|
mac = request.form.get('mac', '').strip()
|
||||||
if not mac:
|
if not mac:
|
||||||
return jsonify({"error": "MAC address is required"}), 400
|
return jsonify({"error": "MAC address is required"}), 400
|
||||||
|
|
||||||
return jsonify(lookup_vendor(mac))
|
return jsonify(get_vendor_info(mac))
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
return total_users, total_groups
|
||||||
|
|||||||
@@ -1,276 +1,49 @@
|
|||||||
from flask import Blueprint, render_template, request, redirect, url_for, jsonify, flash
|
from flask import Blueprint, render_template, request, redirect, url_for
|
||||||
from database import get_db
|
from db_interface import get_all_users, get_all_groups, add_user, update_description, update_vlan, delete_user, refresh_vendors
|
||||||
import mysql.connector, os, time, requests
|
|
||||||
|
|
||||||
user = Blueprint('user', __name__) # ✅ Blueprint name = "user"
|
user = Blueprint('user', __name__, url_prefix='/user')
|
||||||
|
|
||||||
@user.route('/user_list')
|
|
||||||
|
@user.route('/')
|
||||||
def user_list():
|
def user_list():
|
||||||
db = get_db()
|
users = get_all_users()
|
||||||
if db is None:
|
groups = get_all_groups()
|
||||||
return "Database connection failed", 500
|
return render_template('user_list.html', users=users, groups=groups)
|
||||||
|
|
||||||
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.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'])
|
@user.route('/add', methods=['POST'])
|
||||||
def update_user():
|
def add():
|
||||||
mac_address = request.form['mac_address']
|
mac = request.form['mac_address']
|
||||||
description = request.form['description']
|
desc = request.form.get('description', '')
|
||||||
vlan_id = request.form['vlan_id']
|
group_id = request.form['group_id']
|
||||||
new_mac_address = request.form.get('new_mac_address')
|
add_user(mac, desc, group_id)
|
||||||
|
return redirect(url_for('user.user_list'))
|
||||||
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/<mac_address>')
|
@user.route('/update_description', methods=['POST'])
|
||||||
def delete_user(mac_address):
|
def update_description_route():
|
||||||
db = get_db()
|
mac = request.form['mac_address']
|
||||||
if db:
|
desc = request.form.get('description', '')
|
||||||
cursor = db.cursor()
|
update_description(mac, desc)
|
||||||
try:
|
return redirect(url_for('user.user_list'))
|
||||||
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'])
|
@user.route('/update_vlan', methods=['POST'])
|
||||||
def add_user():
|
def update_vlan_route():
|
||||||
try:
|
mac = request.form['mac_address']
|
||||||
data = request.get_json()
|
group_id = request.form['group_id']
|
||||||
mac_address = data.get('mac_address')
|
update_vlan(mac, group_id)
|
||||||
description = data.get('description')
|
return redirect(url_for('user.user_list'))
|
||||||
vlan_id = data.get('vlan_id')
|
|
||||||
|
|
||||||
if not mac_address:
|
|
||||||
return jsonify({'success': False, 'message': 'MAC Address is required'}), 400
|
|
||||||
|
|
||||||
db = get_db()
|
@user.route('/delete', methods=['POST'])
|
||||||
if db is None:
|
def delete():
|
||||||
return jsonify({'success': False, 'message': 'Database connection failed'}), 500
|
mac = request.form['mac_address']
|
||||||
|
delete_user(mac)
|
||||||
|
return redirect(url_for('user.user_list'))
|
||||||
|
|
||||||
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'])
|
@user.route('/refresh_vendors', methods=['POST'])
|
||||||
def refresh_vendors():
|
def refresh():
|
||||||
db = get_db()
|
refresh_vendors()
|
||||||
cursor = db.cursor(dictionary=True)
|
return {'status': 'OK'}
|
||||||
|
|
||||||
api_url = os.getenv('OUI_API_API_URL', 'https://api.maclookup.app/v2/macs/{}').strip('"')
|
|
||||||
api_key = os.getenv('OUI_API_API_KEY', '').strip('"')
|
|
||||||
limit = int(os.getenv('OUI_API_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})
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
if __name__ == "__main__":
|
# This file is used by Gunicorn to start the application
|
||||||
app.run()
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
services:
|
|
||||||
radius:
|
radius:
|
||||||
build:
|
build:
|
||||||
context: ./radius
|
context: ./radius
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class MacRadiusServer(Server):
|
|||||||
reply["Tunnel-Medium-Type"] = 6 # IEEE-802
|
reply["Tunnel-Medium-Type"] = 6 # IEEE-802
|
||||||
reply["Tunnel-Private-Group-Id"] = DEFAULT_VLAN_ID
|
reply["Tunnel-Private-Group-Id"] = DEFAULT_VLAN_ID
|
||||||
self.SendReplyPacket(pkt.fd, reply)
|
self.SendReplyPacket(pkt.fd, reply)
|
||||||
print(f"[INFO] MAC {mac} not found — assigned to fallback VLAN {DEFAULT_VLAN_ID}")
|
print(f"[INFO] MAC {username} not found — assigned to fallback VLAN {DEFAULT_VLAN_ID}")
|
||||||
|
|
||||||
self.SendReplyPacket(pkt.fd, reply)
|
self.SendReplyPacket(pkt.fd, reply)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user